ruby-mp3info 0.6.7 → 0.6.8

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,8 @@
1
+ === 0.6.8 / 2008-08-20
2
+
3
+ * support for MPEG 2.5 (thanks to Oleguer Huguet Ibars)
4
+ * support for vbr files without Xing header
5
+
1
6
  === 0.6.7 / 2008-06-26
2
7
 
3
8
  * Mp3Info#header hash now gives access to additional mpeg attributes (thanks to Andrew Kuklewicz)
data/README.txt CHANGED
@@ -14,6 +14,7 @@ mp3 files.
14
14
  * written in pure ruby
15
15
  * read low-level informations like bitrate, length, samplerate, etc...
16
16
  * read, write, remove id3v1 and id3v2 tags
17
+ * correctly read VBR files (with or without Xing header)
17
18
  * only 2.3 version is supported for writings id3v2 tags
18
19
 
19
20
  == SYNOPSIS:
data/lib/mp3info.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # coding:utf-8
2
- # $Id: mp3info.rb 90 2008-06-26 09:49:38Z moumar $
2
+ # $Id: mp3info.rb 94 2008-08-19 13:34:18Z moumar $
3
3
  # License:: Ruby
4
4
  # Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
5
5
  # Website:: http://ruby-mp3info.rubyforge.org/
@@ -18,24 +18,33 @@ end
18
18
 
19
19
  class Mp3Info
20
20
 
21
- VERSION = "0.6.7"
21
+ VERSION = "0.6.8"
22
22
 
23
23
  LAYER = [ nil, 3, 2, 1]
