ruby-mp3info 0.6.12 → 0.6.13

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,11 @@
1
+ === 0.6.13 / 2009-05-26
2
+
3
+ * fixed bad mapping of artist inside id3 2.2
4
+ * adding fusil fuzzer tests
5
+ * Improved support for id3v2.2 to id3v2.3 field mapping
6
+ * each_frame() iterator
7
+ * removed @bitrate & @length computation based on @tag2[TLEN]
8
+
1
9
  === 0.6.12 / 2009-02-23
2
10
 
3
11
  * fixed bug when @tag2["TLEN"] == 0
data/lib/mp3info.rb CHANGED
@@ -18,7 +18,7 @@ end
18
18
 
19
19
  class Mp3Info
20
20
 
21
- VERSION = "0.6.12"
21
+ VERSION = "0.6.13"
22
22
 
23
23
  LAYER = [ nil, 3, 2, 1]
24
24
  BITRATE = {
@@ -82,7 +82,7 @@ class Mp3Info
82
82
  # for id3v2.2
83
83
  TAG_MAPPING_2_2 = {
84
84
  "title" => "TT2",
85
- "artist" => "TP2",
85
+ "artist" => "TP1",
86
86
  "album" => "TAL",
87
87
  "year" => "TYE",
88
88
  "tracknum" => "TRK",
@@ -259,7 +259,7 @@ class Mp3Info
259
259
  5.times do
260
260
  head = find_next_frame()
261
261
  @first_frame_pos = @file.pos - 4
262
- current_frame = get_frames_infos(head)
262
+ current_frame = Mp3Info.get_frames_infos(head)
263
263
  @mpeg_version = current_frame[:mpeg_version]
264
264
  @layer = current_frame[:layer]
265
265
  @header[:error_protection] = head[16] == 0 ? true : false
@@ -267,11 +267,11 @@ class Mp3Info
267
267
  @samplerate = current_frame[:samplerate]
268
268
  @header[:padding] = current_frame[:padding]
269
269
  @header[:private] = head[8] == 0 ? true : false
270
- @channel_mode = CHANNEL_MODE[@channel_num = bits(head, 7,6)]
271
- @header[:mode_extension] = bits(head, 5,4)
270
+ @channel_mode = CHANNEL_MODE[@channel_num = Mp3Info.bits(head, 7,6)]
271
+ @header[:mode_extension] = Mp3Info.bits(head, 5,4)
272
272
  @header[:copyright] = (head[3] == 1 ? true : false)
273
273
  @header[:original] = (head[2] == 1 ? true : false)
274
- @header[:emphasis] = bits(head, 1,0)
274
+ @header[:emphasis] = Mp3Info.bits(head, 1,0)
275
275
  @vbr = false
276
276
  found = true
277
277
  break
@@ -307,50 +307,21 @@ class Mp3Info
307
307
  # for cbr, calculate duration with the given bitrate
308
308
  stream_size = @file.stat.size - (hastag1? ? TAG1_SIZE : 0) - (@tag2.io_position || 0)
309
309
  @length = ((stream_size << 3)/1000.0)/@bitrate
310
- full_scan_occured = false
311
310
  # read the first 100 frames and decide if the mp3 is vbr and needs full scan
312
311
  begin
313
312
  bitrate, length = frame_scan(100)
314
313
  if @bitrate != bitrate
315
314
  @vbr = true
316
315
  @bitrate, @length = frame_scan
317
- full_scan_occured = true
318
316
  end
319
317
  rescue Mp3InfoInternalError
320
318
  end
321
- if (tlen = @tag2["TLEN"]) && !full_scan_occured
322
- # but if another duration is given and it isn't close (within 5%)
323
- # assume the mp3 is vbr and go with the given duration
324
- @length = (tlen.is_a?(Array) ? tlen.last : tlen).to_i/1000
325
- @bitrate = (stream_size / @bitrate) / 1024
326
- end
327
319
  end
328
320
  ensure
329
321
  @file.close
330
322
  end
331
323
  end
332
324
 
333
- def get_frames_infos(head)
334
- # be sure we are in sync
335
- if ((head & 0xffe00000) != 0xffe00000) || # 11 bit MPEG frame sync
336
- ((head & 0x00060000) == 0x00060000) || # 2 bit layer type
337
- ((head & 0x0000f000) == 0x0000f000) || # 4 bit bitrate
338
- ((head & 0x0000f000) == 0x00000000) || # free format bitstream
339
- ((head & 0x00000c00) == 0x00000c00) || # 2 bit frequency
340
- ((head & 0xffff0000) == 0xfffe0000)
341
- raise Mp3InfoInternalError
342
- end
343
- mpeg_version = [2.5, nil, 2, 1][bits(head, 20,19)]
344
-
345
- layer = LAYER[bits(head, 18,17)]
346
- raise Mp3InfoInternalError if layer == nil || mpeg_version == nil
347
- { :layer => layer,
348
- :bitrate => BITRATE[mpeg_version][layer-1][bits(head, 15,12)-1],
349
- :samplerate => SAMPLERATE[mpeg_version][bits(head, 11,10)],
350
- :mpeg_version => mpeg_version,
351
- :padding => (head[9] == 1) }
352
- end
353
-
354
325
  # "block version" of Mp3Info::new()
355
326
  def self.open(*params)
356
327
  m = self.new(*params)
@@ -415,7 +386,12 @@ class Mp3Info
415
386
  end
416
387
  [pos, length]
417
388
  end
418
-
389
+
390
+ # return the length in seconds of one frame
391
+ def frame_length
392
+ SAMPLES_PER_FRAME[@layer][@mpeg_version] / Float(@samplerate)
393
+ end
394
+
419
395
  # Flush pending modifications to tags and close the file
420
396
  def close
421
397
  puts "close" if $DEBUG
@@ -518,9 +494,58 @@ class Mp3Info
518
494
  s
519
495
  end
520
496
 
497
+ # iterates over each mpeg frame over the file, allowing you to
498
+ # write some funny things, like an mpeg lossless cutter, or frame
499
+ # counter, or whatever you like ;) +frame+ is a hash with the following keys:
500
+ # :layer, :bitrate, :samplerate, :mpeg_version, :padding and :size (in bytes)
501
+ def each_frame
502
+ File.open(@filename, 'r') do |file|
503
+ file.seek(@first_frame_pos, File::SEEK_SET)
504
+ loop do
505
+ head = file.read(4).unpack("N").first
506
+ frame = Mp3Info.get_frames_infos(head)
507
+ file.seek(frame[:size] -4, File::SEEK_CUR)
508
+ yield frame
509
+ #puts "frame #{frame_count} len #{frame[:length]} br #{frame[:bitrate]} @file.pos #{@file.pos}"
510
+ break if file.eof?
511
+ end
512
+ end
513
+ end
521
514
 
522
515
  private
523
516
 
517
+ def Mp3Info.get_frames_infos(head)
518
+ # be sure we are in sync
519
+ if ((head & 0xffe00000) != 0xffe00000) || # 11 bit MPEG frame sync
520
+ ((head & 0x00060000) == 0x00060000) || # 2 bit layer type
521
+ ((head & 0x0000f000) == 0x0000f000) || # 4 bit bitrate
522
+ ((head & 0x0000f000) == 0x00000000) || # free format bitstream
523
+ ((head & 0x00000c00) == 0x00000c00) || # 2 bit frequency
524
+ ((head & 0xffff0000) == 0xfffe0000)
525
+ raise Mp3InfoInternalError
526
+ end
527
+ mpeg_version = [2.5, nil, 2, 1][bits(head, 20,19)]
528
+
529
+ layer = LAYER[bits(head, 18,17)]
530
+ raise Mp3InfoInternalError if layer == nil || mpeg_version == nil
531
+
532
+ bitrate = BITRATE[mpeg_version][layer-1][bits(head, 15,12)-1]
533
+ samplerate = SAMPLERATE[mpeg_version][bits(head, 11,10)]
534
+ padding = (head[9] == 1)
535
+ if layer == 1
536
+ size = (12 * bitrate*1000.0 / samplerate + (padding ? 1 : 0))*4
537
+ else # layer 2 and 3
538
+ size = 144 * (bitrate*1000.0 / samplerate) + (padding ? 1 : 0)
539
+ end
540
+ size = size.to_i
541
+ { :layer => layer,
542
+ :bitrate => bitrate,
543
+ :samplerate => samplerate,
544
+ :mpeg_version => mpeg_version,
545
+ :padding => padding,
546
+ :size => size }
547
+ end
548
+
524
549
  ### parses the id3 tags of the currently open @file
525
550
  def parse_tags
526
551
  return if @file.stat.size < TAG1_SIZE # file is too small
@@ -587,7 +612,7 @@ private
587
612
  raise(Mp3InfoError, "end of file reached") if @file.eof?
588
613
  head = 0xff000000 + (data.getbyte(0) << 16) + (data.getbyte(1) << 8) + data.getbyte(2)
589
614
  begin
590
- get_frames_infos(head)
615
+ Mp3Info.get_frames_infos(head)
591
616
  return head
592
617
  rescue Mp3InfoInternalError
593
618
  @file.seek(-3, IO::SEEK_CUR)
@@ -603,34 +628,21 @@ private
603
628
 
604
629
  def frame_scan(frame_limit = nil)
605
630
  frame_count = bitrate_sum = 0
606
- @file.seek(@first_frame_pos, File::SEEK_SET)
607
- current_frame = nil
608
- loop do
609
- head = @file.read(4).unpack("N").first
610
- current_frame = get_frames_infos(head)
611
- if current_frame[:layer] == 1
612
- frame_length = (12 * current_frame[:bitrate]*1000.0 / current_frame[:samplerate] + (current_frame[:padding] ? 1 : 0))*4
613
- else # layer 2 and 3
614
- frame_length = 144 * (current_frame[:bitrate]*1000.0 / current_frame[:samplerate]) + (current_frame[:padding] ? 1 : 0)
615
- end
616
- frame_length = frame_length.to_i
617
- bitrate_sum += current_frame[:bitrate]
631
+ each_frame do |frame|
632
+ bitrate_sum += frame[:bitrate]
618
633
  frame_count += 1
619
- @file.seek(frame_length -4, File::SEEK_CUR)
620
- #puts "frame #{frame_count} len #{frame_length} br #{current_frame[:bitrate]} @file.pos #{@file.pos}"
621
- break if @file.eof?
622
634
  break if frame_limit && (frame_count >= frame_limit)
623
635
  end
624
-
636
+
625
637
  average_bitrate = bitrate_sum/frame_count.to_f
626
- length = (frame_count-1) * SAMPLES_PER_FRAME[@layer][@mpeg_version] / Float(@samplerate)
638
+ length = (frame_count-1) * frame_length
627
639
  [average_bitrate, length]
628
640
  end
629
641
 
630
642
 
631
643
  ### returns the selected bit range (b, a) as a number
632
644
  ### NOTE: b > a if not, returns 0
633
- def bits(number, b, a)
645
+ def self.bits(number, b, a)
634
646
  t = 0
635
647
  b.downto(a) { |i| t += t + number[i] }
636
648
  t
data/lib/mp3info/id3v2.rb CHANGED
@@ -5,7 +5,10 @@ require "mp3info/extension_modules"
5
5
 
6
6
  class ID3v2Error < StandardError ; end
7
7
 
8
- # This class is not intended to be used directly
8
+ # This class can be used to decode id3v2 tags from files, like .mp3 or .ape for example.
9
+ # It works like a hash, where key represents the tag name as 3 or 4 upper case letters
10
+ # (respectively related to 2.2 and 2.3+ tag) and value represented as array or raw value.
11
+ # Written version is always 2.3.
9
12
  class ID3v2 < DelegateClass(Hash)
10
13
 
11
14
  # Major version used when writing tags
@@ -88,6 +91,69 @@ class ID3v2 < DelegateClass(Hash)
88
91
  "WXXX" => "User defined URL link frame"
89
92
  }
90
93
 
94
+ # Translate V2 to V3 tags
95
+ TAG_MAPPING_2_2_to_2_3 = {
96
+ "BUF" => "RBUF",
97
+ "COM" => "COMM",
98
+ "CRA" => "AENC",
99
+ "EQU" => "EQUA",
100
+ "ETC" => "ETCO",
101
+ "GEO" => "GEOB",
102
+ "MCI" => "MCDI",
103
+ "MLL" => "MLLT",
104
+ "PIC" => "APIC",
105
+ "POP" => "POPM",
106
+ "REV" => "RVRB",
107
+ "RVA" => "RVAD",
108
+ "SLT" => "SYLT",
109
+ "STC" => "SYTC",
110
+ "TAL" => "TALB",
111
+ "TBP" => "TBPM",
112
+ "TCM" => "TCOM",
113
+ "TCO" => "TCON",
114
+ "TCR" => "TCOP",
115
+ "TDA" => "TDAT",
116
+ "TDY" => "TDLY",
117
+ "TEN" => "TENC",
118
+ "TFT" => "TFLT",
119
+ "TIM" => "TIME",
120
+ "TKE" => "TKEY",
121
+ "TLA" => "TLAN",
122
+ "TLE" => "TLEN",
123
+ "TMT" => "TMED",
124
+ "TOA" => "TOPE",
125
+ "TOF" => "TOFN",
126
+ "TOL" => "TOLY",
127
+ "TOR" => "TORY",
128
+ "TOT" => "TOAL",
129
+ "TP1" => "TPE1",
130
+ "TP2" => "TPE2",
131
+ "TP3" => "TPE3",
132
+ "TP4" => "TPE4",
133
+ "TPA" => "TPOS",
134
+ "TPB" => "TPUB",
135
+ "TRC" => "TSRC",
136
+ "TRD" => "TRDA",
137
+ "TRK" => "TRCK",
138
+ "TSI" => "TSIZ",
139
+ "TSS" => "TSSE",
140
+ "TT1" => "TIT1",
141
+ "TT2" => "TIT2",
142
+ "TT3" => "TIT3",
143
+ "TXT" => "TEXT",
144
+ "TXX" => "TXXX",
145
+ "TYE" => "TYER",
146
+ "UFI" => "UFID",
147
+ "ULT" => "USLT",
148
+ "WAF" => "WOAF",
149
+ "WAR" => "WOAR",
150
+ "WAS" => "WOAS",
151
+ "WCM" => "WCOM",
152
+ "WCP" => "WCOP",
153
+ "WPB" => "WPB",
154
+ "WXX" => "WXXX",
155
+ }
156
+
91
157
  # See id3v2.4.0-structure document, at section 4.
92
158
  TEXT_ENCODINGS = ["iso-8859-1", "utf-16", "utf-16be", "utf-8"]
93
159
 
@@ -188,20 +254,28 @@ class ID3v2 < DelegateClass(Hash)
188
254
  @hash.each do |k, v|
189
255
  next unless v
190
256
  next if v.respond_to?("empty?") and v.empty?
257
+
258
+ # Automagically translate V2 to V3 tags
259
+ k = TAG_MAPPING_2_2_to_2_3[k] if TAG_MAPPING_2_2_to_2_3.has_key?(k)
260
+
191
261
  # doesn't encode id3v2.2 tags, which have 3 characters
192
262
  next if k.size != 4
193
- data = encode_tag(k, v.to_s, WRITE_VERSION)
194
- #data << "\x00"*2 #End of tag
263
+
264
+ # Output one flag for each array element, or one only if it's not an array
265
+ [v].flatten.each do |value|
266
+ data = encode_tag(k, value.to_s, WRITE_VERSION)
267
+ #data << "\x00"*2 #End of tag
195
268
 
196
- tag << k[0,4] #4 characte max for a tag's key
197
- #tag << to_syncsafe(data.size) #+1 because of the language encoding byte
198
- size = data.size
199
- if RUBY_VERSION >= "1.9.0"
200
- size = data.dup.force_encoding("binary").size
269
+ tag << k[0,4] #4 characte max for a tag's key
270
+ #tag << to_syncsafe(data.size) #+1 because of the language encoding byte
271
+ size = data.size
272
+ if RUBY_VERSION >= "1.9.0"
273
+ size = data.dup.force_encoding("binary").size
274
+ end
275
+ tag << [size].pack("N") #+1 because of the language encoding byte
276
+ tag << "\x00"*2 #flags
277
+ tag << data
201
278
  end
202
- tag << [size].pack("N") #+1 because of the language encoding byte
203
- tag << "\x00"*2 #flags
204
- tag << data
205
279
  end
206
280
 
207
281
  tag_str = "ID3"
@@ -236,6 +310,7 @@ class ID3v2 < DelegateClass(Hash)
236
310
 
237
311
  case name
238
312
  when "COMM"
313
+ puts "encode COMM: enc: #{text_encoding_index}, lang: #{@options[:lang]}, str: #{transcoded_value.dump}" if $DEBUG
239
314
  [ text_encoding_index , @options[:lang], 0, transcoded_value ].pack("ca3ca*")
240
315
  when /^T/
241
316
  text_encoding_index.chr + transcoded_value
@@ -248,7 +323,7 @@ class ID3v2 < DelegateClass(Hash)
248
323
  def decode_tag(name, raw_value)
249
324
  puts("decode_tag(#{name.inspect}, #{raw_value.inspect})") if $DEBUG
250
325
  case name
251
- when "COMM"
326
+ when /^COM/
252
327
  #FIXME improve this
253
328
  encoding, lang, str = raw_value.unpack("ca3a*")
254
329
  out = raw_value.split(0.chr).last
@@ -257,15 +332,15 @@ class ID3v2 < DelegateClass(Hash)
257
332
  out = raw_value[1..-1]
258
333
  # we need to convert the string in order to match
259
334
  # the requested encoding
260
- if out && encoding != @text_encoding_index
335
+ if encoding && TEXT_ENCODINGS[encoding] && out && encoding != @text_encoding_index
261
336
  begin
262
- Iconv.iconv(@options[:encoding], TEXT_ENCODINGS[encoding], out).first
337
+ out = Iconv.iconv(@options[:encoding], TEXT_ENCODINGS[encoding], out).first
263
338
  rescue Iconv::Failure
264
- return out
265
339
  end
266
- else
267
- return out
268
340
  end
341
+ # remove padding zeros for textual tags
342
+ out.sub!(/\0*$/, '')
343
+ return out
269
344
  else
270
345
  return raw_value
271
346
  end
@@ -276,7 +351,7 @@ class ID3v2 < DelegateClass(Hash)
276
351
  def read_id3v2_3_frames
277
352
  loop do # there are 2 ways to end the loop
278
353
  name = @io.read(4)
279
- if name.getbyte(0) == 0 or name == "MP3e" #bug caused by old tagging application "mp3ext" ( http://www.mutschler.de/mp3ext/ )
354
+ if name.nil? || name.getbyte(0) == 0 || name == "MP3e" #bug caused by old tagging application "mp3ext" ( http://www.mutschler.de/mp3ext/ )
280
355
  @io.seek(-4, IO::SEEK_CUR) # 1. find a padding zero,
281
356
  seek_to_v2_end
282
357
  break
@@ -299,7 +374,7 @@ class ID3v2 < DelegateClass(Hash)
299
374
  def read_id3v2_2_frames
300
375
  loop do
301
376
  name = @io.read(3)
302
- if name.getbyte(0) == 0
377
+ if name.nil? || name.getbyte(0) == 0
303
378
  @io.seek(-3, IO::SEEK_CUR)
304
379
  seek_to_v2_end
305
380
  break
@@ -323,10 +398,6 @@ class ID3v2 < DelegateClass(Hash)
323
398
 
324
399
  data_io = @io.read(size)
325
400
  data = decode_tag(name, data_io)
326
- # remove padding zeros for textual tags
327
- if data && name =~ /^T/
328
- data.sub!(/\0*$/, '')
329
- end
330
401
 
331
402
  if self["TPOS"] =~ /(\d+)\s*\/\s*(\d+)/
332
403
  self["disc_number"] = $1.to_i
@@ -348,7 +419,7 @@ class ID3v2 < DelegateClass(Hash)
348
419
  ### frame, which should be first 0xff byte after id3v2 padding zero's
349
420
  def seek_to_v2_end
350
421
  until @io.getbyte == 0xff
351
- raise EOFError if @io.eof?
422
+ raise ID3v2Error, "got EOF before finding id3v2 end" if @io.eof?
352
423
  end
353
424
  @io.seek(-1, IO::SEEK_CUR)
354
425
  end
@@ -357,11 +428,5 @@ class ID3v2 < DelegateClass(Hash)
357
428
  def to_syncsafe(num)
358
429
  ( (num<<3) & 0x7f000000 ) + ( (num<<2) & 0x7f0000 ) + ( (num<<1) & 0x7f00 ) + ( num & 0x7f )
359
430
  end
360
-
361
- # def method_missing(meth, *args)
362
- # m = meth.id2name
363
- # return nil if TAGS.has_key?(m) and self[m].nil?
364
- # super
365
- # end
366
431
  end
367
432
 
@@ -302,9 +302,11 @@ class Mp3InfoTest < Test::Unit::TestCase
302
302
  expected_tag = {
303
303
  "genre_s" => "Hip Hop/Rap",
304
304
  "title" => "Intro",
305
- "comments" => "\000engiTunPGAP\0000\000\000",
305
+ #"comments" => "\000engiTunPGAP\0000\000\000",
306
+ "comments" => "0",
306
307
  "year" => 2006,
307
308
  "album" => "Air Max",
309
+ "artist" => "Grems Aka Supermicro",
308
310
  "tracknum" => 1 }
309
311
  # test universal tag
310
312
  assert_equal expected_tag, mp3.tag
@@ -318,13 +320,13 @@ class Mp3InfoTest < Test::Unit::TestCase
318
320
  mp3.tag.comments = "comments"
319
321
  mp3.flush
320
322
  expected_tag = {
321
- "artist"=>"toto",
322
- "genre_s"=>"Hip Hop/Rap",
323
- "title"=>"Intro",
324
- "comments"=>"comments",
325
- "year"=>2006,
326
- "album"=>"Air Max",
327
- "tracknum"=>1}
323
+ "artist" => "toto",
324
+ "genre_s" => "Hip Hop/Rap",
325
+ "title" => "Intro",
326
+ "comments" => "comments",
327
+ "year" => 2006,
328
+ "album" => "Air Max",
329
+ "tracknum" => 1}
328
330
 
