ruby-mp3info 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/mp3info.rb +720 -0
  2. metadata +37 -0
data/lib/mp3info.rb ADDED
@@ -0,0 +1,720 @@
1
+ # $Id: mp3info.rb,v 1.5 2005/04/26 13:41:41 moumar Exp $
2
+ # = Description
3
+ #
4
+ # ruby-mp3info gives you access to low level informations on mp3 files
5
+ # (bitrate, length, samplerate, etc...). It can read, write, remove id3v1 tag
6
+ # and read id3v2. It is written in pure ruby.
7
+ #
8
+ #
9
+ # = Download
10
+ #
11
+ # get tar.gz at
12
+ # http://rubyforge.org/projects/ruby-mp3info/
13
+ #
14
+ #
15
+ # = Installation
16
+ #
17
+ # $ ruby install.rb config
18
+ # $ ruby install.rb setup
19
+ # # ruby install.rb install
20
+ #
21
+ # or
22
+ #
23
+ # # gem install ruby-mp3info
24
+ #
25
+ #
26
+ # = Example
27
+ #
28
+ # require "mp3info"
29
+ #
30
+ # mp3info = Mp3Info.new("myfile.mp3")
31
+ # puts mp3info
32
+ #
33
+ #
34
+ # = Testing
35
+ #
36
+ # Test::Unit library is used for tests. see http://testunit.talbott.ws/
37
+ #
38
+ # $ ruby test.rb
39
+ #
40
+ #
41
+ # = ToDo
42
+ #
43
+ # * adding write support for ID3v2 tags
44
+ # * adding a test for id3v2
45
+ # * encoder detection
46
+ #
47
+ #
48
+ # = Changelog
49
+ #
50
+ # [0.4 26/04/2005]
51
+ #
52
+ # * fixes in vbr mode
53
+ # * removed extract_info_from_head() function
54
+ # * now try several times to find a good header frame before giving up
55
+ # * correct handling of unicode in v2 tags. Require standard "iconv" library if such tags are used
56
+ # * FIXED if a tag appears more than one time, create an array with every value found for this tag
57
+ #
58
+ #
59
+ # [0.3 04/05/2004]
60
+ #
61
+ # * massive changes of most of the code to make it easier to read & hopefully run faster
62
+ # * ID2TAGS hash is just informative now, no use of it in the code. id3v2 tag fields are read in directly
63
+ # * added support for id3 v2.2 and v2.4 (0.2.1 only supported v2.3)
64
+ # * much improved vbr duration guessing
65
+ # * made Mp3Info#to_s output to be prettier
66
+ # * moved hastag1? and hastag2? to be class booleans instead of functions (now named hastag1 and hastag2)
67
+ # * fixed a bug on computing "error_protection" attribute
68
+ # * new attribute "tag", which is a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
69
+ # * new method hastag?, which test the presence of any tag
70
+ #
71
+ #
72
+ # [0.2.1 04/09/2003]
73
+ #
74
+ # * filename attribute added
75
+ # * mp3 files are opened read-only now [Alan Davies <alan__DOT_davies__AT__thomson.com>]
76
+ # * Mp3Info#initialize: bugfixes [Alan Davies <alan__DOT_davies__AT__thomson.com>]
77
+ # * put NULLs in year field in id3v1 tags instead of zeros [Alan Davies <alan__DOT_davies__AT__thomson.com>]
78
+ # * Mp3Info#gettag1: remove null at end of strings [Alan Davies <alan__DOT_davies__AT__thomson.com>]
79
+ # * Mp3Info#extract_infos_from_head(): some brackets missed [Alan Davies <alan__DOT_davies__AT__thomson.com>]
80
+ #
81
+ #
82
+ # [0.2 18/08/2003]
83
+ #
84
+ # * writing, reading and removing of id3v1 tags
85
+ # * reading of id3v2 tags
86
+ # * test suite improved
87
+ # * to_s method added
88
+ # * length attribute is a Float now
89
+ #
90
+ #
91
+ # [0.1 17/03/2003]
92
+ #
93
+ # * Initial version
94
+ #
95
+ #
96
+ # License:: Ruby
97
+ # Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
98
+ # Website:: http://ruby-mp3info.rubyforge.org/
99
+
100
+ # Raised on any kind of error related to ruby-mp3info
101
+ class Mp3InfoError < StandardError ; end
102
+
103
+ class Mp3InfoInternalError < StandardError #:nodoc:
104
+ end
105
+
106
+ class Numeric
107
+ ### returns the selected bit range (b, a) as a number
108
+ ### NOTE: b > a if not, returns 0
109
+ def bits(b, a)
110
+ t = 0
111
+ b.downto(a) { |i| t += t + self[i] }
112
+ t
113
+ end
114
+ end
115
+
116
+ class Hash
117
+ ### lets you specify hash["key"] as hash.key
118
+ ### this came from CodingInRuby on RubyGarden
119
+ ### http://www.rubygarden.org/ruby?CodingInRuby
120
+ def method_missing(meth,*args)
121
+ if /=$/=~(meth=meth.id2name) then
122
+ self[meth[0...-1]] = (args.length<2 ? args[0] : args)
123
+ else
124
+ self[meth]
125
+ end
126
+ end
127
+ end
128
+
129
+ class File
130
+ def get32bits
131
+ (getc << 24) + (getc << 16) + (getc << 8) + getc
132
+ end
133
+ def get_syncsafe
134
+ (getc << 21) + (getc << 14) + (getc << 7) + getc
135
+ end
136
+ end
137
+
138
+ class Mp3Info
139
+
140
+ VERSION = "0.4"
141
+
142
+ LAYER = [ nil, 3, 2, 1]
143
+ BITRATE = [
144
+ [
145
+ [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
146
+ [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
147
+ [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] ],
148
+ [
149
+ [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
150
+ [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
151
+ [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
152
+ ]
153
+ ]
154
+ SAMPLERATE = [
155
+ [ 44100, 48000, 32000 ],
156
+ [ 22050, 24000, 16000 ]
157
+ ]
158
+ CHANNEL_MODE = [ "Stereo", "JStereo", "Dual Channel", "Single Channel"]
159
+
160
+ GENRES = [
161
+ "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
162
+ "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
163
+ "Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
164
+ "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
165
+ "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
166
+ "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
167
+ "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
168
+ "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
169
+ "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
170
+ "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
171
+ "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
172
+ "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
173
+ "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
174
+ "Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
175
+ "Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
176
+ "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
177
+ "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
178
+ "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
179
+ "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
180
+ "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
181
+ "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
182
+ "Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
183
+ "Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
184
+ "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
185
+ "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
186
+ "SynthPop" ]
187
+
188
+
189
+ ID2TAGS = {
190
+ "AENC" => "Audio encryption",
191
+ "APIC" => "Attached picture",
192
+ "COMM" => "Comments",
193
+ "COMR" => "Commercial frame",
194
+ "ENCR" => "Encryption method registration",
195
+ "EQUA" => "Equalization",
196
+ "ETCO" => "Event timing codes",
197
+ "GEOB" => "General encapsulated object",
198
+ "GRID" => "Group identification registration",
199
+ "IPLS" => "Involved people list",
200
+ "LINK" => "Linked information",
201
+ "MCDI" => "Music CD identifier",
202
+ "MLLT" => "MPEG location lookup table",
203
+ "OWNE" => "Ownership frame",
204
+ "PRIV" => "Private frame",
205
+ "PCNT" => "Play counter",
206
+ "POPM" => "Popularimeter",
207
+ "POSS" => "Position synchronisation frame",
208
+ "RBUF" => "Recommended buffer size",
209
+ "RVAD" => "Relative volume adjustment",
210
+ "RVRB" => "Reverb",
211
+ "SYLT" => "Synchronized lyric/text",
212
+ "SYTC" => "Synchronized tempo codes",
213
+ "TALB" => "Album/Movie/Show title",
214
+ "TBPM" => "BPM (beats per minute)",
215
+ "TCOM" => "Composer",
216
+ "TCON" => "Content type",
217
+ "TCOP" => "Copyright message",
218
+ "TDAT" => "Date",
219
+ "TDLY" => "Playlist delay",
220
+ "TENC" => "Encoded by",
221
+ "TEXT" => "Lyricist/Text writer",
222
+ "TFLT" => "File type",
223
+ "TIME" => "Time",
224
+ "TIT1" => "Content group description",
225
+ "TIT2" => "Title/songname/content description",
226
+ "TIT3" => "Subtitle/Description refinement",
227
+ "TKEY" => "Initial key",
228
+ "TLAN" => "Language(s)",
229
+ "TLEN" => "Length",
230
+ "TMED" => "Media type",
231
+ "TOAL" => "Original album/movie/show title",
232
+ "TOFN" => "Original filename",
233
+ "TOLY" => "Original lyricist(s)/text writer(s)",
234
+ "TOPE" => "Original artist(s)/performer(s)",
235
+ "TORY" => "Original release year",
236
+ "TOWN" => "File owner/licensee",
237
+ "TPE1" => "Lead performer(s)/Soloist(s)",
238
+ "TPE2" => "Band/orchestra/accompaniment",
239
+ "TPE3" => "Conductor/performer refinement",
240
+ "TPE4" => "Interpreted, remixed, or otherwise modified by",
241
+ "TPOS" => "Part of a set",
242
+ "TPUB" => "Publisher",
243
+ "TRCK" => "Track number/Position in set",
244
+ "TRDA" => "Recording dates",
245
+ "TRSN" => "Internet radio station name",
246
+ "TRSO" => "Internet radio station owner",
247
+ "TSIZ" => "Size",
248
+ "TSRC" => "ISRC (international standard recording code)",
249
+ "TSSE" => "Software/Hardware and settings used for encoding",
250
+ "TYER" => "Year",
251
+ "TXXX" => "User defined text information frame",
252
+ "UFID" => "Unique file identifier",
253
+ "USER" => "Terms of use",
254
+ "USLT" => "Unsychronized lyric/text transcription",
255
+ "WCOM" => "Commercial information",
256
+ "WCOP" => "Copyright/Legal information",
257
+ "WOAF" => "Official audio file webpage",
258
+ "WOAR" => "Official artist/performer webpage",
259
+ "WOAS" => "Official audio source webpage",
260
+ "WORS" => "Official internet radio station homepage",
261
+ "WPAY" => "Payment",
262
+ "WPUB" => "Publishers official webpage",
263
+ "WXXX" => "User defined URL link frame"
264
+ }
265
+
266
+ TAGSIZE = 128
267
+ #MAX_FRAME_COUNT = 6 #number of frame to read for encoder detection
268
+
269
+ # mpeg version = 1 or 2
270
+ attr_reader(:mpeg_version)
271
+
272
+ # layer = 1, 2, or 3
273
+ attr_reader(:layer)
274
+
275
+ # bitrate in kbps
276
+ attr_reader(:bitrate)
277
+
278
+ # samplerate in Hz
279
+ attr_reader(:samplerate)
280
+
281
+ # channel mode => "Stereo", "JStereo", "Dual Channel" or "Single Channel"
282
+ attr_reader(:channel_mode)
283
+
284
+ # variable bitrate => true or false
285
+ attr_reader(:vbr)
286
+
287
+ # length in seconds as a Float
288
+ attr_reader(:length)
289
+
290
+ # error protection => true or false
291
+ attr_reader(:error_protection)
292
+
293
+ #a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
294
+ attr_reader(:tag)
295
+
296
+ # id3v1 tag has a Hash. You can modify it, it will be written when calling
297
+ # "close" method.
298
+ attr_accessor(:tag1)
299
+
300
+ # id3v2 tag as a Hash
301
+ attr_reader(:tag2)
302
+
303
+ # the original filename
304
+ attr_reader(:filename)
305
+
306
+ # Moved hastag1? and hastag2? to be booleans
307
+ attr_reader(:hastag1, :hastag2)
308
+
309
+ # Test the presence of an id3v1 tag in file +filename+
310
+ def self.hastag1?(filename)
311
+ File.open(filename) { |f|
312
+ f.seek(-TAGSIZE, File::SEEK_END)
313
+ f.read(3) == "TAG"
314
+ }
315
+ end
316
+
317
+ # Test the presence of an id3v2 tag in file +filename+
318
+ def self.hastag2?(filename)
319
+ File.open(filename) { |f|
320
+ f.read(3) == "ID3"
321
+ }
322
+ end
323
+
324
+
325
+ # Remove id3v1 tag from +filename+
326
+ def self.removetag1(filename)
327
+ if self.hastag1?(filename)
328
+ newsize = File.size(filename) - TAGSIZE
329
+ File.open(filename, "r+") { |f| f.truncate(newsize) }
330
+ end
331
+ end
332
+
333
+ # Instantiate a new Mp3Info object with name +filename+
334
+ def initialize(filename)
335
+ $stderr.puts("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
336
+ raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
337
+ @filename = filename
338
+ @hastag1, @hastag2 = false
339
+ @tag = Hash.new
340
+ @tag1 = Hash.new
341
+ @tag2 = Hash.new
342
+
343
+ @file = File.new(filename, "rb")
344
+ parse_tags
345
+ @tag_orig = @tag1.dup
346
+
347
+ #creation of a sort of "universal" tag, regardless of the tag version
348
+ if hastag2?
349
+ h = {
350
+ "title" => "TIT2",
351
+ "artist" => "TPE1",
352
+ "album" => "TALB",
353
+ "year" => "TYER",
354
+ "tracknum" => "TRCK",
355
+ "comments" => "COMM",
356
+ "genre" => 255,
357
+ "genre_s" => "TCON"
358
+ }
359
+
360
+ h.each { |k, v| @tag[k] = @tag2[v] }
361
+
362
+ elsif hastag1?
363
+ @tag = @tag1.dup
364
+ end
365
+
366
+
367
+ ### extracts MPEG info from MPEG header and stores it in the hash @mpeg
368
+ ### head (fixnum) = valid 4 byte MPEG header
369
+
370
+ found = false
371
+
372
+ 5.times do
373
+ head = find_next_frame()
374
+ @mpeg_version = [2, 1][head[19]]
375
+ @layer = LAYER[head.bits(18,17)]
376
+ next if @layer.nil?
377
+ @bitrate = BITRATE[@mpeg_version-1][@layer-1][head.bits(15,12)-1]
378
+ @error_protection = head[16] == 0 ? true : false
379
+ @samplerate = SAMPLERATE[@mpeg_version-1][head.bits(11,10)]
380
+ @padding = (head[9] == 1 ? true : false)
381
+ @channel_mode = CHANNEL_MODE[@channel_num = head.bits(7,6)]
382
+ @copyright = (head[3] == 1 ? true : false)
383
+ @original = (head[2] == 1 ? true : false)
384
+ @vbr = false
385
+ found = true
386
+ break
387
+ end
388
+
389
+ raise(Mp3InfoError, "Cannot find good frame") unless found
390
+
391
+
392
+ seek = @mpeg_version == 1 ?
393
+ (@channel_num == 3 ? 17 : 32) :
394
+ (@channel_num == 3 ? 9 : 17)
395
+
396
+ @file.seek(seek, IO::SEEK_CUR)
397
+
398
+ vbr_head = @file.read(4)
399
+ if vbr_head == "Xing"
400
+ flags = @file.get32bits
401
+ @streamsize = @frames = 0
402
+ flags[1] == 1 and @frames = @file.get32bits
403
+ flags[2] == 1 and @streamsize = @file.get32bits
404
+ # currently this just skips the TOC entries if they're found
405
+ @file.seek(100, IO::SEEK_CUR) if flags[0] == 1
406
+ @vbr_quality = @file.get32bits if flags[3] == 1
407
+ @length = (26/1000.0)*@frames
408
+ @bitrate = (((@streamsize/@frames)*@samplerate)/144) >> 10
409
+ @vbr = true
410
+ else
411
+ # for cbr, calculate duration with the given bitrate
412
+ @streamsize = @file.stat.size - (@hastag1 ? TAGSIZE : 0) - (@hastag2 ? @tag2["length"] : 0)
413
+ @length = ((@streamsize << 3)/1000.0)/@bitrate
414
+ if @tag2["TLEN"]
415
+ # but if another duration is given and it isn't close (within 5%)
416
+ # assume the mp3 is vbr and go with the given duration
417
+ tlen = (@tag2["TLEN"].to_i)/1000
418
+ percent_diff = ((@length.to_i-tlen)/tlen.to_f)
419
+ if percent_diff.abs > 0.05
420
+ # without the xing header, this is the best guess without reading
421
+ # every single frame
422
+ @vbr = true
423
+ @length = @tag2["TLEN"].to_i/1000
424
+ @bitrate = (@streamsize / @bitrate) >> 10
425
+ end
426
+ end
427
+ end
428
+ end
429
+
430
+ # "block version" of Mp3Info::new()
431
+ def self.open(filename)
432
+ m = self.new(filename)
433
+ ret = nil
434
+ if block_given?
435
+ begin
436
+ ret = yield(m)
437
+ ensure
438
+ m.close
439
+ end
440
+ else
441
+ ret = m
442
+ end
443
+ ret
444
+ end
445
+
446
+ # Remove id3v1 from mp3
447
+ def removetag1
448
+ if hastag1?
449
+ newsize = @file.stat.size(filename) - TAGSIZE
450
+ @file.truncate(newsize)
451
+ @tag1.clear
452
+ end
453
+ self
454
+ end
455
+
456
+ # Has file an id3v1 or v2 tag? true or false
457
+ def hastag?
458
+ @hastag1 or @hastag2
459
+ end
460
+
461
+ # Has file an id3v1 tag? true or false
462
+ def hastag1?
463
+ @hastag1
464
+ end
465
+
466
+ # Has file an id3v2 tag? true or false
467
+ def hastag2?
468
+ @hastag2
469
+ end
470
+
471
+
472
+ # Flush pending modifications to tags and close the file
473
+ def close
474
+ return if @file.nil?
475
+ if @tag1 != @tag_orig
476
+ @tag_orig.update(@tag1)
477
+ #puts "@tag_orig: #{@tag_orig.inspect}"
478
+ @file.reopen(@filename, 'rb+')
479
+ @file.seek(-TAGSIZE, File::SEEK_END)
480
+ t = @file.read(3)
481
+ if t != 'TAG'
482
+ #append new tag
483
+ @file.seek(0, File::SEEK_END)
484
+ @file.write('TAG')
485
+ end
486
+ str = [
487
+ @tag_orig["title"]||"",
488
+ @tag_orig["artist"]||"",
489
+ @tag_orig["album"]||"",
490
+ ((@tag_orig["year"] != 0) ? ("%04d" % @tag_orig["year"]) : "\0\0\0\0"),
491
+ @tag_orig["comments"]||"",
492
+ 0,
493
+ @tag_orig["tracknum"]||0,
494
+ @tag_orig["genre"]||255
495
+ ].pack("Z30Z30Z30Z4Z28CCC")
496
+ @file.write(str)
497
+ end
498
+ @file.close
499
+ @file = nil
500
+ end
501
+
502
+ # inspect inside Mp3Info
503
+ def to_s
504
+ s = "MPEG #{@mpeg_version} Layer #{@layer} #{@vbr ? "VBR" : "CBR"} #{@bitrate} Kbps #{@channel_mode} #{@samplerate} Hz length #{@length} sec. error protection #{@error_protection} "
505
+ s << "tag1: "+@tag1.inspect+"\n" if @hastag1
506
+ s << "tag2: "+@tag2.inspect+"\n" if @hastag2
507
+ s
508
+ end
509
+
510
+
511
+ private
512
+
513
+ ### parses the id3 tags of the currently open @file
514
+ def parse_tags
515
+ return if @file.stat.size < TAGSIZE # file is too small
516
+ @file.seek(0)
517
+ f3 = @file.read(3)
518
+ gettag1 if f3 == "TAG" # v1 tag at beginning
519
+ gettag2 if f3 == "ID3" # v2 tag at beginning
520
+ unless @hastag1 # v1 tag at end
521
+ # this preserves the file pos if tag2 found, since gettag2 leaves
522
+ # the file at the best guess as to the first MPEG frame
523
+ pos = (@hastag2 ? @file.pos : 0)
524
+ # seek to where id3v1 tag should be
525
+ @file.seek(-TAGSIZE, IO::SEEK_END)
526
+ gettag1 if @file.read(3) == "TAG"
527
+ @file.seek(pos)
528
+ end
529
+ end
530
+
531
+ ### reads in id3 field strings, stripping out non-printable chars
532
+ ### len (fixnum) = number of chars in field
533
+ ### returns string
534
+ def read_id3_string(len)
535
+ #FIXME handle unicode strings
536
+ #return @file.read(len)
537
+ s = ""
538
+ len.times do
539
+ c = @file.getc
540
+ # only append printable characters
541
+ s << c if c >= 32 and c < 254
542
+ end
543
+ return s.strip
544
+ #return (s[0..2] == "eng" ? s[3..-1] : s)
545
+ end
546
+
547
+ ### gets id3v1 tag information from @file
548
+ ### assumes @file is pointing to char after "TAG" id
549
+ def gettag1
550
+ @hastag1 = true
551
+ @tag1["title"] = read_id3_string(30)
552
+ @tag1["artist"] = read_id3_string(30)
553
+ @tag1["album"] = read_id3_string(30)
554
+ year_t = read_id3_string(4).to_i
555
+ @tag1["year"] = year_t unless year_t == 0
556
+ comments = @file.read(30)
557
+ if comments[-2] == 0
558
+ @tag1["tracknum"] = comments[-1].to_i
559
+ comments.chop! #remove the last char
560
+ end
561
+ #@tag1["comments"] = comments.sub!(/\0.*$/, '')
562
+ @tag1["comments"] = comments.strip
563
+ @tag1["genre"] = @file.getc
564
+ @tag1["genre_s"] = GENRES[@tag1["genre"]] || ""
565
+ end
566
+
567
+ ### gets id3v2 tag information from @file
568
+ def gettag2
569
+ @file.seek(3)
570
+ version_maj, version_min, flags = @file.read(3).unpack("CCB4")
571
+ unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
572
+ return unless [2, 3, 4].include?(version_maj)
573
+ @hastag2 = true
574
+ @tag2["version"] = "2.#{version_maj}.#{version_min}"
575
+ tag2_len = @file.get_syncsafe
576
+ case version_maj
577
+ when 2
578
+ read_id3v2_2_frames(tag2_len)
579
+ when 3,4
580
+ # seek past extended header if present
581
+ @file.seek(@file.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
582
+ read_id3v2_3_frames(tag2_len)
583
+ end
584
+ tag2["length"] = @file.pos
585
+ # we should now have @file sitting at the first MPEG frame
586
+ end
587
+
588
+ ### runs thru @file one char at a time looking for best guess of first MPEG
589
+ ### frame, which should be first 0xff byte after id3v2 padding zero's
590
+ ### returns true
591
+ def v2_end?
592
+ until @file.getc == 0xff
593
+ end
594
+ @file.seek(-1, IO::SEEK_CUR)
595
+ true
596
+ end
597
+
598
+ ### reads id3 ver 2.3.x/2.4.x frames and adds the contents to @tag2 hash
599
+ ### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
600
+ ### NOTE: the id3v2 header does not take padding zero's into consideration
601
+ def read_id3v2_3_frames(tag2_len)
602
+ v2end_found = false
603
+ until v2end_found # there are 2 ways to end the loop
604
+ name = @file.read(4)
605
+ if name[0] == 0
606
+ @file.seek(-4, IO::SEEK_CUR) # 1. find a padding zero,
607
+ v2end_found = v2_end? # so we seek to end of zeros
608
+ else
609
+ size = @file.get32bits
610
+ @file.seek(2, IO::SEEK_CUR) # skip flags
611
+ add_value_to_tag2(name, size)
612
+ # case name
613
+ # when /T[A-Z]+|COMM/
614
+ # data = read_id3_string(size-1)
615
+ # add_value_to_tag2(name, data)
616
+ # else
617
+ # @file.seek(size-1, IO::SEEK_CUR)
618
+ # end
619
+ v2end_found = true if @file.pos >= tag2_len # 2. reach length from header
620
+ end
621
+ end
622
+ end
623
+
624
+ ### reads id3 ver 2.2.x frames and adds the contents to @tag2 hash
625
+ ### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
626
+ ### NOTE: the id3v2 header does not take padding zero's into consideration
627
+ def read_id3v2_2_frames(tag2_len)
628
+ v2end_found = false
629
+ until v2end_found
630
+ name = @file.read(3)
631
+ if name[0] == 0
632
+ @file.seek(-3, IO::SEEK_CUR)
633
+ v2end_found = v2_end?
634
+ else
635
+ size = (@file.getc << 16) + (@file.getc << 8) + @file.getc
636
+ add_value_to_tag2(name, size)
637
+ v2end_found = true if @file.pos >= tag2_len
638
+ end
639
+ end
640
+ end
641
+
642
+ ### Add data to tag2["name"]
643
+ ### read lang_encoding, decode data if unicode and
644
+ ### create an array if the key ever exists in the tag
645
+ def add_value_to_tag2(name, size)
646
+ lang_encoding = @file.getc # language encoding bit 0 for iso_8859_1, 1 for unicode
647
+ data = size == 0 ? "" : @file.read(size-1)
648
+
649
+ if lang_encoding == 1 and name[0] == ?T
650
+ require "iconv"
651
+
652
+ #strip byte-order bytes at the beginning of the unicode string if they exists
653
+ data[0..3] =~ /^[\xff\xfe]+$/ and data = data[2..-1]
654
+
655
+ data = Iconv.iconv("ISO-8859-1", "UNICODE", data)[0]
656
+ end
657
+
658
+ if @tag2.keys.include?(name)
659
+ unless @tag2[name].is_a?(Array)
660
+ keep = @tag2[name]
661
+ @tag2[name] = []
662
+ @tag2[name] << keep
663
+ end
664
+ @tag2[name] << data
665
+ else
666
+ @tag2[name] = data
667
+ end
668
+ end
669
+
670
+ ### reads through @file from current pos until it finds a valid MPEG header
671
+ ### returns the MPEG header as FixNum
672
+ def find_next_frame
673
+ # @file will now be sitting at the best guess for where the MPEG frame is.
674
+ # It should be at byte 0 when there's no id3v2 tag.
675
+ # It should be at the end of the id3v2 tag or the zero padding if there
676
+ # is a id3v2 tag.
677
+ start_pos = @file.pos
678
+ dummyproof = @file.stat.size - @file.pos
679
+ dummyproof.times do |i|
680
+ if @file.getc == 0xff
681
+ head = 0xff000000 + (@file.getc << 16) + (@file.getc << 8) + @file.getc
682
+ if check_head(head)
683
+ return head
684
+ else
685
+ @file.seek(-3, IO::SEEK_CUR)
686
+ end
687
+ end
688
+ end
689
+ raise Mp3InfoError
690
+ end
691
+
692
+ ### checks the given header to see if it is valid
693
+ ### head (fixnum) = 4 byte value to test for MPEG header validity
694
+ ### returns true if valid, false if not
695
+ def check_head(head)
696
+ return false if head & 0xffe00000 != 0xffe00000 # 11 bit MPEG frame sync
697
+ return false if head & 0x00060000 == 0x00060000 # 2 bit layer type
698
+ return false if head & 0x0000f000 == 0x0000f000 # 4 bit bitrate
699
+ return false if head & 0x0000f000 == 0x00000000 # free format bitstream
700
+ return false if head & 0x00000c00 == 0x00000c00 # 2 bit frequency
701
+ return false if head & 0xffff0000 == 0xfffe0000
702
+ true
703
+ end
704
+
705
+ end
706
+
707
+ if $0 == __FILE__
708
+ while filename = ARGV.shift
709
+ begin
710
+ info = Mp3Info.new(filename)
711
+ puts filename
712
+ #puts "MPEG #{info.mpeg_version} Layer #{info.layer} #{info.vbr ? "VBR" : "CBR"} #{info.bitrate} Kbps \
713
+ #{info.channel_mode} #{info.samplerate} Hz length #{info.length} sec."
714
+ puts info
715
+ rescue Mp3InfoError => e
716
+ puts "#{filename}\nERROR: #{e}"
717
+ end
718
+ puts
719
+ end
720
+ end
metadata ADDED
@@ -0,0 +1,37 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.4
3
+ specification_version: 1
4
+ name: ruby-mp3info
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.4"
7
+ date: 2005-05-02
8
+ summary: ruby-mp3info is a pure-ruby library that gives low level informations on mp3 files
9
+ require_paths:
10
+ - lib
11
+ email: moumar@rubyforge.org
12
+ homepage: http://ruby-mp3info.rubyforge.org
13
+ rubyforge_project: ruby-mp3info
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ authors:
28
+ - Guillaume Pierronnet
29
+ files:
30
+ - lib/mp3info.rb
31
+ test_files: []
32
+ rdoc_options: []
33
+ extra_rdoc_files: []
34
+ executables: []
35
+ extensions: []
36
+ requirements: []
37
+ dependencies: []