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 +5 -0
- data/README.txt +1 -0
- data/lib/mp3info.rb +105 -51
- data/lib/mp3info/id3v2.rb +7 -1
- data/test/test_ruby-mp3info.rb +15 -1
- metadata +6 -5
data/History.txt
CHANGED
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
|
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.
|
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
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
250
|
-
|
251
|
-
|
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 =
|
254
|
-
@samplerate =
|
255
|
-
@header[:padding] =
|
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
|
-
|
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
|
-
|
281
|
-
flags[1] == 1 and
|
282
|
-
flags[2] == 1 and
|
283
|
-
puts "#{
|
284
|
-
raise(Mp3InfoError, "bad VBR header") if
|
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
|
-
|
295
|
+
#@vbr_quality = @file.get32bits if flags[3] == 1
|
288
296
|
|
289
|
-
|
290
|
-
@length =
|
297
|
+
samples_per_frame = SAMPLES_PER_FRAME[@layer][@mpeg_version]
|
298
|
+
@length = frame_count * samples_per_frame / Float(@samplerate)
|
291
299
|
|
292
|
-
@bitrate = (((
|
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
|
-
|
297
|
-
@length = ((
|
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 = (
|
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
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
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
|
-
|
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
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
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
|
-
|
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
|
data/test/test_ruby-mp3info.rb
CHANGED
@@ -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.
|
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-
|
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.
|
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.
|
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
|