ruby-mp3info 0.6.7 → 0.6.8

Sign up to get free protection for your applications and to get access to all the features.
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