ruby-mp3info 0.6.12 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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