329
331
  assert_equal expected_tag, mp3.tag
330
332
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mp3info
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.12
4
+ version: 0.6.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Pierronnet
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-23 00:00:00 +01:00
12
+ date: 2009-05-26 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -20,7 +20,7 @@ dependencies:
20
20
  requirements:
21
21
  - - ">="
22
22
  - !ruby/object:Gem::Version
23
- version: 1.8.3
23
+ version: 1.9.0
24
24
  version:
25
25
  description: "* written in pure ruby * read low-level informations like bitrate, length, samplerate, etc... * read, write, remove id3v1 and id3v2 tags * correctly read VBR files (with or without Xing header) * only 2.3 version is supported for writings id3v2 tags == SYNOPSIS: a good exercise is to read the test.rb to understand how the library works deeper require \"mp3info\" # read and display infos & tags Mp3Info.open(\"myfile.mp3\") do |mp3info| puts mp3info end # read/write tag1 and tag2 with Mp3Info#tag attribute # when reading tag2 have priority over tag1 # when writing, each tag is written. Mp3Info.open(\"myfile.mp3\") do |mp3| puts mp3.tag.title puts mp3.tag.artist puts mp3.tag.album puts mp3.tag.tracknum mp3.tag.title = \"track title\" mp3.tag.artist = \"artist name\" end Mp3Info.open(\"myfile.mp3\") do |mp3| # you can access four letter v2 tags like this puts mp3.tag2.TIT2 mp3.tag2.TIT2 = \"new TIT2\" # or like that mp3.tag2[\"TIT2\"] # at this time, only COMM tag is processed after reading and before writing # according to ID3v2#options hash mp3.tag2.options[:lang] = \"FRE\" mp3.tag2.COMM = \"my comment in french, correctly handled when reading and writing\" end"
26
26
  email: moumar@rubyforge.org