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 +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
|