diff options
author | Daniel Stenberg <daniel@haxx.se> | 2006-04-19 22:14:45 +0000 |
---|---|---|
committer | Daniel Stenberg <daniel@haxx.se> | 2006-04-19 22:14:45 +0000 |
commit | 3215ed2e85c957e3bff13cf7c49ab197bbf4974d (patch) | |
tree | 9f4e51a0be9db1724073c0f0e4174034a4ae80a4 | |
parent | f9bfd73a24b71c01f402a3310af1fad92c7256ed (diff) | |
download | rockbox-3215ed2e85c957e3bff13cf7c49ab197bbf4974d.tar.gz rockbox-3215ed2e85c957e3bff13cf7c49ab197bbf4974d.zip |
use tag cache
git-svn-id: svn://svn.rockbox.org/rockbox/trunk@9734 a1c6a512-1295-4272-9138-f99709370657
-rwxr-xr-x | tools/songdb.pl | 2123 | ||||
-rw-r--r-- | tools/vorbiscomm.pm | 732 |
2 files changed, 0 insertions, 2855 deletions
diff --git a/tools/songdb.pl b/tools/songdb.pl deleted file mode 100755 index dad7e10496..0000000000 --- a/tools/songdb.pl +++ /dev/null | |||
@@ -1,2123 +0,0 @@ | |||
1 | #!/usr/bin/perl | ||
2 | # | ||
3 | # Rockbox song database docs: | ||
4 | # http://www.rockbox.org/twiki/bin/view/Main/TagDatabase | ||
5 | # | ||
6 | # MP3::Info by Chris Nandor is included verbatim in this script to make | ||
7 | # it runnable standalone on removable drives. See below. | ||
8 | # | ||
9 | |||
10 | use vorbiscomm; | ||
11 | |||
12 | my $db = "rockbox.tagdb"; | ||
13 | my $dir; | ||
14 | my $strip; | ||
15 | my $add; | ||
16 | my $verbose; | ||
17 | my $help; | ||
18 | my $dirisalbum; | ||
19 | my $dirisalbumname; | ||
20 | my $crc = 1; | ||
21 | |||
22 | while($ARGV[0]) { | ||
23 | if($ARGV[0] eq "--db") { | ||
24 | $db = $ARGV[1]; | ||
25 | shift @ARGV; | ||
26 | shift @ARGV; | ||
27 | } | ||
28 | elsif($ARGV[0] eq "--path") { | ||
29 | $dir = $ARGV[1]; | ||
30 | shift @ARGV; | ||
31 | shift @ARGV; | ||
32 | } | ||
33 | elsif($ARGV[0] eq "--strip") { | ||
34 | $strip = $ARGV[1]; | ||
35 | shift @ARGV; | ||
36 | shift @ARGV; | ||
37 | } | ||
38 | elsif($ARGV[0] eq "--add") { | ||
39 | $add = $ARGV[1]; | ||
40 | shift @ARGV; | ||
41 | shift @ARGV; | ||
42 | } | ||
43 | elsif($ARGV[0] eq "--verbose") { | ||
44 | $verbose = 1; | ||
45 | shift @ARGV; | ||
46 | } | ||
47 | elsif($ARGV[0] eq "--nocrc") { | ||
48 | $crc = 0; | ||
49 | shift @ARGV; | ||
50 | } | ||
51 | elsif($ARGV[0] eq "--dirisalbum") { | ||
52 | $dirisalbum = 1; | ||
53 | shift @ARGV; | ||
54 | } | ||
55 | elsif($ARGV[0] eq "--dirisalbumname") { | ||
56 | $dirisalbumname = 1; | ||
57 | shift @ARGV; | ||
58 | } | ||
59 | elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) { | ||
60 | $help = 1; | ||
61 | shift @ARGV; | ||
62 | } | ||
63 | else { | ||
64 | shift @ARGV; | ||
65 | } | ||
66 | } | ||
67 | my %entries; | ||
68 | my %genres; | ||
69 | my %albums; | ||
70 | my %years; | ||
71 | my %filename; | ||
72 | |||
73 | my %lcartists; | ||
74 | my %lcalbums; | ||
75 | |||
76 | my %dir2albumname; | ||
77 | |||
78 | my $dbver = 3; | ||
79 | |||
80 | if(! -d $dir or $help) { | ||
81 | print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir); | ||
82 | print <<MOO | ||
83 | |||
84 | songdb --path <dir> [--dirisalbum] [--dirisalbumname] [--db <file>] [--strip <path>] [--add <path>] [--verbose] [--help] | ||
85 | |||
86 | Options: | ||
87 | |||
88 | --path <dir> Where your music collection is found | ||
89 | --dirisalbum Use dir name as album name if the album name is missing in the | ||
90 | tags | ||
91 | --dirisalbumname Uh, isn\'t this the same as the above? | ||
92 | --db <file> What to call the output file. Defaults to rockbox.tagdb | ||
93 | --strip <path> Removes this string from the left of all file names | ||
94 | --add <path> Adds this string to the left of all file names | ||
95 | --nocrc Disables the CRC32 checksums. It makes the output database not | ||
96 | suitable for runtimedb but it makes this script run much | ||
97 | faster. | ||
98 | --verbose Shows more details while working | ||
99 | --help This text | ||
100 | MOO | ||
101 | ; | ||
102 | exit; | ||
103 | } | ||
104 | |||
105 | sub get_oggtag { | ||
106 | my $fn = shift; | ||
107 | my %hash; | ||
108 | |||
109 | my $ogg = vorbiscomm->new($fn); | ||
110 | |||
111 | my $h= $ogg->load; | ||
112 | |||
113 | # Convert this format into the same format used by the id3 parser hash | ||
114 | |||
115 | foreach my $k ($ogg->comment_tags()) | ||
116 | { | ||
117 | foreach my $cmmt ($ogg->comment($k)) | ||
118 | { | ||
119 | my $n; | ||
120 | if($k =~ /^artist$/i) { | ||
121 | $n = 'ARTIST'; | ||
122 | } | ||
123 | elsif($k =~ /^album$/i) { | ||
124 | $n = 'ALBUM'; | ||
125 | } | ||
126 | elsif($k =~ /^title$/i) { | ||
127 | $n = 'TITLE'; | ||
128 | } | ||
129 | $hash{$n}=$cmmt if($n); | ||
130 | } | ||
131 | } | ||
132 | |||
133 | return \%hash; | ||
134 | } | ||
135 | |||
136 | sub get_ogginfo { | ||
137 | my $fn = shift; | ||
138 | my %hash; | ||
139 | |||
140 | my $ogg = vorbiscomm->new($fn); | ||
141 | |||
142 | my $h= $ogg->load; | ||
143 | |||
144 | return $ogg->{'INFO'}; | ||
145 | } | ||
146 | |||
147 | # return ALL directory entries in the given dir | ||
148 | sub getdir { | ||
149 | my ($dir) = @_; | ||
150 | |||
151 | $dir =~ s|/$|| if ($dir ne "/"); | ||
152 | |||
153 | if (opendir(DIR, $dir)) { | ||
154 | my @all = readdir(DIR); | ||
155 | closedir DIR; | ||
156 | return @all; | ||
157 | } | ||
158 | else { | ||
159 | warn "can't opendir $dir: $!\n"; | ||
160 | } | ||
161 | } | ||
162 | |||
163 | sub extractmp3 { | ||
164 | my ($dir, @files) = @_; | ||
165 | my @mp3; | ||
166 | for(@files) { | ||
167 | if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) { | ||
168 | push @mp3, $_; | ||
169 | } | ||
170 | } | ||
171 | return @mp3; | ||
172 | } | ||
173 | |||
174 | sub extractdirs { | ||
175 | my ($dir, @files) = @_; | ||
176 | $dir =~ s|/$||; | ||
177 | my @dirs; | ||
178 | for(@files) { | ||
179 | if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) { | ||
180 | push @dirs, $_; | ||
181 | } | ||
182 | } | ||
183 | return @dirs; | ||
184 | } | ||
185 | |||
186 | # CRC32 32KB of data (use less if there isn't 32KB available) | ||
187 | |||
188 | sub crc32 { | ||
189 | my ($filename, $index) = @_; | ||
190 | |||
191 | my $len = 32*1024; | ||
192 | |||
193 | if(!$crc) { | ||
194 | return 0; # fixed bad CRC when disabled! | ||
195 | # The runtimedb treats a CRC zero as CRC disabled! | ||
196 | } | ||
197 | |||
198 | if(!open(FILE, "<$filename")) { | ||
199 | print "failed to open \"$filename\" $!\n"; | ||
200 | return 0; | ||
201 | } | ||
202 | |||
203 | # read $data from index $index to $buffer from the file, may return fewer | ||
204 | # bytes when dealing with a very small file. | ||
205 | # | ||
206 | # TODO: make sure we don't include a trailer with metadata when doing this. | ||
207 | # Like a id3v1 tag. | ||
208 | my $nread = sysread FILE, $buffer, $len, $index; | ||
209 | |||
210 | close(FILE); | ||
211 | |||
212 | my @crc_table = | ||
213 | ( # CRC32 lookup table for polynomial 0x04C11DB7 | ||
214 | 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B, | ||
215 | 0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, | ||
216 | 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, 0x4C11DB70, 0x48D0C6C7, | ||
217 | 0x4593E01E, 0x4152FDA9, 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75, | ||
218 | 0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, 0x791D4014, 0x7DDC5DA3, | ||
219 | 0x709F7B7A, 0x745E66CD, 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039, | ||
220 | 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, 0xBE2B5B58, 0xBAEA46EF, | ||
221 | 0xB7A96036, 0xB3687D81, 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D, | ||
222 | 0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, 0xC7361B4C, 0xC3F706FB, | ||
223 | 0xCEB42022, 0xCA753D95, 0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1, | ||
224 | 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, 0x34867077, 0x30476DC0, | ||
225 | 0x3D044B19, 0x39C556AE, 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072, | ||
226 | 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, 0x018AEB13, 0x054BF6A4, | ||
227 | 0x0808D07D, 0x0CC9CDCA, 0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE, | ||
228 | 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02, 0x5E9F46BF, 0x5A5E5B08, | ||
229 | 0x571D7DD1, 0x53DC6066, 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA, | ||
230 | 0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 0xBFA1B04B, 0xBB60ADFC, | ||
231 | 0xB6238B25, 0xB2E29692, 0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6, | ||
232 | 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A, 0xE0B41DE7, 0xE4750050, | ||
233 | 0xE9362689, 0xEDF73B3E, 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2, | ||
234 | 0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 0xD5B88683, 0xD1799B34, | ||
235 | 0xDC3ABDED, 0xD8FBA05A, 0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637, | ||
236 | 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB, 0x4F040D56, 0x4BC510E1, | ||
237 | 0x46863638, 0x42472B8F, 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53, | ||
238 | 0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, 0x36194D42, 0x32D850F5, | ||
239 | 0x3F9B762C, 0x3B5A6B9B, 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF, | ||
240 | 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, 0xF12F560E, 0xF5EE4BB9, | ||
241 | 0xF8AD6D60, 0xFC6C70D7, 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B, | ||
242 | 0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, 0xC423CD6A, 0xC0E2D0DD, | ||
243 | 0xCDA1F604, 0xC960EBB3, 0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7, | ||
244 | 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, 0x9B3660C6, 0x9FF77D71, | ||
245 | 0x92B45BA8, 0x9675461F, 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3, | ||
246 | 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, 0x4E8EE645, 0x4A4FFBF2, | ||
247 | 0x470CDD2B, 0x43CDC09C, 0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8, | ||
248 | 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24, 0x119B4BE9, 0x155A565E, | ||
249 | 0x18197087, 0x1CD86D30, 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC, | ||
250 | 0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 0x2497D08D, 0x2056CD3A, | ||
251 | 0x2D15EBE3, 0x29D4F654, 0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0, | ||
252 | 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C, 0xE3A1CBC1, 0xE760D676, | ||
253 | 0xEA23F0AF, 0xEEE2ED18, 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4, | ||
254 | 0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 0x9ABC8BD5, 0x9E7D9662, | ||
255 | 0x933EB0BB, 0x97FFAD0C, 0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668, | ||
256 | 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4 | ||
257 | ); | ||
258 | |||
259 | my $crc = 0xffffffff; | ||
260 | for ($i = 0; $i < $nread; $i++) { | ||
261 | # get the numeric for the byte of the $i index | ||
262 | $buf = ord(substr($buffer, $i, 1)); | ||
263 | |||
264 | $crc = ($crc << 8) ^ $crc_table[(($crc >> 24) ^ $buf) & 0xFF]; | ||
265 | |||
266 | # printf("%08x\n", $crc); | ||
267 | } | ||
268 | |||
269 | if($crc == 0) { | ||
270 | # rule out the very small risk that this actually returns a zero, as | ||
271 | # the current rockbox code assumes a zero CRC means it is disabled! | ||
272 | # TODO: fix the Rockbox code. This is just a hack. | ||
273 | return 1; | ||
274 | } | ||
275 | |||
276 | return $crc; | ||
277 | } | ||
278 | |||
279 | sub singlefile { | ||
280 | my ($file) = @_; | ||
281 | my $hash; | ||
282 | my $info; | ||
283 | |||
284 | if($file =~ /\.ogg$/i) { | ||
285 | $hash = get_oggtag($file); | ||
286 | |||
287 | $info = get_ogginfo($file); | ||
288 | |||
289 | $hash->{FILECRC} = crc32($file, $info->{audio_offset}); | ||
290 | } | ||
291 | else { | ||
292 | $hash = get_mp3tag($file); | ||
293 | |||
294 | $info = get_mp3info($file); | ||
295 | |||
296 | $hash->{FILECRC} = crc32($file, $info->{headersize}); | ||
297 | } | ||
298 | |||
299 | return $hash; # a hash reference | ||
300 | } | ||
301 | |||
302 | my $maxsongperalbum; | ||
303 | |||
304 | sub dodir { | ||
305 | my ($dir)=@_; | ||
306 | |||
307 | print "$dir\n"; | ||
308 | |||
309 | # getdir() returns all entries in the given dir | ||
310 | my @a = getdir($dir); | ||
311 | |||
312 | # extractmp3 filters out only the mp3 files from all given entries | ||
313 | my @m = extractmp3($dir, @a); | ||
314 | |||
315 | my $f; | ||
316 | |||
317 | for $f (sort @m) { | ||
318 | |||
319 | my $id3 = singlefile("$dir/$f"); | ||
320 | |||
321 | # ARTIST | ||
322 | # COMMENT | ||
323 | # ALBUM | ||
324 | # TITLE | ||
325 | # GENRE | ||
326 | # TRACKNUM | ||
327 | # YEAR | ||
328 | |||
329 | # don't index songs without tags | ||
330 | # um. yes we do. | ||
331 | if (not defined $$id3{'ARTIST'} and | ||
332 | not defined $$id3{'ALBUM'} and | ||
333 | not defined $$id3{'TITLE'}) | ||
334 | { | ||
335 | next; | ||
336 | } | ||
337 | |||
338 | #printf "Artist: %s\n", $id3->{'ARTIST'}; | ||
339 | my $path = "$dir/$f"; | ||
340 | if ($strip ne "" and $path =~ /^$strip(.*)/) { | ||
341 | $path = $1; | ||
342 | } | ||
343 | |||
344 | if ($add ne "") { | ||
345 | $path = $add . $path; | ||
346 | } | ||
347 | |||
348 | # Only use one case-variation of each album/artist | ||
349 | if (exists($lcalbums{lc($$id3{'ALBUM'})})) { | ||
350 | # if another album with different case exists | ||
351 | # use that case (store it in $$id3{'ALBUM'} | ||
352 | $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})}; | ||
353 | } | ||
354 | else { | ||
355 | # else create a new entry in the hash | ||
356 | $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'}; | ||
357 | } | ||
358 | |||
359 | if (exists($lcartists{lc($$id3{'ARTIST'})})) { | ||
360 | $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})}; | ||
361 | } | ||
362 | else { | ||
363 | $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'}; | ||
364 | } | ||
365 | |||
366 | $entries{$path}= $id3; | ||
367 | $artists{$id3->{'ARTIST'}}++ if($id3->{'ARTIST'}); | ||
368 | $genres{$id3->{'GENRE'}}++ if($id3->{'GENRE'}); | ||
369 | $years{$id3->{'YEAR'}}++ if($id3->{'YEAR'}); | ||
370 | |||
371 | # fallback names | ||
372 | $$id3{'ARTIST'} = "<no artist tag>" if ($$id3{'ARTIST'} eq ""); | ||
373 | # Fall back on the directory name (not full path dirname), | ||
374 | # if no album tag | ||
375 | if ($dirisalbum) { | ||
376 | if($dir2albumname{$dir} eq "") { | ||
377 | $dir2albumname{$dir} = $$id3{'ALBUM'}; | ||
378 | } | ||
379 | elsif($dir2albumname{$dir} ne $$id3{'ALBUM'}) { | ||
380 | $dir2albumname{$dir} = (split m[/], $dir)[-1]; | ||
381 | } | ||
382 | } | ||
383 | # if no directory | ||
384 | if ($dirisalbumname) { | ||
385 | $$id3{'ALBUM'} = (split m[/], $dir)[-1] if ($$id3{'ALBUM'} eq ""); | ||
386 | } | ||
387 | $$id3{'ALBUM'} = "<no album tag>" if ($$id3{'ALBUM'} eq ""); | ||
388 | # fall back on basename of the file if no title tag. | ||
389 | my $base; | ||
390 | ($base = $f) =~ s/\.\w+$//; | ||
391 | $$id3{'TITLE'} = $base if ($$id3{'TITLE'} eq ""); | ||
392 | |||
393 | # Append dirname, to handle multi-artist albums | ||
394 | $$id3{'DIR'} = $dir; | ||
395 | my $albumid; | ||
396 | if ($dirisalbum) { | ||
397 | $albumid=$$id3{'DIR'}; | ||
398 | } | ||
399 | else { | ||
400 | $albumid= $id3->{'ALBUM'}."___".$$id3{'DIR'}; | ||
401 | } | ||
402 | #printf "album id: %s\n", $albumid; | ||
403 | |||
404 | # if($id3->{'ALBUM'}."___".$id3->{'DIR'} ne "<no album tag>___<no artist tag>") { | ||
405 | my $num = ++$albums{$albumid}; | ||
406 | if($num > $maxsongperalbum) { | ||
407 | $maxsongperalbum = $num; | ||
408 | $longestalbum = $albumid; | ||
409 | } | ||
410 | $album2songs{$albumid}{$$id3{TITLE}} = $id3; | ||
411 | if($dirisalbum) { | ||
412 | $artist2albums{$$id3{ARTIST}}{$$id3{DIR}} = $id3; | ||
413 | } | ||
414 | else { | ||
415 | $artist2albums{$$id3{ARTIST}}{$$id3{ALBUM}} = $id3; | ||
416 | } | ||
417 | # } | ||
418 | } | ||
419 | |||
420 | if($dirisalbum and $dir2albumname{$dir} eq "") { | ||
421 | $dir2albumname{$dir} = (split m[/], $dir)[-1]; | ||
422 | printf "%s\n", $dir2albumname{$dir}; | ||
423 | } | ||
424 | |||
425 | # extractdirs filters out only subdirectories from all given entries | ||
426 | my @d = extractdirs($dir, @a); | ||
427 | |||
428 | for $d (sort @d) { | ||
429 | $dir =~ s|/$||; | ||
430 | dodir("$dir/$d"); | ||
431 | } | ||
432 | } | ||
433 | |||
434 | |||
435 | dodir($dir); | ||
436 | print "\n"; | ||
437 | |||
438 | print "File name table\n" if ($verbose); | ||
439 | for(sort keys %entries) { | ||
440 | printf(" %s\n", $_) if ($verbose); | ||
441 | my $l = length($_); | ||
442 | if($l > $maxfilelen) { | ||
443 | $maxfilelen = $l; | ||
444 | $longestfilename = $_; | ||
445 | } | ||
446 | } | ||
447 | $maxfilelen++; # include zero termination byte | ||
448 | while($maxfilelen&3) { | ||
449 | $maxfilelen++; | ||
450 | } | ||
451 | |||
452 | my $maxsonglen = 0; | ||
453 | my $sc; | ||
454 | print "\nSong title table\n" if ($verbose); | ||
455 | |||
456 | for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) { | ||
457 | printf(" %s\n", $entries{$_}->{'TITLE'} ) if ($verbose); | ||
458 | my $l = length($entries{$_}->{'TITLE'}); | ||
459 | if($l > $maxsonglen) { | ||
460 | $maxsonglen = $l; | ||
461 | $longestsong = $entries{$_}->{'TITLE'}; | ||
462 | } | ||
463 | } | ||
464 | $maxsonglen++; # include zero termination byte | ||
465 | while($maxsonglen&3) { | ||
466 | $maxsonglen++; | ||
467 | } | ||
468 | |||
469 | my $maxartistlen = 0; | ||
470 | print "\nArtist table\n" if ($verbose); | ||
471 | my $i=0; | ||
472 | my %artistcount; | ||
473 | for(sort {uc($a) cmp uc($b)} keys %artists) { | ||
474 | printf(" %s: %d\n", $_, $i) if ($verbose); | ||
475 | $artistcount{$_}=$i++; | ||
476 | my $l = length($_); | ||
477 | if($l > $maxartistlen) { | ||
478 | $maxartistlen = $l; | ||
479 | $longestartist = $_; | ||
480 | } | ||
481 | |||
482 | $l = scalar keys %{$artist2albums{$_}}; | ||
483 | if ($l > $maxalbumsperartist) { | ||
484 | $maxalbumsperartist = $l; | ||
485 | $longestartistalbum = $_; | ||
486 | } | ||
487 | } | ||
488 | $maxartistlen++; # include zero termination byte | ||
489 | while($maxartistlen&3) { | ||
490 | $maxartistlen++; | ||
491 | } | ||
492 | |||
493 | print "\nGenre table\n" if ($verbose); | ||
494 | for(sort keys %genres) { | ||
495 | my $l = length($_); | ||
496 | if($l > $maxgenrelen) { | ||
497 | $maxgenrelen = $l; | ||
498 | $longestgenrename = $_; | ||
499 | } | ||
500 | } | ||
501 | |||
502 | $maxgenrelen++; #include zero termination byte | ||
503 | while($maxgenrelen&3) { | ||
504 | $maxgenrelen++; | ||
505 | } | ||
506 | |||
507 | |||
508 | if ($verbose) { | ||
509 | print "\nYear table\n"; | ||
510 | for(sort keys %years) { | ||
511 | printf(" %s\n", $_); | ||
512 | } | ||
513 | } | ||
514 | |||
515 | print "\nAlbum table\n" if ($verbose); | ||
516 | my $maxalbumlen = 0; | ||
517 | my %albumcount; | ||
518 | $i=0; | ||
519 | my @albumssort; | ||
520 | if($dirisalbum) { | ||
521 | @albumssort = sort {uc($dir2albumname{$a}) cmp uc($dir2albumname{$b})} keys %albums; | ||
522 | } | ||
523 | else { | ||
524 | @albumssort = sort {uc($a) cmp uc($b)} keys %albums; | ||
525 | } | ||
526 | for(@albumssort) { | ||
527 | my @moo=split(/___/, $_); | ||
528 | printf(" %s\n", $moo[0]) if ($verbose); | ||
529 | $albumcount{$_} = $i++; | ||
530 | my $l; | ||
531 | if($dirisalbum) { | ||
532 | $l = length($dir2albumname{$_}); | ||
533 | } | ||
534 | else { | ||
535 | $l = length($moo[0]); | ||
536 | } | ||
537 | if($l > $maxalbumlen) { | ||
538 | $maxalbumlen = $l; | ||
539 | if($dirisalbum) { | ||
540 | $longestalbumname = $dir2albumname{$_}; | ||
541 | } | ||
542 | else { | ||
543 | $longestalbumname = $moo[0]; | ||
544 | } | ||
545 | } | ||
546 | } | ||
547 | $maxalbumlen++; # include zero termination byte | ||
548 | while($maxalbumlen&3) { | ||
549 | $maxalbumlen++; | ||
550 | } | ||
551 | |||
552 | |||
553 | |||
554 | sub dumpshort { | ||
555 | my ($num)=@_; | ||
556 | |||
557 | # print "int: $num\n"; | ||
558 | |||
559 | print DB pack "n", $num; | ||
560 | } | ||
561 | |||
562 | sub dumpint { | ||
563 | my ($num)=@_; | ||
564 | |||
565 | # print "int: $num\n"; | ||
566 | |||
567 | print DB pack "N", $num; | ||
568 | } | ||
569 | |||
570 | if (!scalar keys %entries) { | ||
571 | print "No songs found. Did you specify the right --path ?\n"; | ||
572 | print "Use the --help parameter to see all options.\n"; | ||
573 | exit; | ||
574 | } | ||
575 | |||
576 | if ($db) { | ||
577 | my $songentrysize = $maxsonglen + 12 + $maxgenrelen+ 12; | ||
578 | my $albumentrysize = $maxalbumlen + 4 + $maxsongperalbum*4; | ||
579 | my $artistentrysize = $maxartistlen + $maxalbumsperartist*4; | ||
580 | my $fileentrysize = $maxfilelen + 12; | ||
581 | |||
582 | printf "Number of artists : %d\n", scalar keys %artists; | ||
583 | printf "Number of albums : %d\n", scalar keys %albums; | ||
584 | printf "Number of songs / files : %d\n", scalar keys %entries; | ||
585 | print "Max artist length : $maxartistlen ($longestartist)\n"; | ||
586 | print "Max album length : $maxalbumlen ($longestalbumname)\n"; | ||
587 | print "Max song length : $maxsonglen ($longestsong)\n"; | ||
588 | print "Max songs per album : $maxsongperalbum ($longestalbum)\n"; | ||
589 | print "Max albums per artist: $maxalbumsperartist ($longestartistalbum)\n"; | ||
590 | print "Max genre length : $maxgenrelen ($longestgenrename)\n"; | ||
591 | print "Max file length : $maxfilelen ($longestfilename)\n"; | ||
592 | print "Database version: $dbver\n" if ($verbose); | ||
593 | print "Song Entry Size : $songentrysize ($maxsonglen + 12 + $maxgenrelen + 4)\n" if ($verbose); | ||
594 | print "Album Entry Size: $albumentrysize ($maxalbumlen + 4 + $maxsongperalbum * 4)\n" if ($verbose); | ||
595 | print "Artist Entry Size: $artistentrysize ($maxartistlen + $maxalbumsperartist * 4)\n" if ($verbose); | ||
596 | print "File Entry Size: $fileentrysize ($maxfilelen + 12)\n" if ($verbose); | ||
597 | |||
598 | |||
599 | open(DB, ">$db") || die "couldn't make $db"; | ||
600 | binmode(DB); | ||
601 | printf DB "RDB%c", $dbver; | ||
602 | |||
603 | $pathindex = 68; # paths always start at index 68 | ||
604 | |||
605 | $artistindex = $pathindex; | ||
606 | |||
607 | # set total size of song title table | ||
608 | $sc = scalar(keys %entries) * $songentrysize; | ||
609 | my $ac = scalar(keys %albums) * $albumentrysize; | ||
610 | my $arc = scalar(keys %artists) * $artistentrysize; | ||
611 | $albumindex = $artistindex + $arc; # arc is size of all artists | ||
612 | $songindex = $albumindex + $ac; # ac is size of all albums | ||
613 | my $fileindex = $songindex + $sc; # sc is size of all songs | ||
614 | |||
615 | dumpint($artistindex); # file position index of artist table | ||
616 | dumpint($albumindex); # file position index of album table | ||
617 | dumpint($songindex); # file position index of song table | ||
618 | dumpint($fileindex); # file position index of file table | ||
619 | dumpint(scalar(keys %artists)); # number of artists | ||
620 | dumpint(scalar(keys %albums)); # number of albums | ||
621 | dumpint(scalar(keys %entries)); # number of songs | ||
622 | dumpint(scalar(keys %entries)); # number of files | ||
623 | dumpint($maxartistlen); # length of artist name field | ||
624 | dumpint($maxalbumlen); # length of album name field | ||
625 | dumpint($maxsonglen); # length of song name field | ||
626 | dumpint($maxgenrelen); #length of genre field | ||
627 | dumpint($maxfilelen); # length of file field | ||
628 | dumpint($maxsongperalbum); # number of entries in songs-per-album array | ||
629 | dumpint($maxalbumsperartist); # number of entries in albums-per-artist array | ||
630 | dumpint(-1); # rundb dirty | ||
631 | |||
632 | #### TABLE of artists ### | ||
633 | # name of artist1 | ||
634 | # pointers to albums of artist1 | ||
635 | |||
636 | for (sort {uc($a) cmp uc($b)} keys %artists) { | ||
637 | my $artist = $_; | ||
638 | my $str = $_."\x00" x ($maxartistlen - length($_)); | ||
639 | print DB $str; | ||
640 | |||
641 | for (sort keys %{$artist2albums{$artist}}) { | ||
642 | my $id3 = $artist2albums{$artist}{$_}; | ||
643 | my $a; | ||
644 | if($dirisalbum) { | ||
645 | $a = $albumcount{"$$id3{'DIR'}"} * $albumentrysize; | ||
646 | } | ||
647 | else { | ||
648 | $a = $albumcount{"$$id3{'ALBUM'}___$$id3{'DIR'}"} * $albumentrysize; | ||
649 | } | ||
650 | dumpint($a + $albumindex); | ||
651 | } | ||
652 | |||
653 | for (scalar keys %{$artist2albums{$artist}} .. $maxalbumsperartist-1) { | ||
654 | print DB "\x00\x00\x00\x00"; | ||
655 | } | ||
656 | |||
657 | } | ||
658 | |||
659 | ### Build song offset info. | ||
660 | my $offset = $songindex; | ||
661 | for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) { | ||
662 | my $id3 = $entries{$_}; | ||
663 | $$id3{'songoffset'} = $offset; | ||
664 | $offset += $songentrysize; | ||
665 | } | ||
666 | |||
667 | |||
668 | #### TABLE of albums ### | ||
669 | # name of album1 | ||
670 | # pointers to artists of album1 | ||
671 | # pointers to songs on album1 | ||
672 | |||
673 | for(@albumssort) { | ||
674 | my $albumid = $_; | ||
675 | my @moo=split(/___/, $_); | ||
676 | my $t; | ||
677 | my $str; | ||
678 | if($dirisalbum) { | ||
679 | $t = $dir2albumname{$albumid}; | ||
680 | } | ||
681 | else { | ||
682 | $t = $moo[0]; | ||
683 | } | ||
684 | $str = $t."\x00" x ($maxalbumlen - length($t)); | ||
685 | print DB $str; | ||
686 | |||
687 | my @songlist = keys %{$album2songs{$albumid}}; | ||
688 | my $id3 = $album2songs{$albumid}{$songlist[0]}; | ||
689 | |||
690 | #printf "(d) albumid: %s artist: %s\n",$albumid, $id3->{'ARTIST'}; | ||
691 | |||
692 | my $aoffset = $artistcount{$id3->{'ARTIST'}} * $artistentrysize; | ||
693 | dumpint($aoffset + $artistindex); # pointer to artist of this album | ||
694 | |||
695 | if (defined $id3->{'TRACKNUM'}) { | ||
696 | @songlist = sort { | ||
697 | $album2songs{$albumid}{$a}->{'TRACKNUM'} <=> | ||
698 | $album2songs{$albumid}{$b}->{'TRACKNUM'} | ||
699 | } @songlist; | ||
700 | } | ||
701 | else { | ||
702 | @songlist = sort @songlist; | ||
703 | } | ||
704 | |||
705 | for (@songlist) { | ||
706 | my $id3 = $album2songs{$albumid}{$_}; | ||
707 | dumpint($$id3{'songoffset'}); | ||
708 | } | ||
709 | |||
710 | for (scalar keys %{$album2songs{$albumid}} .. $maxsongperalbum-1) { | ||
711 | print DB "\x00\x00\x00\x00"; | ||
712 | } | ||
713 | } | ||
714 | |||
715 | #### Build filename offset info | ||
716 | my $l=$fileindex; | ||
717 | my %filenamepos; | ||
718 | for $f (sort {uc($a) cmp uc($b)} keys %entries) { | ||
719 | $filenamepos{$f}= $l; | ||
720 | $l += $fileentrysize; | ||
721 | } | ||
722 | |||
723 | #### TABLE of songs ### | ||
724 | # title of song1 | ||
725 | # pointer to artist of song1 | ||
726 | # pointer to album of song1 | ||
727 | # pointer to filename of song1 | ||
728 | |||
729 | for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) { | ||
730 | my $f = $_; | ||
731 | my $id3 = $entries{$f}; | ||
732 | my $t = $id3->{'TITLE'}; | ||
733 | my $g = $id3->{'GENRE'}; | ||
734 | my $str = $t."\x00" x ($maxsonglen- length($t)); | ||
735 | |||
736 | print DB $str; # title | ||
737 | $str = $g."\x00" x ($maxgenrelen - length($g)); | ||
738 | |||
739 | my $a = $artistcount{$id3->{'ARTIST'}} * $artistentrysize; | ||
740 | dumpint($a + $artistindex); # pointer to artist of this song | ||
741 | |||
742 | if($dirisalbum) { | ||
743 | $a = $albumcount{"$$id3{DIR}"} * $albumentrysize; | ||
744 | } | ||
745 | else { | ||
746 | $a = $albumcount{"$$id3{ALBUM}___$$id3{DIR}"} * $albumentrysize; | ||
747 | } | ||
748 | dumpint($a + $albumindex); # pointer to album of this song | ||
749 | |||
750 | # pointer to filename of this song | ||
751 | dumpint($filenamepos{$f}); | ||
752 | print DB $str; #genre | ||
753 | dumpshort(-1); | ||
754 | dumpshort($id3->{'YEAR'}); | ||
755 | dumpint(-1); | ||
756 | dumpshort($id3->{'TRACKNUM'}); | ||
757 | dumpshort(-1); | ||
758 | } | ||
759 | |||
760 | #### TABLE of file names ### | ||
761 | # path1 | ||
762 | |||
763 | for $f (sort {uc($a) cmp uc($b)} %entries) { | ||
764 | my $str = $f."\x00" x ($maxfilelen- length($f)); | ||
765 | my $id3 = $entries{$f}; | ||
766 | print DB $str; | ||
767 | #print STDERR "CRC: ".."\n"; | ||
768 | dumpint($id3->{'FILECRC'}); # CRC32 of the song data | ||
769 | dumpint($id3->{'songoffset'}); # offset to song data | ||
770 | dumpint(-1); # offset to rundb data. always set to -1. this is updated by rockbox code on the player. | ||
771 | } | ||
772 | |||
773 | close(DB); | ||
774 | } | ||
775 | |||
776 | ### | ||
777 | ### Here follows module MP3::Info Copyright (c) 1998-2004 Chris Nandor | ||
778 | ### Modified by Björn Stenberg to remove use of external libraries | ||
779 | ### | ||
780 | |||
781 | our( | ||
782 | @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION, $REVISION, | ||
783 | @mp3_genres, %mp3_genres, @winamp_genres, %winamp_genres, $try_harder, | ||
784 | @t_bitrate, @t_sampling_freq, @frequency_tbl, %v1_tag_fields, | ||
785 | @v1_tag_names, %v2_tag_names, %v2_to_v1_names, $AUTOLOAD, | ||
786 | @mp3_info_fields | ||
787 | ); | ||
788 | |||
789 | @ISA = 'Exporter'; | ||
790 | @EXPORT = qw( | ||
791 | set_mp3tag get_mp3tag get_mp3info remove_mp3tag | ||
792 | use_winamp_genres | ||
793 | ); | ||
794 | @EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8); | ||
795 | %EXPORT_TAGS = ( | ||
796 | genres => [qw(@mp3_genres %mp3_genres)], | ||
797 | utf8 => [qw(use_mp3_utf8)], | ||
798 | all => [@EXPORT, @EXPORT_OK] | ||
799 | ); | ||
800 | |||
801 | # $Id$ | ||
802 | ($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/; | ||
803 | $VERSION = '1.02'; | ||
804 | |||
805 | =pod | ||
806 | |||
807 | =head1 NAME | ||
808 | |||
809 | MP3::Info - Manipulate / fetch info from MP3 audio files | ||
810 | |||
811 | =head1 SYNOPSIS | ||
812 | |||
813 | #!perl -w | ||
814 | use MP3::Info; | ||
815 | my $file = 'Pearls_Before_Swine.mp3'; | ||
816 | set_mp3tag($file, 'Pearls Before Swine', q"77's", | ||
817 | 'Sticks and Stones', '1990', | ||
818 | q"(c) 1990 77's LTD.", 'rock & roll'); | ||
819 | |||
820 | my $tag = get_mp3tag($file) or die "No TAG info"; | ||
821 | $tag->{GENRE} = 'rock'; | ||
822 | set_mp3tag($file, $tag); | ||
823 | |||
824 | my $info = get_mp3info($file); | ||
825 | printf "$file length is %d:%d\n", $info->{MM}, $info->{SS}; | ||
826 | |||
827 | =cut | ||
828 | |||
829 | { | ||
830 | my $c = -1; | ||
831 | # set all lower-case and regular-cased versions of genres as keys | ||
832 | # with index as value of each key | ||
833 | %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres; | ||
834 | |||
835 | # do it again for winamp genres | ||
836 | $c = -1; | ||
837 | %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres; | ||
838 | } | ||
839 | |||
840 | =pod | ||
841 | |||
842 | my $mp3 = new MP3::Info $file; | ||
843 | $mp3->title('Perls Before Swine'); | ||
844 | printf "$file length is %s, title is %s\n", | ||
845 | $mp3->time, $mp3->title; | ||
846 | |||
847 | |||
848 | =head1 DESCRIPTION | ||
849 | |||
850 | =over 4 | ||
851 | |||
852 | =item $mp3 = MP3::Info-E<gt>new(FILE) | ||
853 | |||
854 | OOP interface to the rest of the module. The same keys | ||
855 | available via get_mp3info and get_mp3tag are available | ||
856 | via the returned object (using upper case or lower case; | ||
857 | but note that all-caps "VERSION" will return the module | ||
858 | version, not the MP3 version). | ||
859 | |||
860 | Passing a value to one of the methods will set the value | ||
861 | for that tag in the MP3 file, if applicable. | ||
862 | |||
863 | =cut | ||
864 | |||
865 | sub new { | ||
866 | my($pack, $file) = @_; | ||
867 | |||
868 | my $info = get_mp3info($file) or return undef; | ||
869 | my $tags = get_mp3tag($file) || { map { ($_ => undef) } @v1_tag_names }; | ||
870 | my %self = ( | ||
871 | FILE => $file, | ||
872 | TRY_HARDER => 0 | ||
873 | ); | ||
874 | |||
875 | @self{@mp3_info_fields, @v1_tag_names, 'file'} = ( | ||
876 | @{$info}{@mp3_info_fields}, | ||
877 | @{$tags}{@v1_tag_names}, | ||
878 | $file | ||
879 | ); | ||
880 | |||
881 | return bless \%self, $pack; | ||
882 | } | ||
883 | |||
884 | sub can { | ||
885 | my $self = shift; | ||
886 | return $self->SUPER::can(@_) unless ref $self; | ||
887 | my $name = uc shift; | ||
888 | return sub { $self->$name(@_) } if exists $self->{$name}; | ||
889 | return undef; | ||
890 | } | ||
891 | |||
892 | sub AUTOLOAD { | ||
893 | my($self) = @_; | ||
894 | (my $name = uc $AUTOLOAD) =~ s/^.*://; | ||
895 | |||
896 | if (exists $self->{$name}) { | ||
897 | my $sub = exists $v1_tag_fields{$name} | ||
898 | ? sub { | ||
899 | if (defined $_[1]) { | ||
900 | $_[0]->{$name} = $_[1]; | ||
901 | set_mp3tag($_[0]->{FILE}, $_[0]); | ||
902 | } | ||
903 | return $_[0]->{$name}; | ||
904 | } | ||
905 | : sub { | ||
906 | return $_[0]->{$name} | ||
907 | }; | ||
908 | |||
909 | *{$AUTOLOAD} = $sub; | ||
910 | goto &$AUTOLOAD; | ||
911 | |||
912 | } else { | ||
913 | warn(sprintf "No method '$name' available in package %s.", | ||
914 | __PACKAGE__); | ||
915 | } | ||
916 | } | ||
917 | |||
918 | sub DESTROY { | ||
919 | |||
920 | } | ||
921 | |||
922 | |||
923 | =item use_mp3_utf8([STATUS]) | ||
924 | |||
925 | Tells MP3::Info to (or not) return TAG info in UTF-8. | ||
926 | TRUE is 1, FALSE is 0. Default is FALSE. | ||
927 | |||
928 | Will only be able to it on if Unicode::String is available. ID3v2 | ||
929 | tags will be converted to UTF-8 according to the encoding specified | ||
930 | in each tag; ID3v1 tags will be assumed Latin-1 and converted | ||
931 | to UTF-8. | ||
932 | |||
933 | Function returns status (TRUE/FALSE). If no argument is supplied, | ||
934 | or an unaccepted argument is supplied, function merely returns status. | ||
935 | |||
936 | This function is not exported by default, but may be exported | ||
937 | with the C<:utf8> or C<:all> export tag. | ||
938 | |||
939 | =cut | ||
940 | |||
941 | my $unicode_module = eval { require Unicode::String }; | ||
942 | my $UNICODE = 0; | ||
943 | |||
944 | sub use_mp3_utf8 { | ||
945 | my($val) = @_; | ||
946 | if ($val == 1) { | ||
947 | $UNICODE = 1 if $unicode_module; | ||
948 | } elsif ($val == 0) { | ||
949 | $UNICODE = 0; | ||
950 | } | ||
951 | return $UNICODE; | ||
952 | } | ||
953 | |||
954 | =pod | ||
955 | |||
956 | =item use_winamp_genres() | ||
957 | |||
958 | Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres> | ||
959 | (adds 68 additional genres to the default list of 80). | ||
960 | This is a separate function because these are non-standard | ||
961 | genres, but they are included because they are widely used. | ||
962 | |||
963 | You can import the data structures with one of: | ||
964 | |||
965 | use MP3::Info qw(:genres); | ||
966 | use MP3::Info qw(:DEFAULT :genres); | ||
967 | use MP3::Info qw(:all); | ||
968 | |||
969 | =cut | ||
970 | |||
971 | sub use_winamp_genres { | ||
972 | %mp3_genres = %winamp_genres; | ||
973 | @mp3_genres = @winamp_genres; | ||
974 | return 1; | ||
975 | } | ||
976 | |||
977 | =pod | ||
978 | |||
979 | =pod | ||
980 | |||
981 | =item get_mp3tag (FILE [, VERSION, RAW_V2]) | ||
982 | |||
983 | Returns hash reference containing tag information in MP3 file. The keys | ||
984 | returned are the same as those supplied for C<set_mp3tag>, except in the | ||
985 | case of RAW_V2 being set. | ||
986 | |||
987 | If VERSION is C<1>, the information is taken from the ID3v1 tag (if present). | ||
988 | If VERSION is C<2>, the information is taken from the ID3v2 tag (if present). | ||
989 | If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and | ||
990 | then, if present, the ID3v2 tag information will override any existing ID3v1 | ||
991 | tag info. | ||
992 | |||
993 | If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation | ||
994 | of text encoding. The key name is the same as the frame ID (ID to name mappings | ||
995 | are in the global %v2_tag_names). | ||
996 | |||
997 | If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if | ||
998 | necessary, etc. It also takes multiple values for a given key (such as comments) | ||
999 | and puts them in an arrayref. | ||
1000 | |||
1001 | If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will | ||
1002 | not be read. | ||
1003 | |||
1004 | Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>), | ||
1005 | (unless RAW_V2 is C<1>). | ||
1006 | |||
1007 | Also returns a TAGVERSION key, containing the ID3 version used for the returned | ||
1008 | data (if TAGVERSION argument is C<0>, may contain two versions). | ||
1009 | |||
1010 | =cut | ||
1011 | |||
1012 | sub get_mp3tag { | ||
1013 | my($file, $ver, $raw_v2) = @_; | ||
1014 | my($tag, $v1, $v2, $v2h, %info, @array, $fh); | ||
1015 | $raw_v2 ||= 0; | ||
1016 | $ver = !$ver ? 0 : ($ver == 2 || $ver == 1) ? $ver : 0; | ||
1017 | |||
1018 | if (not (defined $file && $file ne '')) { | ||
1019 | $@ = "No file specified"; | ||
1020 | return undef; | ||
1021 | } | ||
1022 | |||
1023 | if (not -s $file) { | ||
1024 | $@ = "File is empty"; | ||
1025 | return undef; | ||
1026 | } | ||
1027 | |||
1028 | if (ref $file) { # filehandle passed | ||
1029 | $fh = $file; | ||
1030 | } else { | ||
1031 | $fh = gensym; | ||
1032 | if (not open $fh, "< $file\0") { | ||
1033 | $@ = "Can't open $file: $!"; | ||
1034 | return undef; | ||
1035 | } | ||
1036 | } | ||
1037 | |||
1038 | binmode $fh; | ||
1039 | |||
1040 | if ($ver < 2) { | ||
1041 | seek $fh, -128, 2; | ||
1042 | while(defined(my $line = <$fh>)) { $tag .= $line } | ||
1043 | |||
1044 | if ($tag =~ /^TAG/) { | ||
1045 | $v1 = 1; | ||
1046 | if (substr($tag, -3, 2) =~ /\000[^\000]/) { | ||
1047 | (undef, @info{@v1_tag_names}) = | ||
1048 | (unpack('a3a30a30a30a4a28', $tag), | ||
1049 | ord(substr($tag, -2, 1)), | ||
1050 | $mp3_genres[ord(substr $tag, -1)]); | ||
1051 | $info{TAGVERSION} = 'ID3v1.1'; | ||
1052 | } else { | ||
1053 | (undef, @info{@v1_tag_names[0..4, 6]}) = | ||
1054 | (unpack('a3a30a30a30a4a30', $tag), | ||
1055 | $mp3_genres[ord(substr $tag, -1)]); | ||
1056 | $info{TAGVERSION} = 'ID3v1'; | ||
1057 | } | ||
1058 | if ($UNICODE) { | ||
1059 | for my $key (keys %info) { | ||
1060 | next unless $info{$key}; | ||
1061 | my $u = Unicode::String::latin1($info{$key}); | ||
1062 | $info{$key} = $u->utf8; | ||
1063 | } | ||
1064 | } | ||
1065 | } elsif ($ver == 1) { | ||
1066 | _close($file, $fh); | ||
1067 | $@ = "No ID3v1 tag found"; | ||
1068 | return undef; | ||
1069 | } | ||
1070 | } | ||
1071 | |||
1072 | ($v2, $v2h) = _get_v2tag($fh); | ||
1073 | |||
1074 | unless ($v1 || $v2) { | ||
1075 | _close($file, $fh); | ||
1076 | $@ = "No ID3 tag found"; | ||
1077 | return undef; | ||
1078 | } | ||
1079 | |||
1080 | if (($ver == 0 || $ver == 2) && $v2) { | ||
1081 | if ($raw_v2 == 1 && $ver == 2) { | ||
1082 | %info = %$v2; | ||
1083 | $info{TAGVERSION} = $v2h->{version}; | ||
1084 | } else { | ||
1085 | my $hash = $raw_v2 == 2 ? { map { ($_, $_) } keys %v2_tag_names } : \%v2_to_v1_names; | ||
1086 | for my $id (keys %$hash) { | ||
1087 | if (exists $v2->{$id}) { | ||
1088 | if ($id =~ /^TCON?$/ && $v2->{$id} =~ /^.?\((\d+)\)/) { | ||
1089 | $info{$hash->{$id}} = $mp3_genres[$1]; | ||
1090 | } else { | ||
1091 | my $data1 = $v2->{$id}; | ||
1092 | |||
1093 | # this is tricky ... if this is an arrayref, we want | ||
1094 | # to only return one, so we pick the first one. but | ||
1095 | # if it is a comment, we pick the first one where the | ||
1096 | # first charcter after the language is NULL and not an | ||
1097 | # additional sub-comment, because that is most likely | ||
1098 | # to be the user-supplied comment | ||
1099 | |||
1100 | if (ref $data1 && !$raw_v2) { | ||
1101 | if ($id =~ /^COMM?$/) { | ||
1102 | my($newdata) = grep /^(....\000)/, @{$data1}; | ||
1103 | $data1 = $newdata || $data1->[0]; | ||
1104 | } else { | ||
1105 | $data1 = $data1->[0]; | ||
1106 | } | ||
1107 | } | ||
1108 | |||
1109 | $data1 = [ $data1 ] if ! ref $data1; | ||
1110 | |||
1111 | for my $data (@$data1) { | ||
1112 | $data =~ s/^(.)//; # strip first char (text encoding) | ||
1113 | my $encoding = $1; | ||
1114 | my $desc; | ||
1115 | if ($id =~ /^COM[M ]?$/) { | ||
1116 | $data =~ s/^(?:...)//; # strip language | ||
1117 | $data =~ s/^(.*?)\000+//; # strip up to first NULL(s), | ||
1118 | # for sub-comment | ||
1119 | $desc = $1; | ||
1120 | } | ||
1121 | |||
1122 | if ($UNICODE) { | ||
1123 | if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE | ||
1124 | my $u = Unicode::String::utf16($data); | ||
1125 | $data = $u->utf8; | ||
1126 | $data =~ s/^\xEF\xBB\xBF//; # strip BOM | ||
1127 | } elsif ($encoding eq "\000") { | ||
1128 | my $u = Unicode::String::latin1($data); | ||
1129 | $data = $u->utf8; | ||
1130 | } | ||
1131 | } | ||
1132 | |||
1133 | if ($raw_v2 == 2 && $desc) { | ||
1134 | $data = { $desc => $data }; | ||
1135 | } | ||
1136 | |||
1137 | if ($raw_v2 == 2 && exists $info{$hash->{$id}}) { | ||
1138 | if (ref $info{$hash->{$id}} eq 'ARRAY') { | ||
1139 | push @{$info{$hash->{$id}}}, $data; | ||
1140 | } else { | ||
1141 | $info{$hash->{$id}} = [ $info{$hash->{$id}}, $data ]; | ||
1142 | } | ||
1143 | } else { | ||
1144 | $info{$hash->{$id}} = $data; | ||
1145 | } | ||
1146 | } | ||
1147 | } | ||
1148 | } | ||
1149 | } | ||
1150 | if ($ver == 0 && $info{TAGVERSION}) { | ||
1151 | $info{TAGVERSION} .= ' / ' . $v2h->{version}; | ||
1152 | } else { | ||
1153 | $info{TAGVERSION} = $v2h->{version}; | ||
1154 | } | ||
1155 | } | ||
1156 | } | ||
1157 | |||
1158 | unless ($raw_v2 && $ver == 2) { | ||
1159 | foreach my $key (keys %info) { | ||
1160 | if (defined $info{$key}) { | ||
1161 | $info{$key} =~ s/\000+.*//g; | ||
1162 | $info{$key} =~ s/\s+$//; | ||
1163 | } | ||
1164 | } | ||
1165 | |||
1166 | for (@v1_tag_names) { | ||
1167 | $info{$_} = '' unless defined $info{$_}; | ||
1168 | } | ||
1169 | } | ||
1170 | |||
1171 | if (keys %info && exists $info{GENRE} && ! defined $info{GENRE}) { | ||
1172 | $info{GENRE} = ''; | ||
1173 | } | ||
1174 | |||
1175 | _close($file, $fh); | ||
1176 | |||
1177 | return keys %info ? {%info} : undef; | ||
1178 | } | ||
1179 | |||
1180 | sub _get_v2tag { | ||
1181 | my($fh) = @_; | ||
1182 | my($off, $myseek, $myseek_22, $myseek_23, $v2, $h, $hlen, $num); | ||
1183 | $h = {}; | ||
1184 | |||
1185 | $v2 = _get_v2head($fh) or return; | ||
1186 | if ($v2->{major_version} < 2) { | ||
1187 | warn "This is $v2->{version}; " . | ||
1188 | "ID3v2 versions older than ID3v2.2.0 not supported\n" | ||
1189 | if $^W; | ||
1190 | return; | ||
1191 | } | ||
1192 | |||
1193 | if ($v2->{major_version} == 2) { | ||
1194 | $hlen = 6; | ||
1195 | $num = 3; | ||
1196 | } else { | ||
1197 | $hlen = 10; | ||
1198 | $num = 4; | ||
1199 | } | ||
1200 | |||
1201 | $myseek = sub { | ||
1202 | seek $fh, $off, 0; | ||
1203 | read $fh, my($bytes), $hlen; | ||
1204 | return unless $bytes =~ /^([A-Z0-9]{$num})/ | ||
1205 | || ($num == 4 && $bytes =~ /^(COM )/); # stupid iTunes | ||
1206 | my($id, $size) = ($1, $hlen); | ||
1207 | my @bytes = reverse unpack "C$num", substr($bytes, $num, $num); | ||
1208 | for my $i (0 .. ($num - 1)) { | ||
1209 | $size += $bytes[$i] * 256 ** $i; | ||
1210 | } | ||
1211 | return($id, $size); | ||
1212 | }; | ||
1213 | |||
1214 | $off = $v2->{ext_header_size} + 10; | ||
1215 | |||
1216 | while ($off < $v2->{tag_size}) { | ||
1217 | my($id, $size) = &$myseek or last; | ||
1218 | seek $fh, $off + $hlen, 0; | ||
1219 | read $fh, my($bytes), $size - $hlen; | ||
1220 | if (exists $h->{$id}) { | ||
1221 | if (ref $h->{$id} eq 'ARRAY') { | ||
1222 | push @{$h->{$id}}, $bytes; | ||
1223 | } else { | ||
1224 | $h->{$id} = [$h->{$id}, $bytes]; | ||
1225 | } | ||
1226 | } else { | ||
1227 | $h->{$id} = $bytes; | ||
1228 | } | ||
1229 | $off += $size; | ||
1230 | } | ||
1231 | |||
1232 | return($h, $v2); | ||
1233 | } | ||
1234 | |||
1235 | |||
1236 | =pod | ||
1237 | |||
1238 | =item get_mp3info (FILE) | ||
1239 | |||
1240 | Returns hash reference containing file information for MP3 file. | ||
1241 | This data cannot be changed. Returned data: | ||
1242 | |||
1243 | VERSION MPEG audio version (1, 2, 2.5) | ||
1244 | LAYER MPEG layer description (1, 2, 3) | ||
1245 | STEREO boolean for audio is in stereo | ||
1246 | |||
1247 | VBR boolean for variable bitrate | ||
1248 | BITRATE bitrate in kbps (average for VBR files) | ||
1249 | FREQUENCY frequency in kHz | ||
1250 | SIZE bytes in audio stream | ||
1251 | |||
1252 | SECS total seconds | ||
1253 | MM minutes | ||
1254 | SS leftover seconds | ||
1255 | MS leftover milliseconds | ||
1256 | TIME time in MM:SS | ||
1257 | |||
1258 | COPYRIGHT boolean for audio is copyrighted | ||
1259 | PADDING boolean for MP3 frames are padded | ||
1260 | MODE channel mode (0 = stereo, 1 = joint stereo, | ||
1261 | 2 = dual channel, 3 = single channel) | ||
1262 | FRAMES approximate number of frames | ||
1263 | FRAME_LENGTH approximate length of a frame | ||
1264 | VBR_SCALE VBR scale from VBR header | ||
1265 | |||
1266 | On error, returns nothing and sets C<$@>. | ||
1267 | |||
1268 | =cut | ||
1269 | |||
1270 | sub get_mp3info { | ||
1271 | my($file) = @_; | ||
1272 | my($off, $myseek, $byte, $eof, $h, $tot, $fh); | ||
1273 | |||
1274 | if (not (defined $file && $file ne '')) { | ||
1275 | $@ = "No file specified"; | ||
1276 | return undef; | ||
1277 | } | ||
1278 | |||
1279 | if (not -s $file) { | ||
1280 | $@ = "File is empty"; | ||
1281 | return undef; | ||
1282 | } | ||
1283 | |||
1284 | if (ref $file) { # filehandle passed | ||
1285 | $fh = $file; | ||
1286 | } else { | ||
1287 | $fh = gensym; | ||
1288 | if (not open $fh, "< $file\0") { | ||
1289 | $@ = "Can't open $file: $!"; | ||
1290 | return undef; | ||
1291 | } | ||
1292 | } | ||
1293 | |||
1294 | $off = 0; | ||
1295 | $tot = 4096; | ||
1296 | |||
1297 | $myseek = sub { | ||
1298 | seek $fh, $off, 0; | ||
1299 | read $fh, $byte, 4; | ||
1300 | }; | ||
1301 | |||
1302 | binmode $fh; | ||
1303 | &$myseek; | ||
1304 | |||
1305 | if ($off == 0) { | ||
1306 | if (my $id3v2 = _get_v2head($fh)) { | ||
1307 | $tot += $off += $id3v2->{tag_size}; | ||
1308 | &$myseek; | ||
1309 | } | ||
1310 | } | ||
1311 | |||
1312 | $h = _get_head($byte); | ||
1313 | until (_is_mp3($h)) { | ||
1314 | $off++; | ||
1315 | &$myseek; | ||
1316 | $h = _get_head($byte); | ||
1317 | if ($off > $tot && !$try_harder) { | ||
1318 | _close($file, $fh); | ||
1319 | $@ = "Couldn't find MP3 header (perhaps set " . | ||
1320 | '$MP3::Info::try_harder and retry)'; | ||
1321 | return undef; | ||
1322 | } | ||
1323 | } | ||
1324 | |||
1325 | my $vbr = _get_vbr($fh, $h, \$off); | ||
1326 | |||
1327 | $h->{headersize}=$off; # data size prepending the actual mp3 data | ||
1328 | |||
1329 | seek $fh, 0, 2; | ||
1330 | $eof = tell $fh; | ||
1331 | seek $fh, -128, 2; | ||
1332 | $off += 128 if <$fh> =~ /^TAG/ ? 1 : 0; | ||
1333 | |||
1334 | _close($file, $fh); | ||
1335 | |||
1336 | $h->{size} = $eof - $off; | ||
1337 | |||
1338 | return _get_info($h, $vbr); | ||
1339 | } | ||
1340 | |||
1341 | sub _get_info { | ||
1342 | my($h, $vbr) = @_; | ||
1343 | my $i; | ||
1344 | |||
1345 | $i->{VERSION} = $h->{IDR} == 2 ? 2 : $h->{IDR} == 3 ? 1 : | ||
1346 | $h->{IDR} == 0 ? 2.5 : 0; | ||
1347 | $i->{LAYER} = 4 - $h->{layer}; | ||
1348 | $i->{VBR} = defined $vbr ? 1 : 0; | ||
1349 | |||
1350 | $i->{COPYRIGHT} = $h->{copyright} ? 1 : 0; | ||
1351 | $i->{PADDING} = $h->{padding_bit} ? 1 : 0; | ||
1352 | $i->{STEREO} = $h->{mode} == 3 ? 0 : 1; | ||
1353 | $i->{MODE} = $h->{mode}; | ||
1354 | |||
1355 | $i->{SIZE} = $vbr && $vbr->{bytes} ? $vbr->{bytes} : $h->{size}; | ||
1356 | |||
1357 | my $mfs = $h->{fs} / ($h->{ID} ? 144000 : 72000); | ||
1358 | $i->{FRAMES} = int($vbr && $vbr->{frames} | ||
1359 | ? $vbr->{frames} | ||
1360 | : $i->{SIZE} / $h->{bitrate} / $mfs | ||
1361 | ); | ||
1362 | |||
1363 | if ($vbr) { | ||
1364 | $i->{VBR_SCALE} = $vbr->{scale} if $vbr->{scale}; | ||
1365 | $h->{bitrate} = $i->{SIZE} / $i->{FRAMES} * $mfs; | ||
1366 | if (not $h->{bitrate}) { | ||
1367 | $@ = "Couldn't determine VBR bitrate"; | ||
1368 | return undef; | ||
1369 | } | ||
1370 | } | ||
1371 | |||
1372 | $h->{'length'} = ($i->{SIZE} * 8) / $h->{bitrate} / 10; | ||
1373 | $i->{SECS} = $h->{'length'} / 100; | ||
1374 | $i->{MM} = int $i->{SECS} / 60; | ||
1375 | $i->{SS} = int $i->{SECS} % 60; | ||
1376 | $i->{MS} = (($i->{SECS} - ($i->{MM} * 60) - $i->{SS}) * 1000); | ||
1377 | # $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS}); | ||
1378 | # int($i->{MS} / 100 * 75); # is this right? | ||
1379 | $i->{TIME} = sprintf "%.2d:%.2d", @{$i}{'MM', 'SS'}; | ||
1380 | |||
1381 | $i->{BITRATE} = int $h->{bitrate}; | ||
1382 | # should we just return if ! FRAMES? | ||
1383 | $i->{FRAME_LENGTH} = int($h->{size} / $i->{FRAMES}) if $i->{FRAMES}; | ||
1384 | $i->{FREQUENCY} = $frequency_tbl[3 * $h->{IDR} + $h->{sampling_freq}]; | ||
1385 | |||
1386 | $i->{headersize} = $h->{headersize}; | ||
1387 | |||
1388 | return $i; | ||
1389 | } | ||
1390 | |||
1391 | sub _get_head { | ||
1392 | my($byte) = @_; | ||
1393 | my($bytes, $h); | ||
1394 | |||
1395 | $bytes = _unpack_head($byte); | ||
1396 | @$h{qw(IDR ID layer protection_bit | ||
1397 | bitrate_index sampling_freq padding_bit private_bit | ||
1398 | mode mode_extension copyright original | ||
1399 | emphasis version_index bytes)} = ( | ||
1400 | ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1, | ||
1401 | ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1, | ||
1402 | ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1, | ||
1403 | $bytes&3, ($bytes>>19)&3, $bytes | ||
1404 | ); | ||
1405 | |||
1406 | $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}]; | ||
1407 | $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}]; | ||
1408 | |||
1409 | return $h; | ||
1410 | } | ||
1411 | |||
1412 | sub _is_mp3 { | ||
1413 | my $h = $_[0] or return undef; | ||
1414 | return ! ( # all below must be false | ||
1415 | $h->{bitrate_index} == 0 | ||
1416 | || | ||
1417 | $h->{version_index} == 1 | ||
1418 | || | ||
1419 | ($h->{bytes} & 0xFFE00000) != 0xFFE00000 | ||
1420 | || | ||
1421 | !$h->{fs} | ||
1422 | || | ||
1423 | !$h->{bitrate} | ||
1424 | || | ||
1425 | $h->{bitrate_index} == 15 | ||
1426 | || | ||
1427 | !$h->{layer} | ||
1428 | || | ||
1429 | $h->{sampling_freq} == 3 | ||
1430 | || | ||
1431 | $h->{emphasis} == 2 | ||
1432 | || | ||
1433 | !$h->{bitrate_index} | ||
1434 | || | ||
1435 | ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000 | ||
1436 | || | ||
1437 | ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1) | ||
1438 | || | ||
1439 | ($h->{mode_extension} != 0 && $h->{mode} != 1) | ||
1440 | ); | ||
1441 | } | ||
1442 | |||
1443 | sub _get_vbr { | ||
1444 | my($fh, $h, $roff) = @_; | ||
1445 | my($off, $bytes, @bytes, $myseek, %vbr); | ||
1446 | |||
1447 | $off = $$roff; | ||
1448 | @_ = (); # closure confused if we don't do this | ||
1449 | |||
1450 | $myseek = sub { | ||
1451 | my $n = $_[0] || 4; | ||
1452 | seek $fh, $off, 0; | ||
1453 | read $fh, $bytes, $n; | ||
1454 | $off += $n; | ||
1455 | }; | ||
1456 | |||
1457 | $off += 4; | ||
1458 | |||
1459 | if ($h->{ID}) { # MPEG1 | ||
1460 | $off += $h->{mode} == 3 ? 17 : 32; | ||
1461 | } else { # MPEG2 | ||
1462 | $off += $h->{mode} == 3 ? 9 : 17; | ||
1463 | } | ||
1464 | |||
1465 | &$myseek; | ||
1466 | return unless $bytes eq 'Xing'; | ||
1467 | |||
1468 | &$myseek; | ||
1469 | $vbr{flags} = _unpack_head($bytes); | ||
1470 | |||
1471 | if ($vbr{flags} & 1) { | ||
1472 | &$myseek; | ||
1473 | $vbr{frames} = _unpack_head($bytes); | ||
1474 | } | ||
1475 | |||
1476 | if ($vbr{flags} & 2) { | ||
1477 | &$myseek; | ||
1478 | $vbr{bytes} = _unpack_head($bytes); | ||
1479 | } | ||
1480 | |||
1481 | if ($vbr{flags} & 4) { | ||
1482 | $myseek->(100); | ||
1483 | # Not used right now ... | ||
1484 | # $vbr{toc} = _unpack_head($bytes); | ||
1485 | } | ||
1486 | |||
1487 | if ($vbr{flags} & 8) { # (quality ind., 0=best 100=worst) | ||
1488 | &$myseek; | ||
1489 | $vbr{scale} = _unpack_head($bytes); | ||
1490 | } else { | ||
1491 | $vbr{scale} = -1; | ||
1492 | } | ||
1493 | |||
1494 | $$roff = $off; | ||
1495 | return \%vbr; | ||
1496 | } | ||
1497 | |||
1498 | sub _get_v2head { | ||
1499 | my $fh = $_[0] or return; | ||
1500 | my($h, $bytes, @bytes); | ||
1501 | |||
1502 | # check first three bytes for 'ID3' | ||
1503 | seek $fh, 0, 0; | ||
1504 | read $fh, $bytes, 3; | ||
1505 | return unless $bytes eq 'ID3'; | ||
1506 | |||
1507 | # get version | ||
1508 | read $fh, $bytes, 2; | ||
1509 | $h->{version} = sprintf "ID3v2.%d.%d", | ||
1510 | @$h{qw[major_version minor_version]} = | ||
1511 | unpack 'c2', $bytes; | ||
1512 | |||
1513 | # get flags | ||
1514 | read $fh, $bytes, 1; | ||
1515 | if ($h->{major_version} == 2) { | ||
1516 | @$h{qw[unsync compression]} = | ||
1517 | (unpack 'b8', $bytes)[7, 6]; | ||
1518 | $h->{ext_header} = 0; | ||
1519 | $h->{experimental} = 0; | ||
1520 | } else { | ||
1521 | @$h{qw[unsync ext_header experimental]} = | ||
1522 | (unpack 'b8', $bytes)[7, 6, 5]; | ||
1523 | } | ||
1524 | |||
1525 | # get ID3v2 tag length from bytes 7-10 | ||
1526 | $h->{tag_size} = 10; # include ID3v2 header size | ||
1527 | read $fh, $bytes, 4; | ||
1528 | @bytes = reverse unpack 'C4', $bytes; | ||
1529 | foreach my $i (0 .. 3) { | ||
1530 | # whoaaaaaa nellllllyyyyyy! | ||
1531 | $h->{tag_size} += $bytes[$i] * 128 ** $i; | ||
1532 | } | ||
1533 | |||
1534 | # get extended header size | ||
1535 | $h->{ext_header_size} = 0; | ||
1536 | if ($h->{ext_header}) { | ||
1537 | $h->{ext_header_size} += 10; | ||
1538 | read $fh, $bytes, 4; | ||
1539 | @bytes = reverse unpack 'C4', $bytes; | ||
1540 | for my $i (0..3) { | ||
1541 | $h->{ext_header_size} += $bytes[$i] * 256 ** $i; | ||
1542 | } | ||
1543 | } | ||
1544 | |||
1545 | return $h; | ||
1546 | } | ||
1547 | |||
1548 | sub _unpack_head { | ||
1549 | unpack('l', pack('L', unpack('N', $_[0]))); | ||
1550 | } | ||
1551 | |||
1552 | sub _close { | ||
1553 | my($file, $fh) = @_; | ||
1554 | unless (ref $file) { # filehandle not passed | ||
1555 | close $fh or warn "Problem closing '$file': $!"; | ||
1556 | } | ||
1557 | } | ||
1558 | |||
1559 | BEGIN { | ||
1560 | @mp3_genres = ( | ||
1561 | 'Blues', | ||
1562 | 'Classic Rock', | ||
1563 | 'Country', | ||
1564 | 'Dance', | ||
1565 | 'Disco', | ||
1566 | 'Funk', | ||
1567 | 'Grunge', | ||
1568 | 'Hip-Hop', | ||
1569 | 'Jazz', | ||
1570 | 'Metal', | ||
1571 | 'New Age', | ||
1572 | 'Oldies', | ||
1573 | 'Other', | ||
1574 | 'Pop', | ||
1575 | 'R&B', | ||
1576 | 'Rap', | ||
1577 | 'Reggae', | ||
1578 | 'Rock', | ||
1579 | 'Techno', | ||
1580 | 'Industrial', | ||
1581 | 'Alternative', | ||
1582 | 'Ska', | ||
1583 | 'Death Metal', | ||
1584 | 'Pranks', | ||
1585 | 'Soundtrack', | ||
1586 | 'Euro-Techno', | ||
1587 | 'Ambient', | ||
1588 | 'Trip-Hop', | ||
1589 | 'Vocal', | ||
1590 | 'Jazz+Funk', | ||
1591 | 'Fusion', | ||
1592 | 'Trance', | ||
1593 | 'Classical', | ||
1594 | 'Instrumental', | ||
1595 | 'Acid', | ||
1596 | 'House', | ||
1597 | 'Game', | ||
1598 | 'Sound Clip', | ||
1599 | 'Gospel', | ||
1600 | 'Noise', | ||
1601 | 'AlternRock', | ||
1602 | 'Bass', | ||
1603 | 'Soul', | ||
1604 | 'Punk', | ||
1605 | 'Space', | ||
1606 | 'Meditative', | ||
1607 | 'Instrumental Pop', | ||
1608 | 'Instrumental Rock', | ||
1609 | 'Ethnic', | ||
1610 | 'Gothic', | ||
1611 | 'Darkwave', | ||
1612 | 'Techno-Industrial', | ||
1613 | 'Electronic', | ||
1614 | 'Pop-Folk', | ||
1615 | 'Eurodance', | ||
1616 | 'Dream', | ||
1617 | 'Southern Rock', | ||
1618 | 'Comedy', | ||
1619 | 'Cult', | ||
1620 | 'Gangsta', | ||
1621 | 'Top 40', | ||
1622 | 'Christian Rap', | ||
1623 | 'Pop/Funk', | ||
1624 | 'Jungle', | ||
1625 | 'Native American', | ||
1626 | 'Cabaret', | ||
1627 | 'New Wave', | ||
1628 | 'Psychadelic', | ||
1629 | 'Rave', | ||
1630 | 'Showtunes', | ||
1631 | 'Trailer', | ||
1632 | 'Lo-Fi', | ||
1633 | 'Tribal', | ||
1634 | 'Acid Punk', | ||
1635 | 'Acid Jazz', | ||
1636 | 'Polka', | ||
1637 | 'Retro', | ||
1638 | 'Musical', | ||
1639 | 'Rock & Roll', | ||
1640 | 'Hard Rock', | ||
1641 | ); | ||
1642 | |||
1643 | @winamp_genres = ( | ||
1644 | @mp3_genres, | ||
1645 | 'Folk', | ||
1646 | 'Folk-Rock', | ||
1647 | 'National Folk', | ||
1648 | 'Swing', | ||
1649 | 'Fast Fusion', | ||
1650 | 'Bebob', | ||
1651 | 'Latin', | ||
1652 | 'Revival', | ||
1653 | 'Celtic', | ||
1654 | 'Bluegrass', | ||
1655 | 'Avantgarde', | ||
1656 | 'Gothic Rock', | ||
1657 | 'Progressive Rock', | ||
1658 | 'Psychedelic Rock', | ||
1659 | 'Symphonic Rock', | ||
1660 | 'Slow Rock', | ||
1661 | 'Big Band', | ||
1662 | 'Chorus', | ||
1663 | 'Easy Listening', | ||
1664 | 'Acoustic', | ||
1665 | 'Humour', | ||
1666 | 'Speech', | ||
1667 | 'Chanson', | ||
1668 | 'Opera', | ||
1669 | 'Chamber Music', | ||
1670 | 'Sonata', | ||
1671 | 'Symphony', | ||
1672 | 'Booty Bass', | ||
1673 | 'Primus', | ||
1674 | 'Porn Groove', | ||
1675 | 'Satire', | ||
1676 | 'Slow Jam', | ||
1677 | 'Club', | ||
1678 | 'Tango', | ||
1679 | 'Samba', | ||
1680 | 'Folklore', | ||
1681 | 'Ballad', | ||
1682 | 'Power Ballad', | ||
1683 | 'Rhythmic Soul', | ||
1684 | 'Freestyle', | ||
1685 | 'Duet', | ||
1686 | 'Punk Rock', | ||
1687 | 'Drum Solo', | ||
1688 | 'Acapella', | ||
1689 | 'Euro-House', | ||
1690 | 'Dance Hall', | ||
1691 | 'Goa', | ||
1692 | 'Drum & Bass', | ||
1693 | 'Club-House', | ||
1694 | 'Hardcore', | ||
1695 | 'Terror', | ||
1696 | 'Indie', | ||
1697 | 'BritPop', | ||
1698 | 'Negerpunk', | ||
1699 | 'Polsk Punk', | ||
1700 | 'Beat', | ||
1701 | 'Christian Gangsta Rap', | ||
1702 | 'Heavy Metal', | ||
1703 | 'Black Metal', | ||
1704 | 'Crossover', | ||
1705 | 'Contemporary Christian', | ||
1706 | 'Christian Rock', | ||
1707 | 'Merengue', | ||
1708 | 'Salsa', | ||
1709 | 'Thrash Metal', | ||
1710 | 'Anime', | ||
1711 | 'JPop', | ||
1712 | 'Synthpop', | ||
1713 | ); | ||
1714 | |||
1715 | @t_bitrate = ([ | ||
1716 | [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], | ||
1717 | [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], | ||
1718 | [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160] | ||
1719 | ],[ | ||
1720 | [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448], | ||
1721 | [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], | ||
1722 | [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] | ||
1723 | ]); | ||
1724 | |||
1725 | @t_sampling_freq = ( | ||
1726 | [11025, 12000, 8000], | ||
1727 | [undef, undef, undef], # reserved | ||
1728 | [22050, 24000, 16000], | ||
1729 | [44100, 48000, 32000] | ||
1730 | ); | ||
1731 | |||
1732 | @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 } | ||
1733 | map { @$_ } @t_sampling_freq; | ||
1734 | |||
1735 | @mp3_info_fields = qw( | ||
1736 | VERSION | ||
1737 | LAYER | ||
1738 | STEREO | ||
1739 | VBR | ||
1740 | BITRATE | ||
1741 | FREQUENCY | ||
1742 | SIZE | ||
1743 | SECS | ||
1744 | MM | ||
1745 | SS | ||
1746 | MS | ||
1747 | TIME | ||
1748 | COPYRIGHT | ||
1749 | PADDING | ||
1750 | MODE | ||
1751 | FRAMES | ||
1752 | FRAME_LENGTH | ||
1753 | VBR_SCALE | ||
1754 | ); | ||
1755 | |||
1756 | %v1_tag_fields = | ||
1757 | (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4); | ||
1758 | |||
1759 | @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE); | ||
1760 | |||
1761 | %v2_to_v1_names = ( | ||
1762 | # v2.2 tags | ||
1763 | 'TT2' => 'TITLE', | ||
1764 | 'TP1' => 'ARTIST', | ||
1765 | 'TAL' => 'ALBUM', | ||
1766 | 'TYE' => 'YEAR', | ||
1767 | 'COM' => 'COMMENT', | ||
1768 | 'TRK' => 'TRACKNUM', | ||
1769 | 'TCO' => 'GENRE', # not clean mapping, but ... | ||
1770 | # v2.3 tags | ||
1771 | 'TIT2' => 'TITLE', | ||
1772 | 'TPE1' => 'ARTIST', | ||
1773 | 'TALB' => 'ALBUM', | ||
1774 | 'TYER' => 'YEAR', | ||
1775 | 'COMM' => 'COMMENT', | ||
1776 | 'TRCK' => 'TRACKNUM', | ||
1777 | 'TCON' => 'GENRE', | ||
1778 | ); | ||
1779 | |||
1780 | %v2_tag_names = ( | ||
1781 | # v2.2 tags | ||
1782 | 'BUF' => 'Recommended buffer size', | ||
1783 | 'CNT' => 'Play counter', | ||
1784 | 'COM' => 'Comments', | ||
1785 | 'CRA' => 'Audio encryption', | ||
1786 | 'CRM' => 'Encrypted meta frame', | ||
1787 | 'ETC' => 'Event timing codes', | ||
1788 | 'EQU' => 'Equalization', | ||
1789 | 'GEO' => 'General encapsulated object', | ||
1790 | 'IPL' => 'Involved people list', | ||
1791 | 'LNK' => 'Linked information', | ||
1792 | 'MCI' => 'Music CD Identifier', | ||
1793 | 'MLL' => 'MPEG location lookup table', | ||
1794 | 'PIC' => 'Attached picture', | ||
1795 | 'POP' => 'Popularimeter', | ||
1796 | 'REV' => 'Reverb', | ||
1797 | 'RVA' => 'Relative volume adjustment', | ||
1798 | 'SLT' => 'Synchronized lyric/text', | ||
1799 | 'STC' => 'Synced tempo codes', | ||
1800 | 'TAL' => 'Album/Movie/Show title', | ||
1801 | 'TBP' => 'BPM (Beats Per Minute)', | ||
1802 | 'TCM' => 'Composer', | ||
1803 | 'TCO' => 'Content type', | ||
1804 | 'TCR' => 'Copyright message', | ||
1805 | 'TDA' => 'Date', | ||
1806 | 'TDY' => 'Playlist delay', | ||
1807 | 'TEN' => 'Encoded by', | ||
1808 | 'TFT' => 'File type', | ||
1809 | 'TIM' => 'Time', | ||
1810 | 'TKE' => 'Initial key', | ||
1811 | 'TLA' => 'Language(s)', | ||
1812 | 'TLE' => 'Length', | ||
1813 | 'TMT' => 'Media type', | ||
1814 | 'TOA' => 'Original artist(s)/performer(s)', | ||
1815 | 'TOF' => 'Original filename', | ||
1816 | 'TOL' => 'Original Lyricist(s)/text writer(s)', | ||
1817 | 'TOR' => 'Original release year', | ||
1818 | 'TOT' => 'Original album/Movie/Show title', | ||
1819 | 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group', | ||
1820 | 'TP2' => 'Band/Orchestra/Accompaniment', | ||
1821 | 'TP3' => 'Conductor/Performer refinement', | ||
1822 | 'TP4' => 'Interpreted, remixed, or otherwise modified by', | ||
1823 | 'TPA' => 'Part of a set', | ||
1824 | 'TPB' => 'Publisher', | ||
1825 | 'TRC' => 'ISRC (International Standard Recording Code)', | ||
1826 | 'TRD' => 'Recording dates', | ||
1827 | 'TRK' => 'Track number/Position in set', | ||
1828 | 'TSI' => 'Size', | ||
1829 | 'TSS' => 'Software/hardware and settings used for encoding', | ||
1830 | 'TT1' => 'Content group description', | ||
1831 | 'TT2' => 'Title/Songname/Content description', | ||
1832 | 'TT3' => 'Subtitle/Description refinement', | ||
1833 | 'TXT' => 'Lyricist/text writer', | ||
1834 | 'TXX' => 'User defined text information frame', | ||
1835 | 'TYE' => 'Year', | ||
1836 | 'UFI' => 'Unique file identifier', | ||
1837 | 'ULT' => 'Unsychronized lyric/text transcription', | ||
1838 | 'WAF' => 'Official audio file webpage', | ||
1839 | 'WAR' => 'Official artist/performer webpage', | ||
1840 | 'WAS' => 'Official audio source webpage', | ||
1841 | 'WCM' => 'Commercial information', | ||
1842 | 'WCP' => 'Copyright/Legal information', | ||
1843 | 'WPB' => 'Publishers official webpage', | ||
1844 | 'WXX' => 'User defined URL link frame', | ||
1845 | |||
1846 | # v2.3 tags | ||
1847 | 'AENC' => 'Audio encryption', | ||
1848 | 'APIC' => 'Attached picture', | ||
1849 | 'COMM' => 'Comments', | ||
1850 | 'COMR' => 'Commercial frame', | ||
1851 | 'ENCR' => 'Encryption method registration', | ||
1852 | 'EQUA' => 'Equalization', | ||
1853 | 'ETCO' => 'Event timing codes', | ||
1854 | 'GEOB' => 'General encapsulated object', | ||
1855 | 'GRID' => 'Group identification registration', | ||
1856 | 'IPLS' => 'Involved people list', | ||
1857 | 'LINK' => 'Linked information', | ||
1858 | 'MCDI' => 'Music CD identifier', | ||
1859 | 'MLLT' => 'MPEG location lookup table', | ||
1860 | 'OWNE' => 'Ownership frame', | ||
1861 | 'PCNT' => 'Play counter', | ||
1862 | 'POPM' => 'Popularimeter', | ||
1863 | 'POSS' => 'Position synchronisation frame', | ||
1864 | 'PRIV' => 'Private frame', | ||
1865 | 'RBUF' => 'Recommended buffer size', | ||
1866 | 'RVAD' => 'Relative volume adjustment', | ||
1867 | 'RVRB' => 'Reverb', | ||
1868 | 'SYLT' => 'Synchronized lyric/text', | ||
1869 | 'SYTC' => 'Synchronized tempo codes', | ||
1870 | 'TALB' => 'Album/Movie/Show title', | ||
1871 | 'TBPM' => 'BPM (beats per minute)', | ||
1872 | 'TCOM' => 'Composer', | ||
1873 | 'TCON' => 'Content type', | ||
1874 | 'TCOP' => 'Copyright message', | ||
1875 | 'TDAT' => 'Date', | ||
1876 | 'TDLY' => 'Playlist delay', | ||
1877 | 'TENC' => 'Encoded by', | ||
1878 | 'TEXT' => 'Lyricist/Text writer', | ||
1879 | 'TFLT' => 'File type', | ||
1880 | 'TIME' => 'Time', | ||
1881 | 'TIT1' => 'Content group description', | ||
1882 | 'TIT2' => 'Title/songname/content description', | ||
1883 | 'TIT3' => 'Subtitle/Description refinement', | ||
1884 | 'TKEY' => 'Initial key', | ||
1885 | 'TLAN' => 'Language(s)', | ||
1886 | 'TLEN' => 'Length', | ||
1887 | 'TMED' => 'Media type', | ||
1888 | 'TOAL' => 'Original album/movie/show title', | ||
1889 | 'TOFN' => 'Original filename', | ||
1890 | 'TOLY' => 'Original lyricist(s)/text writer(s)', | ||
1891 | 'TOPE' => 'Original artist(s)/performer(s)', | ||
1892 | 'TORY' => 'Original release year', | ||
1893 | 'TOWN' => 'File owner/licensee', | ||
1894 | 'TPE1' => 'Lead performer(s)/Soloist(s)', | ||
1895 | 'TPE2' => 'Band/orchestra/accompaniment', | ||
1896 | 'TPE3' => 'Conductor/performer refinement', | ||
1897 | 'TPE4' => 'Interpreted, remixed, or otherwise modified by', | ||
1898 | 'TPOS' => 'Part of a set', | ||
1899 | 'TPUB' => 'Publisher', | ||
1900 | 'TRCK' => 'Track number/Position in set', | ||
1901 | 'TRDA' => 'Recording dates', | ||
1902 | 'TRSN' => 'Internet radio station name', | ||
1903 | 'TRSO' => 'Internet radio station owner', | ||
1904 | 'TSIZ' => 'Size', | ||
1905 | 'TSRC' => 'ISRC (international standard recording code)', | ||
1906 | 'TSSE' => 'Software/Hardware and settings used for encoding', | ||
1907 | 'TXXX' => 'User defined text information frame', | ||
1908 | 'TYER' => 'Year', | ||
1909 | 'UFID' => 'Unique file identifier', | ||
1910 | 'USER' => 'Terms of use', | ||
1911 | 'USLT' => 'Unsychronized lyric/text transcription', | ||
1912 | 'WCOM' => 'Commercial information', | ||
1913 | 'WCOP' => 'Copyright/Legal information', | ||
1914 | 'WOAF' => 'Official audio file webpage', | ||
1915 | 'WOAR' => 'Official artist/performer webpage', | ||
1916 | 'WOAS' => 'Official audio source webpage', | ||
1917 | 'WORS' => 'Official internet radio station homepage', | ||
1918 | 'WPAY' => 'Payment', | ||
1919 | 'WPUB' => 'Publishers official webpage', | ||
1920 | 'WXXX' => 'User defined URL link frame', | ||
1921 | |||
1922 | # v2.4 additional tags | ||
1923 | # note that we don't restrict tags from 2.3 or 2.4, | ||
1924 | 'ASPI' => 'Audio seek point index', | ||
1925 | 'EQU2' => 'Equalisation (2)', | ||
1926 | 'RVA2' => 'Relative volume adjustment (2)', | ||
1927 | 'SEEK' => 'Seek frame', | ||
1928 | 'SIGN' => 'Signature frame', | ||
1929 | 'TDEN' => 'Encoding time', | ||
1930 | 'TDOR' => 'Original release time', | ||
1931 | 'TDRC' => 'Recording time', | ||
1932 | 'TDRL' => 'Release time', | ||
1933 | 'TDTG' => 'Tagging time', | ||
1934 | 'TIPL' => 'Involved people list', | ||
1935 | 'TMCL' => 'Musician credits list', | ||
1936 | 'TMOO' => 'Mood', | ||
1937 | 'TPRO' => 'Produced notice', | ||
1938 | 'TSOA' => 'Album sort order', | ||
1939 | 'TSOP' => 'Performer sort order', | ||
1940 | 'TSOT' => 'Title sort order', | ||
1941 | 'TSST' => 'Set subtitle', | ||
1942 | |||
1943 | # grrrrrrr | ||
1944 | 'COM ' => 'Broken iTunes comments', | ||
1945 | ); | ||
1946 | } | ||
1947 | |||
1948 | 1; | ||
1949 | |||
1950 | __END__ | ||
1951 | |||
1952 | =pod | ||
1953 | |||
1954 | =back | ||
1955 | |||
1956 | =head1 TROUBLESHOOTING | ||
1957 | |||
1958 | If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">). | ||
1959 | If you cannot figure out why it does not work for you, please put the MP3 file in | ||
1960 | a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me | ||
1961 | mail regarding where I can get the file, with a detailed description of the problem. | ||
1962 | |||
1963 | If I download the file, after debugging the problem I will not keep the MP3 file | ||
1964 | if it is not legal for me to have it. Just let me know if it is legal for me to | ||
1965 | keep it or not. | ||
1966 | |||
1967 | |||
1968 | =head1 TODO | ||
1969 | |||
1970 | =over 4 | ||
1971 | |||
1972 | =item ID3v2 Support | ||
1973 | |||
1974 | Still need to do more for reading tags, such as using Compress::Zlib to decompress | ||
1975 | compressed tags. But until I see this in use more, I won't bother. If something | ||
1976 | does not work properly with reading, follow the instructions above for | ||
1977 | troubleshooting. | ||
1978 | |||
1979 | ID3v2 I<writing> is coming soon. | ||
1980 | |||
1981 | =item Get data from scalar | ||
1982 | |||
1983 | Instead of passing a file spec or filehandle, pass the | ||
1984 | data itself. Would take some work, converting the seeks, etc. | ||
1985 | |||
1986 | =item Padding bit ? | ||
1987 | |||
1988 | Do something with padding bit. | ||
1989 | |||
1990 | =item Test suite | ||
1991 | |||
1992 | Test suite could use a bit of an overhaul and update. Patches very welcome. | ||
1993 | |||
1994 | =over 4 | ||
1995 | |||
1996 | =item * | ||
1997 | |||
1998 | Revamp getset.t. Test all the various get_mp3tag args. | ||
1999 | |||
2000 | =item * | ||
2001 | |||
2002 | Test Unicode. | ||
2003 | |||
2004 | =item * | ||
2005 | |||
2006 | Test OOP API. | ||
2007 | |||
2008 | =item * | ||
2009 | |||
2010 | Test error handling, check more for missing files, bad MP3s, etc. | ||
2011 | |||
2012 | =back | ||
2013 | |||
2014 | =item Other VBR | ||
2015 | |||
2016 | Right now, only Xing VBR is supported. | ||
2017 | |||
2018 | =back | ||
2019 | |||
2020 | |||
2021 | =head1 THANKS | ||
2022 | |||
2023 | Edward Allen E<lt>allenej@c51844-a.spokn1.wa.home.comE<gt>, | ||
2024 | Vittorio Bertola E<lt>v.bertola@vitaminic.comE<gt>, | ||
2025 | Michael Blakeley E<lt>mike@blakeley.comE<gt>, | ||
2026 | Per Bolmstedt E<lt>tomten@kol14.comE<gt>, | ||
2027 | Tony Bowden E<lt>tony@tmtm.comE<gt>, | ||
2028 | Tom Brown E<lt>thecap@usa.netE<gt>, | ||
2029 | Sergio Camarena E<lt>scamarena@users.sourceforge.netE<gt>, | ||
2030 | Chris Dawson E<lt>cdawson@webiphany.comE<gt>, | ||
2031 | Luke Drumm E<lt>lukedrumm@mypad.comE<gt>, | ||
2032 | Kyle Farrell E<lt>kyle@cantametrix.comE<gt>, | ||
2033 | Jeffrey Friedl E<lt>jfriedl@yahoo.comE<gt>, | ||
2034 | brian d foy E<lt>comdog@panix.comE<gt>, | ||
2035 | Ben Gertzfield E<lt>che@debian.orgE<gt>, | ||
2036 | Brian Goodwin E<lt>brian@fuddmain.comE<gt>, | ||
2037 | Todd Hanneken E<lt>thanneken@hds.harvard.eduE<gt>, | ||
2038 | Todd Harris E<lt>harris@cshl.orgE<gt>, | ||
2039 | Woodrow Hill E<lt>asim@mindspring.comE<gt>, | ||
2040 | Kee Hinckley E<lt>nazgul@somewhere.comE<gt>, | ||
2041 | Roman Hodek E<lt>Roman.Hodek@informatik.uni-erlangen.deE<gt>, | ||
2042 | Peter Kovacs E<lt>kovacsp@egr.uri.eduE<gt>, | ||
2043 | Johann Lindvall, | ||
2044 | Peter Marschall E<lt>peter.marschall@mayn.deE<gt>, | ||
2045 | Trond Michelsen E<lt>mike@crusaders.noE<gt>, | ||
2046 | Dave O'Neill E<lt>dave@nexus.carleton.caE<gt>, | ||
2047 | Christoph Oberauer E<lt>christoph.oberauer@sbg.ac.atE<gt>, | ||
2048 | Jake Palmer E<lt>jake.palmer@db.comE<gt>, | ||
2049 | Andrew Phillips E<lt>asp@wasteland.orgE<gt>, | ||
2050 | David Reuteler E<lt>reuteler@visi.comE<gt>, | ||
2051 | John Ruttenberg E<lt>rutt@chezrutt.comE<gt>, | ||
2052 | Matthew Sachs E<lt>matthewg@zevils.comE<gt>, | ||
2053 | E<lt>scfc_de@users.sf.netE<gt>, | ||
2054 | Hermann Schwaerzler E<lt>Hermann.Schwaerzler@uibk.ac.atE<gt>, | ||
2055 | Chris Sidi E<lt>sidi@angband.orgE<gt>, | ||
2056 | Roland Steinbach E<lt>roland@support-system.comE<gt>, | ||
2057 | Stuart E<lt>schneis@users.sourceforge.netE<gt>, | ||
2058 | Jeffery Sumler E<lt>jsumler@mediaone.netE<gt>, | ||
2059 | Predrag Supurovic E<lt>mpgtools@dv.co.yuE<gt>, | ||
2060 | Bogdan Surdu E<lt>tim@go.roE<gt>, | ||
2061 | E<lt>tim@tim-landscheidt.deE<gt>, | ||
2062 | Pass F. B. Travis E<lt>pftravis@bellsouth.netE<gt>, | ||
2063 | Tobias Wagener E<lt>tobias@wagener.nuE<gt>, | ||
2064 | Ronan Waide E<lt>waider@stepstone.ieE<gt>, | ||
2065 | Andy Waite E<lt>andy@mailroute.comE<gt>, | ||
2066 | Ken Williams E<lt>ken@forum.swarthmore.eduE<gt>, | ||
2067 | Meng Weng Wong E<lt>mengwong@pobox.comE<gt>. | ||
2068 | |||
2069 | |||
2070 | =head1 AUTHOR AND COPYRIGHT | ||
2071 | |||
2072 | Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/ | ||
2073 | |||
2074 | Copyright (c) 1998-2003 Chris Nandor. All rights reserved. This program is | ||
2075 | free software; you can redistribute it and/or modify it under the terms | ||
2076 | of the Artistic License, distributed with Perl. | ||
2077 | |||
2078 | |||
2079 | =head1 SEE ALSO | ||
2080 | |||
2081 | =over 4 | ||
2082 | |||
2083 | =item MP3::Info Project Page | ||
2084 | |||
2085 | http://projects.pudge.net/ | ||
2086 | |||
2087 | =item mp3tools | ||
2088 | |||
2089 | http://www.zevils.com/linux/mp3tools/ | ||
2090 | |||
2091 | =item mpgtools | ||
2092 | |||
2093 | http://www.dv.co.yu/mpgscript/mpgtools.htm | ||
2094 | http://www.dv.co.yu/mpgscript/mpeghdr.htm | ||
2095 | |||
2096 | =item mp3tool | ||
2097 | |||
2098 | http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html | ||
2099 | |||
2100 | =item ID3v2 | ||
2101 | |||
2102 | http://www.id3.org/ | ||
2103 | |||
2104 | =item Xing Variable Bitrate | ||
2105 | |||
2106 | http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/ | ||
2107 | |||
2108 | =item MP3Ext | ||
2109 | |||
2110 | http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/ | ||
2111 | |||
2112 | =item Xmms | ||
2113 | |||
2114 | http://www.xmms.org/ | ||
2115 | |||
2116 | |||
2117 | =back | ||
2118 | |||
2119 | =head1 VERSION | ||
2120 | |||
2121 | v1.02, Sunday, March 2, 2003 | ||
2122 | |||
2123 | =cut | ||
diff --git a/tools/vorbiscomm.pm b/tools/vorbiscomm.pm deleted file mode 100644 index f2e48e8632..0000000000 --- a/tools/vorbiscomm.pm +++ /dev/null | |||
@@ -1,732 +0,0 @@ | |||
1 | ############################################################################# | ||
2 | # This is | ||
3 | # http://search.cpan.org/~amolloy/Ogg-Vorbis-Header-PurePerl-0.07/PurePerl.pm | ||
4 | # written by Andrew Molloy | ||
5 | # Code under GNU GENERAL PUBLIC LICENCE v2 | ||
6 | # $Id$ | ||
7 | ############################################################################# | ||
8 | |||
9 | package vorbiscomm; | ||
10 | |||
11 | use 5.005; | ||
12 | use strict; | ||
13 | use warnings; | ||
14 | |||
15 | use Fcntl qw/SEEK_END/; | ||
16 | |||
17 | our $VERSION = '0.07'; | ||
18 | |||
19 | sub new | ||
20 | { | ||
21 | my $class = shift; | ||
22 | my $file = shift; | ||
23 | |||
24 | return load($class, $file); | ||
25 | } | ||
26 | |||
27 | sub load | ||
28 | { | ||
29 | my $class = shift; | ||
30 | my $file = shift; | ||
31 | my $from_new = shift; | ||
32 | my %data; | ||
33 | my $self; | ||
34 | |||
35 | # there must be a better way... | ||
36 | if ($class eq 'vorbiscomm') | ||
37 | { | ||
38 | $self = bless \%data, $class; | ||
39 | } | ||
40 | else | ||
41 | { | ||
42 | $self = $class; | ||
43 | } | ||
44 | |||
45 | if ($self->{'FILE_LOADED'}) | ||
46 | { | ||
47 | return $self; | ||
48 | } | ||
49 | |||
50 | $self->{'FILE_LOADED'} = 1; | ||
51 | |||
52 | # check that the file exists and is readable | ||
53 | unless ( -e $file && -r _ ) | ||
54 | { | ||
55 | warn "File does not exist or cannot be read."; | ||
56 | # file does not exist, can't do anything | ||
57 | return undef; | ||
58 | } | ||
59 | # open up the file | ||
60 | open FILE, $file; | ||
61 | # make sure dos-type systems can handle it... | ||
62 | binmode FILE; | ||
63 | |||
64 | $data{'filename'} = $file; | ||
65 | $data{'fileHandle'} = \*FILE; | ||
66 | |||
67 | if (_init(\%data)) { | ||
68 | _loadInfo(\%data); | ||
69 | _loadComments(\%data); | ||
70 | _calculateTrackLength(\%data); | ||
71 | } | ||
72 | |||
73 | close FILE; | ||
74 | |||
75 | return $self; | ||
76 | } | ||
77 | |||
78 | sub info | ||
79 | { | ||
80 | my $self = shift; | ||
81 | my $key = shift; | ||
82 | |||
83 | # if the user did not supply a key, return the entire hash | ||
84 | unless ($key) | ||
85 | { | ||
86 | return $self->{'INFO'}; | ||
87 | } | ||
88 | |||
89 | # otherwise, return the value for the given key | ||
90 | return $self->{'INFO'}{lc $key}; | ||
91 | } | ||
92 | |||
93 | sub comment_tags | ||
94 | { | ||
95 | my $self = shift; | ||
96 | |||
97 | if ( $self && $self->{'COMMENT_KEYS'} ) { | ||
98 | return @{$self->{'COMMENT_KEYS'}}; | ||
99 | } | ||
100 | |||
101 | return undef; | ||
102 | } | ||
103 | |||
104 | sub comment | ||
105 | { | ||
106 | my $self = shift; | ||
107 | my $key = shift; | ||
108 | |||
109 | # if the user supplied key does not exist, return undef | ||
110 | unless($self->{'COMMENTS'}{lc $key}) | ||
111 | { | ||
112 | return undef; | ||
113 | } | ||
114 | |||
115 | return @{$self->{'COMMENTS'}{lc $key}}; | ||
116 | } | ||
117 | |||
118 | sub add_comments | ||
119 | { | ||
120 | warn "Ogg::Vorbis::Header::PurePerl add_comments() unimplemented."; | ||
121 | } | ||
122 | |||
123 | sub edit_comment | ||
124 | { | ||
125 | warn "Ogg::Vorbis::Header::PurePerl edit_comment() unimplemented."; | ||
126 | } | ||
127 | |||
128 | sub delete_comment | ||
129 | { | ||
130 | warn "Ogg::Vorbis::Header::PurePerl delete_comment() unimplemented."; | ||
131 | } | ||
132 | |||
133 | sub clear_comments | ||
134 | { | ||
135 | warn "Ogg::Vorbis::Header::PurePerl clear_comments() unimplemented."; | ||
136 | } | ||
137 | |||
138 | sub path | ||
139 | { | ||
140 | my $self = shift; | ||
141 | |||
142 | return $self->{'fileName'}; | ||
143 | } | ||
144 | |||
145 | sub write_vorbis | ||
146 | { | ||
147 | warn "Ogg::Vorbis::Header::PurePerl write_vorbis unimplemented."; | ||
148 | } | ||
149 | |||
150 | # "private" methods | ||
151 | |||
152 | sub _init | ||
153 | { | ||
154 | my $data = shift; | ||
155 | my $fh = $data->{'fileHandle'}; | ||
156 | my $byteCount = 0; | ||
157 | |||
158 | # check the header to make sure this is actually an Ogg-Vorbis file | ||
159 | $byteCount = _checkHeader($data); | ||
160 | |||
161 | unless($byteCount) | ||
162 | { | ||
163 | # if it's not, we can't do anything | ||
164 | return undef; | ||
165 | } | ||
166 | |||
167 | $data->{'startInfoHeader'} = $byteCount; | ||
168 | return 1; # Success | ||
169 | } | ||
170 | |||
171 | sub _checkHeader | ||
172 | { | ||
173 | my $data = shift; | ||
174 | my $fh = $data->{'fileHandle'}; | ||
175 | my $buffer; | ||
176 | my $pageSegCount; | ||
177 | my $byteCount = 0; # stores how far into the file we've read, | ||
178 | # so later reads into the file can skip right | ||
179 | # past all of the header stuff | ||
180 | |||
181 | # check that the first four bytes are 'OggS' | ||
182 | read($fh, $buffer, 4); | ||
183 | if ($buffer ne 'OggS') | ||
184 | { | ||
185 | warn "This is not an Ogg bitstream (no OggS header)."; | ||
186 | return undef; | ||
187 | } | ||
188 | $byteCount += 4; | ||
189 | |||
190 | # check the stream structure version (1 byte, should be 0x00) | ||
191 | read($fh, $buffer, 1); | ||
192 | if (ord($buffer) != 0x00) | ||
193 | { | ||
194 | warn "This is not an Ogg bitstream (invalid structure version)."; | ||
195 | return undef; | ||
196 | } | ||
197 | $byteCount += 1; | ||
198 | |||
199 | # check the header type flag | ||
200 | # This is a bitfield, so technically we should check all of the bits | ||
201 | # that could potentially be set. However, the only value this should | ||
202 | # possibly have at the beginning of a proper Ogg-Vorbis file is 0x02, | ||
203 | # so we just check for that. If it's not that, we go on anyway, but | ||
204 | # give a warning (this behavior may (should?) be modified in the future. | ||
205 | read($fh, $buffer, 1); | ||
206 | if (ord($buffer) != 0x02) | ||
207 | { | ||
208 | warn "Invalid header type flag (trying to go ahead anyway)."; | ||
209 | } | ||
210 | $byteCount += 1; | ||
211 | |||
212 | # skip to the page_segments count | ||
213 | read($fh, $buffer, 20); | ||
214 | $byteCount += 20; | ||
215 | # we do nothing with this data | ||
216 | |||
217 | # read the number of page segments | ||
218 | read($fh, $buffer, 1); | ||
219 | $pageSegCount = ord($buffer); | ||
220 | $byteCount += 1; | ||
221 | |||
222 | # read $pageSegCount bytes, then throw 'em out | ||
223 | read($fh, $buffer, $pageSegCount); | ||
224 | $byteCount += $pageSegCount; | ||
225 | |||
226 | # check packet type. Should be 0x01 (for indentification header) | ||
227 | read($fh, $buffer, 1); | ||
228 | if (ord($buffer) != 0x01) | ||
229 | { | ||
230 | warn "Wrong vorbis header type, giving up."; | ||
231 | return undef; | ||
232 | } | ||
233 | $byteCount += 1; | ||
234 | |||
235 | # check that the packet identifies itself as 'vorbis' | ||
236 | read($fh, $buffer, 6); | ||
237 | if ($buffer ne 'vorbis') | ||
238 | { | ||
239 | warn "This does not appear to be a vorbis stream, giving up."; | ||
240 | return undef; | ||
241 | } | ||
242 | $byteCount += 6; | ||
243 | |||
244 | # at this point, we assume the bitstream is valid | ||
245 | return $byteCount; | ||
246 | } | ||
247 | |||
248 | sub _loadInfo | ||
249 | { | ||
250 | my $data = shift; | ||
251 | my $start = $data->{'startInfoHeader'}; | ||
252 | my $fh = $data->{'fileHandle'}; | ||
253 | my $buffer; | ||
254 | my $byteCount = $start; | ||
255 | my %info; | ||
256 | |||
257 | seek $fh, $start, 0; | ||
258 | |||
259 | # read the vorbis version | ||
260 | read($fh, $buffer, 4); | ||
261 | $info{'version'} = _decodeInt($buffer); | ||
262 | $byteCount += 4; | ||
263 | |||
264 | # read the number of audio channels | ||
265 | read($fh, $buffer, 1); | ||
266 | $info{'channels'} = ord($buffer); | ||
267 | $byteCount += 1; | ||
268 | |||
269 | # read the sample rate | ||
270 | read($fh, $buffer, 4); | ||
271 | $info{'rate'} = _decodeInt($buffer); | ||
272 | $byteCount += 4; | ||
273 | |||
274 | # read the bitrate maximum | ||
275 | read($fh, $buffer, 4); | ||
276 | $info{'bitrate_upper'} = _decodeInt($buffer); | ||
277 | $byteCount += 4; | ||
278 | |||
279 | # read the bitrate nominal | ||
280 | read($fh, $buffer, 4); | ||
281 | $info{'bitrate_nominal'} = _decodeInt($buffer); | ||
282 | $byteCount += 4; | ||
283 | |||
284 | # read the bitrate minimal | ||
285 | read($fh, $buffer, 4); | ||
286 | $info{'bitrate_lower'} = _decodeInt($buffer); | ||
287 | $byteCount += 4; | ||
288 | |||
289 | # read the blocksize_0 and blocksize_1 | ||
290 | read($fh, $buffer, 1); | ||
291 | # these are each 4 bit fields, whose actual value is 2 to the power | ||
292 | # of the value of the field | ||
293 | $info{'blocksize_0'} = 2 << ((ord($buffer) & 0xF0) >> 4); | ||
294 | $info{'blocksize_1'} = 2 << (ord($buffer) & 0x0F); | ||
295 | $byteCount += 1; | ||
296 | |||
297 | # read the framing_flag | ||
298 | read($fh, $buffer, 1); | ||
299 | $info{'framing_flag'} = ord($buffer); | ||
300 | $byteCount += 1; | ||
301 | |||
302 | # bitrate_window is -1 in the current version of vorbisfile | ||
303 | $info{'bitrate_window'} = -1; | ||
304 | |||
305 | $data->{'startCommentHeader'} = $byteCount; | ||
306 | |||
307 | $data->{'INFO'} = \%info; | ||
308 | } | ||
309 | |||
310 | sub _loadComments | ||
311 | { | ||
312 | my $data = shift; | ||
313 | my $fh = $data->{'fileHandle'}; | ||
314 | my $start = $data->{'startCommentHeader'}; | ||
315 | my $buffer; | ||
316 | my $page_segments; | ||
317 | my $vendor_length; | ||
318 | my $user_comment_count; | ||
319 | my $byteCount = $start; | ||
320 | my %comments; | ||
321 | |||
322 | seek $fh, $start, 0; | ||
323 | |||
324 | # check that the first four bytes are 'OggS' | ||
325 | read($fh, $buffer, 4); | ||
326 | if ($buffer ne 'OggS') | ||
327 | { | ||
328 | warn "No comment header?"; | ||
329 | return undef; | ||
330 | } | ||
331 | $byteCount += 4; | ||
332 | |||
333 | # skip over next ten bytes | ||
334 | read($fh, $buffer, 10); | ||
335 | $byteCount += 10; | ||
336 | |||
337 | # read the stream serial number | ||
338 | read($fh, $buffer, 4); | ||
339 | push @{$data->{'commentSerialNumber'}}, _decodeInt($buffer); | ||
340 | $byteCount += 4; | ||
341 | |||
342 | # read the page sequence number (should be 0x01) | ||
343 | read($fh, $buffer, 4); | ||
344 | if (_decodeInt($buffer) != 0x01) | ||
345 | { | ||
346 | warn "Comment header page sequence number is not 0x01: " + | ||
347 | _decodeInt($buffer); | ||
348 | warn "Going to keep going anyway."; | ||
349 | } | ||
350 | $byteCount += 4; | ||
351 | |||
352 | # and ignore the page checksum for now | ||
353 | read($fh, $buffer, 4); | ||
354 | $byteCount += 4; | ||
355 | |||
356 | # get the number of entries in the segment_table... | ||
357 | read($fh, $buffer, 1); | ||
358 | $page_segments = _decodeInt($buffer); | ||
359 | $byteCount += 1; | ||
360 | # then skip on past it | ||
361 | read($fh, $buffer, $page_segments); | ||
362 | $byteCount += $page_segments; | ||
363 | |||
364 | # check the header type (should be 0x03) | ||
365 | read($fh, $buffer, 1); | ||
366 | if (ord($buffer) != 0x03) | ||
367 | { | ||
368 | warn "Wrong header type: " . ord($buffer); | ||
369 | } | ||
370 | $byteCount += 1; | ||
371 | |||
372 | # now we should see 'vorbis' | ||
373 | read($fh, $buffer, 6); | ||
374 | if ($buffer ne 'vorbis') | ||
375 | { | ||
376 | warn "Missing comment header. Should have found 'vorbis', found " . | ||
377 | $buffer; | ||
378 | } | ||
379 | $byteCount += 6; | ||
380 | |||
381 | # get the vendor length | ||
382 | read($fh, $buffer, 4); | ||
383 | $vendor_length = _decodeInt($buffer); | ||
384 | $byteCount += 4; | ||
385 | |||
386 | # read in the vendor | ||
387 | read($fh, $buffer, $vendor_length); | ||
388 | $comments{'vendor'} = $buffer; | ||
389 | $byteCount += $vendor_length; | ||
390 | |||
391 | # read in the number of user comments | ||
392 | read($fh, $buffer, 4); | ||
393 | $user_comment_count = _decodeInt($buffer); | ||
394 | $byteCount += 4; | ||
395 | |||
396 | $data->{'COMMENT_KEYS'} = []; | ||
397 | |||
398 | # finally, read the comments | ||
399 | for (my $i = 0; $i < $user_comment_count; $i++) | ||
400 | { | ||
401 | # first read the length | ||
402 | read($fh, $buffer, 4); | ||
403 | my $comment_length = _decodeInt($buffer); | ||
404 | $byteCount += 4; | ||
405 | |||
406 | # then the comment itself | ||
407 | read($fh, $buffer, $comment_length); | ||
408 | $byteCount += $comment_length; | ||
409 | |||
410 | my ($key) = $buffer =~ /^([^=]+)/; | ||
411 | my ($value) = $buffer =~ /=(.*)$/; | ||
412 | |||
413 | push @{$comments{lc $key}}, $value; | ||
414 | push @{$data->{'COMMENT_KEYS'}}, lc $key; | ||
415 | } | ||
416 | |||
417 | # read past the framing_bit | ||
418 | read($fh, $buffer, 1); | ||
419 | $byteCount += 1; | ||
420 | |||
421 | $data->{'INFO'}{'offset'} = $byteCount; | ||
422 | |||
423 | $data->{'COMMENTS'} = \%comments; | ||
424 | |||
425 | # Now find the offset of the first page | ||
426 | # with audio data. | ||
427 | while(_findPage($fh)) | ||
428 | { | ||
429 | $byteCount = tell($fh) - 4; | ||
430 | |||
431 | # version flag | ||
432 | read($fh, $buffer, 1); | ||
433 | if (ord($buffer) != 0x00) | ||
434 | { | ||
435 | warn "Invalid stream structure version: " . | ||
436 | sprintf("%x", ord($buffer)); | ||
437 | return; | ||
438 | } | ||
439 | |||
440 | # header type flag | ||
441 | read($fh, $buffer, 1); | ||
442 | # Audio data starts as a fresh packet on a new page, so | ||
443 | # if header_type is odd it's not a fresh packet | ||
444 | next if ( ord($buffer) % 2 ); | ||
445 | |||
446 | # skip past granule position, stream_serial_number, | ||
447 | # page_sequence_number, and crc | ||
448 | read($fh, $buffer, 20); | ||
449 | |||
450 | # page_segments | ||
451 | read($fh, $buffer, 1); | ||
452 | my $page_segments = ord($buffer); | ||
453 | |||
454 | # skip past the segment table | ||
455 | read($fh, $buffer, $page_segments); | ||
456 | |||
457 | # read packet_type byte | ||
458 | read($fh, $buffer, 1); | ||
459 | |||
460 | # Not an audio packet. All audio packet numbers are even | ||
461 | next if ( ord($buffer) % 2 ); | ||
462 | |||
463 | # Found the first audio packet | ||
464 | last; | ||
465 | } | ||
466 | |||
467 | $data->{'INFO'}{'audio_offset'} = $byteCount; | ||
468 | } | ||
469 | |||
470 | sub _calculateTrackLength | ||
471 | { | ||
472 | my $data = shift; | ||
473 | my $fh = $data->{'fileHandle'}; | ||
474 | my $buffer; | ||
475 | my $pageSize; | ||
476 | my $granule_position; | ||
477 | |||
478 | seek($fh,-8500,SEEK_END); # that magic number is from vorbisfile.c | ||
479 | # in the constant CHUNKSIZE, which comes | ||
480 | # with the comment /* a shade over 8k; | ||
481 | # anyone using pages well over 8k gets | ||
482 | # what they deserve */ | ||
483 | |||
484 | # we just keep looking through the headers until we get to the last one | ||
485 | # (there might be a couple of blocks here) | ||
486 | while(_findPage($fh)) | ||
487 | { | ||
488 | # stream structure version - must be 0x00 | ||
489 | read($fh, $buffer, 1); | ||
490 | if (ord($buffer) != 0x00) | ||
491 | { | ||
492 | warn "Invalid stream structure version: " . | ||
493 | sprintf("%x", ord($buffer)); | ||
494 | return; | ||
495 | } | ||
496 | |||
497 | # header type flag | ||
498 | read($fh, $buffer, 1); | ||
499 | # we should check this, but for now we'll just ignore it | ||
500 | |||
501 | # absolute granule position - this is what we need! | ||
502 | read($fh, $buffer, 8); | ||
503 | $granule_position = _decodeInt($buffer); | ||
504 | |||
505 | # skip past stream_serial_number, page_sequence_number, and crc | ||
506 | read($fh, $buffer, 12); | ||
507 | |||
508 | # page_segments | ||
509 | read($fh, $buffer, 1); | ||
510 | my $page_segments = ord($buffer); | ||
511 | |||
512 | # reset pageSize | ||
513 | $pageSize = 0; | ||
514 | |||
515 | # calculate approx. page size | ||
516 | for (my $i = 0; $i < $page_segments; $i++) | ||
517 | { | ||
518 | read($fh, $buffer, 1); | ||
519 | $pageSize += ord($buffer); | ||
520 | } | ||
521 | |||
522 | seek $fh, $pageSize, 1; | ||
523 | } | ||
524 | |||
525 | $data->{'INFO'}{'length'} = | ||
526 | int($granule_position / $data->{'INFO'}{'rate'}); | ||
527 | } | ||
528 | |||
529 | sub _findPage | ||
530 | { | ||
531 | # search forward in the file for the 'OggS' page header | ||
532 | my $fh = shift; | ||
533 | my $char; | ||
534 | my $curStr = ''; | ||
535 | |||
536 | while (read($fh, $char, 1)) | ||
537 | { | ||
538 | $curStr = $char . $curStr; | ||
539 | $curStr = substr($curStr, 0, 4); | ||
540 | |||
541 | # we are actually looking for the string 'SggO' because we | ||
542 | # tack character on to our test string backwards, to make | ||
543 | # trimming it to 4 characters easier. | ||
544 | if ($curStr eq 'SggO') | ||
545 | { | ||
546 | return 1; | ||
547 | } | ||
548 | } | ||
549 | |||
550 | return undef; | ||
551 | } | ||
552 | |||
553 | sub _decodeInt | ||
554 | { | ||
555 | my $bytes = shift; | ||
556 | my $num = 0; | ||
557 | my @byteList = split //, $bytes; | ||
558 | my $numBytes = @byteList; | ||
559 | my $mult = 1; | ||
560 | |||
561 | for (my $i = 0; $i < $numBytes; $i ++) | ||
562 | { | ||
563 | $num += ord($byteList[$i]) * $mult; | ||
564 | $mult *= 256; | ||
565 | } | ||
566 | |||
567 | return $num; | ||
568 | } | ||
569 | |||
570 | sub _decodeInt5Bit | ||
571 | { | ||
572 | my $byte = ord(shift); | ||
573 | |||
574 | $byte = $byte & 0xF8; # clear out the bottm 3 bits | ||
575 | $byte = $byte >> 3; # and shifted down to where it belongs | ||
576 | |||
577 | return $byte; | ||
578 | } | ||
579 | |||
580 | sub _decodeInt4Bit | ||
581 | { | ||
582 | my $byte = ord(shift); | ||
583 | |||
584 | $byte = $byte & 0xFC; # clear out the bottm 4 bits | ||
585 | $byte = $byte >> 4; # and shifted down to where it belongs | ||
586 | |||
587 | return $byte; | ||
588 | } | ||
589 | |||
590 | sub _ilog | ||
591 | { | ||
592 | my $x = shift; | ||
593 | my $ret = 0; | ||
594 | |||
595 | unless ($x > 0) | ||
596 | { | ||
597 | return 0; | ||
598 | } | ||
599 | |||
600 | while ($x > 0) | ||
601 | { | ||
602 | $ret++; | ||
603 | $x = $x >> 1; | ||
604 | } | ||
605 | |||
606 | return $ret; | ||
607 | } | ||
608 | |||
609 | 1; | ||
610 | __DATA__ | ||
611 | |||
612 | =head1 NAME | ||
613 | |||
614 | Ogg::Vorbis::Header::PurePerl - An object-oriented interface to Ogg Vorbis | ||
615 | information and comment fields, implemented entirely in Perl. Intended to be | ||
616 | a drop in replacement for Ogg::Vobis::Header. | ||
617 | |||
618 | Unlike Ogg::Vorbis::Header, this module will go ahead and fill in all of the | ||
619 | information fields as soon as you construct the object. In other words, | ||
620 | the C<new> and C<load> constructors have identical behavior. | ||
621 | |||
622 | =head1 SYNOPSIS | ||
623 | |||
624 | use Ogg::Vorbis::Header::PurePerl; | ||
625 | my $ogg = Ogg::Vorbis::Header::PurePerl->new("song.ogg"); | ||
626 | while (my ($k, $v) = each %{$ogg->info}) { | ||
627 | print "$k: $v\n"; | ||
628 | } | ||
629 | foreach my $com ($ogg->comment_tags) { | ||
630 | print "$com: $_\n" foreach $ogg->comment($com); | ||
631 | } | ||
632 | |||
633 | =head1 DESCRIPTION | ||
634 | |||
635 | This module is intended to be a drop in replacement for Ogg::Vorbis::Header, | ||
636 | implemented entirely in Perl. It provides an object-oriented interface to | ||
637 | Ogg Vorbis information and comment fields. (NOTE: This module currently | ||
638 | supports only read operations). | ||
639 | |||
640 | =head1 CONSTRUCTORS | ||
641 | |||
642 | =head2 C<new ($filename)> | ||
643 | |||
644 | Opens an Ogg Vorbis file, ensuring that it exists and is actually an | ||
645 | Ogg Vorbis stream. This method does not actually read any of the | ||
646 | information or comment fields, and closes the file immediately. | ||
647 | |||
648 | =head2 C<load ([$filename])> | ||
649 | |||
650 | Opens an Ogg Vorbis file, ensuring that it exists and is actually an | ||
651 | Ogg Vorbis stream, then loads the information and comment fields. This | ||
652 | method can also be used without a filename to load the information | ||
653 | and fields of an already constructed instance. | ||
654 | |||
655 | =head1 INSTANCE METHODS | ||
656 | |||
657 | =head2 C<info ([$key])> | ||
658 | |||
659 | Returns a hashref containing information about the Ogg Vorbis file from | ||
660 | the file's information header. Hash fields are: version, channels, rate, | ||
661 | bitrate_upper, bitrate_nominal, bitrate_lower, bitrate_window, and length. | ||
662 | The bitrate_window value is not currently used by the vorbis codec, and | ||
663 | will always be -1. | ||
664 | |||
665 | The optional parameter, key, allows you to retrieve a single value from | ||
666 | the object's hash. Returns C<undef> if the key is not found. | ||
667 | |||
668 | =head2 C<comment_tags ()> | ||
669 | |||
670 | Returns an array containing the key values for the comment fields. | ||
671 | These values can then be passed to C<comment> to retrieve their values. | ||
672 | |||
673 | =head2 C<comment ($key)> | ||
674 | |||
675 | Returns an array of comment values associated with the given key. | ||
676 | |||
677 | =head2 C<add_comments ($key, $value, [$key, $value, ...])> | ||
678 | |||
679 | Unimplemented. | ||
680 | |||
681 | =head2 C<edit_comment ($key, $value, [$num])> | ||
682 | |||
683 | Unimplemented. | ||
684 | |||
685 | =head2 C<delete_comment ($key, [$num])> | ||
686 | |||
687 | Unimplemented. | ||
688 | |||
689 | =head2 C<clear_comments ([@keys])> | ||
690 | |||
691 | Unimplemented. | ||
692 | |||
693 | =head2 C<write_vorbis ()> | ||
694 | |||
695 | Unimplemented. | ||
696 | |||
697 | =head2 C<path ()> | ||
698 | |||
699 | Returns the path/filename of the file the object represents. | ||
700 | |||
701 | =head1 NOTE | ||
702 | |||
703 | This is ALPHA SOFTWARE. It may very well be very broken. Do not use it in | ||
704 | a production environment. You have been warned. | ||
705 | |||
706 | =head1 ACKNOWLEDGEMENTS | ||
707 | |||
708 | Dave Brown <cpan@dagbrown.com> made this module significantly faster | ||
709 | at calculating the length of ogg files. | ||
710 | |||
711 | Robert Moser II <rlmoser@earthlink.net> fixed a problem with files that | ||
712 | have no comments. | ||
713 | |||
714 | =head1 AUTHOR | ||
715 | |||
716 | Andrew Molloy E<lt>amolloy@kaizolabs.comE<gt> | ||
717 | |||
718 | =head1 COPYRIGHT | ||
719 | |||
720 | Copyright (c) 2003, Andrew Molloy. All Rights Reserved. | ||
721 | |||
722 | This program is free software; you can redistribute it and/or modify it | ||
723 | under the terms of the GNU General Public License as published by the | ||
724 | Free Software Foundation; either version 2 of the License, or (at | ||
725 | your option) any later version. A copy of this license is included | ||
726 | with this module (LICENSE.GPL). | ||
727 | |||
728 | =head1 SEE ALSO | ||
729 | |||
730 | L<Ogg::Vorbis::Header>, L<Ogg::Vorbis::Decoder> | ||
731 | |||
732 | =cut | ||