24
- BITRATE = [
24
+ BITRATE = {
25
+ 1 =>
25
26
  [
26
27
  [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
27
28
  [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
28
29
  [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] ],
30
+ 2 =>
31
+ [
32
+ [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
33
+ [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
34
+ [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
35
+ ],
36
+ 2.5 =>
29
37
  [
30
38
  [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
31
39
  [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
32
40
  [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
33
41
  ]
34
- ]
35
- SAMPLERATE = [
36
- [ 44100, 48000, 32000 ],
37
- [ 22050, 24000, 16000 ]
38
- ]
42
+ }
43
+ SAMPLERATE = {
44
+ 1 => [ 44100, 48000, 32000 ],
45
+ 2 => [ 22050, 24000, 16000 ],
46
+ 2.5 => [ 11025, 12000, 8000 ]
47
+ }
39
48
  CHANNEL_MODE = [ "Stereo", "JStereo", "Dual Channel", "Single Channel"]
40
49
 
41
50
  GENRES = [
@@ -94,9 +103,10 @@ class Mp3Info
94
103
 
95
104
  # http://www.codeproject.com/audio/MPEGAudioInfo.asp
96
105
  SAMPLES_PER_FRAME = [
97
- [384, 384, 384], # Layer I
98
- [1152, 1152, 1152], # Layer II
99
- [1152, 576, 576] # Layer III
106
+ nil,
107
+ {1=>384, 2=>384, 2.5=>384}, # Layer I
108
+ {1=>1152, 2=>1152, 2.5=>1152}, # Layer II
109
+ {1=>1152, 2=>576, 2.5=>576} # Layer III
100
110
  ]
101
111
 
102
112
  # mpeg version = 1 or 2
@@ -117,9 +127,6 @@ class Mp3Info
117
127
  # variable bitrate => true or false
118
128
  attr_reader(:vbr)
119
129
 
120
- # only used in vbr mode
121
- attr_reader(:samples_per_frame)
122
-
123
130
  # Hash representing values in the MP3 frame header. Keys are one of the following:
124
131
  # - :private (boolean)
125
132
  # - :copyright (boolean)
@@ -244,15 +251,17 @@ class Mp3Info
244
251
 
245
252
  found = false
246
253
 
254
+ head = nil
247
255
  5.times do
248
256
  head = find_next_frame()
249
- @mpeg_version = [2, 1][head[19]]
250
- @layer = LAYER[bits(head, 18,17)]
251
- next if @layer.nil?
257
+ @first_frame_pos = @file.pos - 4
258
+ current_frame = get_frames_infos(head)
259
+ @mpeg_version = current_frame[:mpeg_version]
260
+ @layer = current_frame[:layer]
252
261
  @header[:error_protection] = head[16] == 0 ? true : false
253
- @bitrate = BITRATE[@mpeg_version-1][@layer-1][bits(head, 15,12)-1]
254
- @samplerate = SAMPLERATE[@mpeg_version-1][bits(head, 11,10)]
255
- @header[:padding] = (head[9] == 1 ? true : false)
262
+ @bitrate = current_frame[:bitrate]
263
+ @samplerate = current_frame[:samplerate]
264
+ @header[:padding] = current_frame[:padding]
256
265
  @header[:private] = head[8] == 0 ? true : false
257
266
  @channel_mode = CHANNEL_MODE[@channel_num = bits(head, 7,6)]
258
267
  @header[:mode_extension] = bits(head, 5,4)
@@ -261,12 +270,11 @@ class Mp3Info
261
270
  @header[:emphasis] = bits(head, 1,0)
262
271
  @vbr = false
263
272
  found = true
264
- break
273
+ break
265
274
  end
266
275
 
267
276
  raise(Mp3InfoError, "Cannot find good frame") unless found
268
277
 
269
-
270
278
  seek = @mpeg_version == 1 ?
271
279
  (@channel_num == 3 ? 17 : 32) :
272
280
  (@channel_num == 3 ? 9 : 17)
@@ -277,24 +285,33 @@ class Mp3Info
277
285
  if vbr_head == "Xing"
278
286
  puts "Xing header (VBR) detected" if $DEBUG
279
287
  flags = @file.get32bits
280
- @streamsize = @frames = 0
281
- flags[1] == 1 and @frames = @file.get32bits
282
- flags[2] == 1 and @streamsize = @file.get32bits
283
- puts "#{@frames} frames" if $DEBUG
284
- raise(Mp3InfoError, "bad VBR header") if @frames.zero?
288
+ stream_size = frame_count = 0
289
+ flags[1] == 1 and frame_count = @file.get32bits
290
+ flags[2] == 1 and stream_size = @file.get32bits
291
+ puts "#{frame_count} frames" if $DEBUG
292
+ raise(Mp3InfoError, "bad VBR header") if frame_count.zero?
285
293
  # currently this just skips the TOC entries if they're found
286
294
  @file.seek(100, IO::SEEK_CUR) if flags[0] == 1
287
- @vbr_quality = @file.get32bits if flags[3] == 1
295
+ #@vbr_quality = @file.get32bits if flags[3] == 1
288
296
 
289
- @samples_per_frame = SAMPLES_PER_FRAME[@layer-1][@mpeg_version-1]
290
- @length = @frames * @samples_per_frame / Float(@samplerate)
297
+ samples_per_frame = SAMPLES_PER_FRAME[@layer][@mpeg_version]
298
+ @length = frame_count * samples_per_frame / Float(@samplerate)
291
299
 
292
- @bitrate = (((@streamsize/@frames)*@samplerate)/144) >> 10
300
+ @bitrate = (((stream_size/frame_count)*@samplerate)/144) >> 10
293
301
  @vbr = true
294
302
  else
295
303
  # for cbr, calculate duration with the given bitrate
296
- @streamsize = @file.stat.size - (hastag1? ? TAG1_SIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
297
- @length = ((@streamsize << 3)/1000.0)/@bitrate
304
+ stream_size = @file.stat.size - (hastag1? ? TAG1_SIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
305
+ @length = ((stream_size << 3)/1000.0)/@bitrate
306
+ # read the first 100 frames and decide if the mp3 is vbr and needs full scan
307
+ begin
308
+ bitrate, length = frame_scan(100)
309
+ if @bitrate != bitrate
310
+ @vbr = true
311
+ @bitrate, @length = frame_scan
312
+ end
313
+ rescue Mp3InfoInternalError
314
+ end
298
315
  if @tag2["TLEN"]
299
316
  # but if another duration is given and it isn't close (within 5%)
300
317
  # assume the mp3 is vbr and go with the given duration
@@ -305,7 +322,7 @@ class Mp3Info
305
322
  # every single frame
306
323
  @vbr = true
307
324
  @length = @tag2["TLEN"].to_i/1000
308
- @bitrate = (@streamsize / @bitrate) >> 10
325
+ @bitrate = (stream_size / @bitrate) >> 10
309
326
  end
310
327
  end
311
328
  end
@@ -314,6 +331,25 @@ class Mp3Info
314
331
  end
315
332
  end
316
333
 
334
+ def get_frames_infos(head)
335
+ # be sure we are in sync
336
+ if (head & 0xffe00000 != 0xffe00000) || # 11 bit MPEG frame sync
337
+ (head & 0x00060000 == 0x00060000) || # 2 bit layer type
338
+ (head & 0x0000f000 == 0x0000f000) || # 4 bit bitrate
339
+ (head & 0x0000f000 == 0x00000000) || # free format bitstream
340
+ (head & 0x00000c00 == 0x00000c00) || # 2 bit frequency
341
+ (head & 0xffff0000 == 0xfffe0000)
342
+ raise Mp3InfoInternalError
343
+ end
344
+ mpeg_version = [2.5, nil, 2, 1][bits(head, 20,19)]
345
+ layer = LAYER[bits(head, 18,17)]
346
+ { :layer => layer,
347
+ :bitrate => BITRATE[mpeg_version][layer-1][bits(head, 15,12)-1],
348
+ :samplerate => SAMPLERATE[mpeg_version][bits(head, 11,10)],
349
+ :mpeg_version => mpeg_version,
350
+ :padding => (head[9] == 1) }
351
+ end
352
+
317
353
  # "block version" of Mp3Info::new()
318
354
  def self.open(*params)
319
355
  m = self.new(*params)
@@ -539,7 +575,6 @@ private
539
575
  # It should be at byte 0 when there's no id3v2 tag.
540
576
  # It should be at the end of the id3v2 tag or the zero padding if there
541
577
  # is a id3v2 tag.
542
-
543
578
  #dummyproof = @file.stat.size - @file.pos => WAS TOO MUCH
544
579
  dummyproof = [ @file.stat.size - @file.pos, 2000000 ].min
545
580
  dummyproof.times do |i|
@@ -547,29 +582,48 @@ private
547
582
  data = @file.read(3)
548
583
  raise(Mp3InfoError, "end of file reached") if @file.eof?
549
584
  head = 0xff000000 + (data.getbyte(0) << 16) + (data.getbyte(1) << 8) + data.getbyte(2)
550
- if check_head(head)
551
- return head
552
- else
553
- @file.seek(-3, IO::SEEK_CUR)
585
+ begin
586
+ get_frames_infos(head)
587
+ return head
588
+ rescue Mp3InfoInternalError
589
+ @file.seek(-3, IO::SEEK_CUR)
554
590
  end
555
591
  end
556
592
  end
557
- raise Mp3InfoError, "cannot find a valid frame after reading #{dummyproof} bytes"
593
+ if @file.eof?
594
+ raise Mp3InfoError, "cannot find a valid frame: got EOF"
595
+ else
596
+ raise Mp3InfoError, "cannot find a valid frame after reading #{dummyproof} bytes"
597
+ end
558
598
  end
559
599
 
560
- ### checks the given header to see if it is valid
561
- ### head (fixnum) = 4 byte value to test for MPEG header validity
562
- ### returns true if valid, false if not
563
- def check_head(head)
564
- return false if head & 0xffe00000 != 0xffe00000 # 11 bit MPEG frame sync
565
- return false if head & 0x00060000 == 0x00060000 # 2 bit layer type
566
- return false if head & 0x0000f000 == 0x0000f000 # 4 bit bitrate
567
- return false if head & 0x0000f000 == 0x00000000 # free format bitstream
568
- return false if head & 0x00000c00 == 0x00000c00 # 2 bit frequency
569
- return false if head & 0xffff0000 == 0xfffe0000
570
- true
600
+ def frame_scan(frame_limit = nil)
601
+ frame_count = bitrate_sum = 0
602
+ @file.seek(@first_frame_pos, File::SEEK_SET)
603
+ current_frame = nil
604
+ loop do
605
+ head = @file.read(4).unpack("N").first
606
+ current_frame = get_frames_infos(head)
607
+ if current_frame[:layer] == 1
608
+ frame_length = (12 * current_frame[:bitrate]*1000.0 / current_frame[:samplerate] + (current_frame[:padding] ? 1 : 0))*4
609
+ else # layer 2 and 3
610
+ frame_length = 144 * (current_frame[:bitrate]*1000.0 / current_frame[:samplerate]) + (current_frame[:padding] ? 1 : 0)
611
+ end
612
+ frame_length = frame_length.to_i
613
+ bitrate_sum += current_frame[:bitrate]
614
+ frame_count += 1
615
+ @file.seek(frame_length -4, File::SEEK_CUR)
616
+ #puts "frame #{frame_count} len #{frame_length} br #{current_frame[:bitrate]} @file.pos #{@file.pos}"
617
+ break if @file.eof?
618
+ break if frame_limit && (frame_count >= frame_limit)
619
+ end
620
+
621
+ average_bitrate = bitrate_sum/frame_count.to_f
622
+ length = (frame_count-1) * SAMPLES_PER_FRAME[@layer][@mpeg_version] / Float(@samplerate)
623
+ [average_bitrate, length]
571
624
  end
572
625
 
626
+
573
627
  ### returns the selected bit range (b, a) as a number
574
628
  ### NOTE: b > a if not, returns 0
575
629
  def bits(number, b, a)
data/lib/mp3info/id3v2.rb CHANGED
@@ -225,7 +225,13 @@ class ID3v2 < DelegateClass(Hash)
225
225
  # in id3v2.3 tags, there is only 2 encodings possible
226
226
  transcoded_value = value
227
227
  if text_encoding_index >= 2
228
- transcoded_value = Iconv.iconv(TEXT_ENCODINGS[1], TEXT_ENCODINGS[text_encoding_index], value).first
228
+ begin
229
+ transcoded_value = Iconv.iconv(TEXT_ENCODINGS[1],
230
+ TEXT_ENCODINGS[text_encoding_index],
231
+ value).first
232
+ rescue Iconv::Failure
233
+ transcoded_value = value
234
+ end
229
235
  text_encoding_index = 1
230
236
  end
231
237
  end
@@ -109,7 +109,6 @@ class Mp3InfoTest < Test::Unit::TestCase
109
109
 
110
110
  Mp3Info.open(TEMP_FILE) do |info|
111
111
  assert(info.vbr)
112
- assert_equal(1152, info.samples_per_frame)
113
112
  assert_in_delta(174.210612, info.length, 0.000001)
114
113
  end
115
114
  end
@@ -405,6 +404,7 @@ class Mp3InfoTest < Test::Unit::TestCase
405
404
  end
406
405
  end
407
406
 
407
+ =begin
408
408
  def test_should_raises_exception_when_writing_badly_encoded_frames
409
409
  assert_raises(Iconv::Failure) do
410
410
  Mp3Info.open(TEMP_FILE, :encoding => 'utf-8') do |mp3|
@@ -412,6 +412,7 @@ class Mp3InfoTest < Test::Unit::TestCase
412
412
  end
413
413
  end
414
414
  end
415
+ =end
415
416
 
416
417
  def test_audio_content
417
418
  require "digest/md5"
@@ -438,6 +439,19 @@ class Mp3InfoTest < Test::Unit::TestCase
438
439
  end
439
440
  end
440
441
 
442
+ def test_headerless_vbr_file
443
+ mp3_length = 3
444
+ # this will generate a 15 sec mp3 file (44100hz*16bit*2channels) = 60/4 = 15
445
+ system("dd if=/dev/urandom bs=44100 count=#{mp3_length*4} 2>/dev/null | \
446
+ lame -v -m s --vbr-new --preset 128 -r -s 44.1 --bitwidth 16 - - > #{TEMP_FILE} 2>/dev/null")
447
+
448
+ Mp3Info.open(TEMP_FILE) do |mp3|
449
+ assert mp3.vbr
450
+ assert_in_delta(mp3_length, mp3.length, 0.1)
451
+ assert_in_delta(128, mp3.bitrate, 8)
452
+ end
453
+ end
454
+
441
455
  def compute_audio_content_mp3_digest(mp3)
442
456
  pos, size = mp3.audio_content
443
457
  data = File.open(mp3.filename) do |f|
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.7
4
+ version: 0.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Pierronnet
@@ -9,19 +9,20 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-06-26 00:00:00 +02:00
12
+ date: 2008-08-20 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: hoe
17
+ type: :development
17
18
  version_requirement:
18
19
  version_requirements: !ruby/object:Gem::Requirement
19
20
  requirements:
20
21
  - - ">="
21
22
  - !ruby/object:Gem::Version
22
- version: 1.6.0
23
+ version: 1.7.0
23
24
  version:
24
- description: "* written in pure ruby * read low-level informations like bitrate, length, samplerate, etc... * read, write, remove id3v1 and id3v2 tags * 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"
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"
25
26
  email: moumar@rubyforge.org
26
27
  executables: []
27
28
 
@@ -64,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
65
  requirements: []
65
66
 
66
67
  rubyforge_project: ruby-mp3info
67
- rubygems_version: 1.1.1
68
+ rubygems_version: 1.2.0
68
69
  signing_key:
69
70
  specification_version: 2
70
71
  summary: ruby-mp3info is a pure-ruby library to retrieve low level informations on mp3 files and manipulate id3v1 and id3v2 tags