mp3file 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +29 -0
- data/Rakefile +25 -0
- data/doc/id3v2.2.0.txt +1658 -0
- data/doc/id3v2.3.0.txt +2024 -0
- data/doc/id3v2.4.0-frames.txt +1732 -0
- data/doc/id3v2.4.0-structure.txt +731 -0
- data/lib/mp3file/id3v1_tag.rb +58 -0
- data/lib/mp3file/id3v2/bit_padded_int.rb +24 -0
- data/lib/mp3file/id3v2/frame_header.rb +84 -0
- data/lib/mp3file/id3v2/header.rb +67 -0
- data/lib/mp3file/id3v2/tag.rb +25 -0
- data/lib/mp3file/id3v2/text_frame.rb +4 -0
- data/lib/mp3file/id3v2/version.rb +40 -0
- data/lib/mp3file/id3v2.rb +12 -0
- data/lib/mp3file/mp3_file.rb +182 -0
- data/lib/mp3file/mp3_header.rb +109 -0
- data/lib/mp3file/version.rb +3 -0
- data/lib/mp3file/xing_header.rb +39 -0
- data/lib/mp3file.rb +12 -0
- data/mp3file.gemspec +26 -0
- data/spec/common_helpers.rb +12 -0
- data/spec/files/bret_96.mp3 +0 -0
- data/spec/files/bret_id3v1.mp3 +0 -0
- data/spec/files/bret_id3v2.mp3 +0 -0
- data/spec/files/bret_vbr_6.mp3 +0 -0
- data/spec/mp3file/id3v1_tag_spec.rb +62 -0
- data/spec/mp3file/id3v2/bit_padded_int_spec.rb +59 -0
- data/spec/mp3file/id3v2/frame_header_spec.rb +79 -0
- data/spec/mp3file/id3v2/header_spec.rb +137 -0
- data/spec/mp3file/id3v2/tag_spec.rb +22 -0
- data/spec/mp3file/id3v2/version_spec.rb +56 -0
- data/spec/mp3file/mp3_file_spec.rb +90 -0
- data/spec/mp3file/mp3_header_spec.rb +336 -0
- data/spec/mp3file/xing_header_spec.rb +75 -0
- metadata +117 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module Mp3file
|
2
|
+
class InvalidID3v1TagError < Mp3fileError; end
|
3
|
+
|
4
|
+
class ID3v1Tag
|
5
|
+
attr_accessor(:title, :artist, :album, :year, :comment, :track, :genre)
|
6
|
+
|
7
|
+
class ID3v1TagFormat < BinData::Record
|
8
|
+
string(:tag_id, :length => 3, :check_value => lambda { value == 'TAG' })
|
9
|
+
string(:title, :length => 30)
|
10
|
+
string(:artist, :length => 30)
|
11
|
+
string(:album, :length => 30)
|
12
|
+
string(:year, :length => 4)
|
13
|
+
string(:comment, :length => 30)
|
14
|
+
uint8(:genre_id)
|
15
|
+
end
|
16
|
+
|
17
|
+
# First group is the original spec, the second are Winamp extensions.
|
18
|
+
GENRES =
|
19
|
+
%w{ Blues Classic\ Rock Country Dance Disco Funk Grunge Hip-Hop Jazz
|
20
|
+
Metal New\ Age Oldies Other Pop R&B Rap Reggae Rock Techno Industrial
|
21
|
+
Alternative Ska Death\ Metal Pranks Soundtrack Euro-Techno Ambient Trip-Hop
|
22
|
+
Vocal Jazz+Funk Fusion Trance Classical Instrumental Acid House Game
|
23
|
+
Sound\ Clip Gospel Noise AlternRock Bass Soul Punk Space Meditative
|
24
|
+
Instrumental\ Pop Instrumental\ Rock Ethnic Gothic Darkwave Techno-Industrial
|
25
|
+
Electronic Pop-Folk Eurodance Dream Southern\ Rock Comedy Cult Gangsta
|
26
|
+
Top\ 40 Christian\ Rap Pop/Funk Jungle Native\ American Cabaret New\ Wave
|
27
|
+
Psychadelic Rave Showtunes Trailer Lo-Fi Tribal Acid\ Punk Acid\ Jazz Polka
|
28
|
+
Retro Musical Rock\ &\ Roll Hard\ Rock
|
29
|
+
|
30
|
+
Folk Folk-Rock National\ Folk Swing
|
31
|
+
Fast\ Fusion Bebob Latin Revival Celtic Bluegrass Avantgarde Gothic\ Rock
|
32
|
+
Progressive\ Rock Psychedelic\ Rock Symphonic\ Rock Slow\ Rock Big\ Band
|
33
|
+
Chorus Easy\ Listening Acoustic Humour Speech Chanson Opera Chamber\ Music
|
34
|
+
Sonata Symphony Booty\ Bass Primus Porn\ Groove Satire Slow\ Jam Club Tango
|
35
|
+
Samba Folklore Ballad Power\ Ballad Rhythmic\ Soul Freestyle Duet Punk\ Rock
|
36
|
+
Drum\ Solo A\ capella Euro-House Dance\ Hall }
|
37
|
+
|
38
|
+
def initialize(io)
|
39
|
+
@tag = nil
|
40
|
+
begin
|
41
|
+
@tag = ID3v1TagFormat.read(io)
|
42
|
+
rescue BinData::ValidityError => ve
|
43
|
+
raise InvalidID3v1TagError, ve.message
|
44
|
+
end
|
45
|
+
|
46
|
+
@title = @tag.title.split("\x00").first
|
47
|
+
@artist = @tag.artist.split("\x00").first
|
48
|
+
@album = @tag.album.split("\x00").first
|
49
|
+
@year = @tag.year
|
50
|
+
split_comment = @tag.comment.split("\x00").reject { |s| s == '' }
|
51
|
+
@comment = split_comment.first
|
52
|
+
if split_comment.size > 1
|
53
|
+
@track = split_comment.last.bytes.first
|
54
|
+
end
|
55
|
+
@genre = GENRES[@tag.genre_id]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Mp3file::ID3v2
|
2
|
+
module BitPaddedInt
|
3
|
+
def self.unpad_number(num, bits = 7)
|
4
|
+
field = 2**bits - 1
|
5
|
+
rv = 0
|
6
|
+
0.upto(3) do |i|
|
7
|
+
rv += (num & field) >> (i*(8-bits))
|
8
|
+
field = field << 8
|
9
|
+
end
|
10
|
+
rv
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.pad_number(num, bits = 7)
|
14
|
+
field = 2**bits - 1
|
15
|
+
num2 = num
|
16
|
+
rv = 0
|
17
|
+
0.upto(3) do |i|
|
18
|
+
rv += (num2 & field) << (i*8)
|
19
|
+
num2 = num2 >> bits
|
20
|
+
end
|
21
|
+
rv
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Mp3file::ID3v2
|
2
|
+
class FrameHeader
|
3
|
+
class ID3v220FrameHeaderFormat < BinData::Record
|
4
|
+
string(:frame_id, :length => 3)
|
5
|
+
uint24be(:frame_size)
|
6
|
+
end
|
7
|
+
|
8
|
+
class ID3v230FrameHeaderFormat < BinData::Record
|
9
|
+
string(:frame_id, :length => 4)
|
10
|
+
uint32be(:frame_size)
|
11
|
+
bit1(:tag_alter_preserve)
|
12
|
+
bit1(:file_alter_preserve)
|
13
|
+
bit1(:read_only)
|
14
|
+
bit5(:unused1, :check_value => lambda { value == 0 })
|
15
|
+
bit1(:compression)
|
16
|
+
bit1(:encryption)
|
17
|
+
bit1(:has_group)
|
18
|
+
bit5(:unused2, :check_value => lambda { value == 0 })
|
19
|
+
uint8(:encryption_type, :onlyif => lambda { encryption == 1 })
|
20
|
+
uint8(:group_id, :onlyif => lambda { has_group == 1 })
|
21
|
+
end
|
22
|
+
|
23
|
+
class ID3v240FrameHeaderFormat < BinData::Record
|
24
|
+
string(:frame_id, :length => 4)
|
25
|
+
uint32be(:frame_size)
|
26
|
+
bit1(:unused1, :check_value => lambda { value == 0 })
|
27
|
+
bit1(:tag_alter_preserve)
|
28
|
+
bit1(:file_alter_preserve)
|
29
|
+
bit1(:read_only)
|
30
|
+
bit4(:unused2, :check_value => lambda { value == 0 })
|
31
|
+
bit1(:unused3, :check_value => lambda { value == 0 })
|
32
|
+
bit1(:group)
|
33
|
+
bit2(:unused4, :check_value => lambda { value == 0 })
|
34
|
+
bit1(:compression)
|
35
|
+
bit1(:encryption)
|
36
|
+
bit1(:unsynchronized)
|
37
|
+
bit1(:data_length_indicator)
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader(:frame_id, :size,
|
41
|
+
:preserve_on_altered_tag, :preserve_on_altered_file,
|
42
|
+
:read_only, :compressed, :encrypted, :encryption_type,
|
43
|
+
:group, :unsynchronized, :data_length)
|
44
|
+
|
45
|
+
def initialize(io, tag)
|
46
|
+
@tag = tag
|
47
|
+
header = nil
|
48
|
+
@preserve_on_altered_tag = false
|
49
|
+
@preserve_on_altered_file = false
|
50
|
+
@read_only = false
|
51
|
+
@compressed = false
|
52
|
+
@encrypted = false
|
53
|
+
@group = nil
|
54
|
+
@unsynchronized = false
|
55
|
+
@data_length = 0
|
56
|
+
|
57
|
+
begin
|
58
|
+
if @tag.version >= ID3V2_2_0 && @tag.version < ID3V2_3_0
|
59
|
+
header = ID3v220FrameHeaderFormat.read(io)
|
60
|
+
elsif @tag.version >= ID3V2_3_0 && @tag.version < ID3V2_4_0
|
61
|
+
header = ID3v230FrameHeaderFormat.read(io)
|
62
|
+
@preserve_on_altered_tag = header.tag_alter_preserve == 1
|
63
|
+
@preserve_on_altered_file = header.file_alter_preserve == 1
|
64
|
+
@read_only = header.read_only == 1
|
65
|
+
@compressed = header.compression == 1
|
66
|
+
if header.encryption == 1
|
67
|
+
@encrypted = true
|
68
|
+
@encryption_type = header.encryption_type
|
69
|
+
end
|
70
|
+
if header.has_group == 1
|
71
|
+
@group = header.group_id
|
72
|
+
end
|
73
|
+
elsif @tag.version >= ID3V2_4_0
|
74
|
+
header = ID3v240FrameHeaderFormat.read(io)
|
75
|
+
end
|
76
|
+
rescue BinData::ValidityError => ve
|
77
|
+
raise InvalidID3v2TagError, ve.message
|
78
|
+
end
|
79
|
+
|
80
|
+
@frame_id = header.frame_id
|
81
|
+
@size = BitPaddedInt.unpad_number(header.frame_size)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Mp3file::ID3v2
|
2
|
+
class Header
|
3
|
+
attr_reader(
|
4
|
+
:version,
|
5
|
+
:unsynchronized,
|
6
|
+
:extended_header,
|
7
|
+
:compression,
|
8
|
+
:experimental,
|
9
|
+
:footer,
|
10
|
+
:tag_size)
|
11
|
+
|
12
|
+
class ID3v2HeaderFormat < BinData::Record
|
13
|
+
string(:tag_id, :length => 3, :check_value => lambda { value == 'ID3' })
|
14
|
+
uint8(:vmaj, :check_value => lambda { value >= 2 && value <= 4 })
|
15
|
+
uint8(:vmin)
|
16
|
+
|
17
|
+
bit1(:unsynchronized)
|
18
|
+
bit1(:extended_header)
|
19
|
+
bit1(:experimental)
|
20
|
+
bit1(:footer)
|
21
|
+
bit4(:unused, :check_value => lambda { value == 0 })
|
22
|
+
|
23
|
+
uint32be(:size_padded)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(io)
|
27
|
+
header = nil
|
28
|
+
begin
|
29
|
+
header = ID3v2HeaderFormat.read(io)
|
30
|
+
rescue BinData::ValidityError => ve
|
31
|
+
raise InvalidID3v2TagError, ve.message
|
32
|
+
end
|
33
|
+
|
34
|
+
@version = Version.new(header.vmaj, header.vmin)
|
35
|
+
|
36
|
+
@unsynchronized = false
|
37
|
+
@extended_header = false
|
38
|
+
@compression = false
|
39
|
+
@experimental = false
|
40
|
+
@footer = false
|
41
|
+
|
42
|
+
if @version >= ID3V2_2_0 && @version < ID3V2_3_0
|
43
|
+
@unsynchronized = header.unsynchronized == 1
|
44
|
+
# Bit 6 was redefined in v2.3.0+, and we picked the new name
|
45
|
+
# for it above.
|
46
|
+
@compression = header.extended_header == 1
|
47
|
+
if header.experimental == 1 || header.footer == 1
|
48
|
+
raise InvalidID3v2TagError, "Invalid flag set in ID3v2.2 header"
|
49
|
+
end
|
50
|
+
elsif @version >= ID3V2_3_0 && @version < ID3V2_4_0
|
51
|
+
@unsynchronized = header.unsynchronized == 1
|
52
|
+
@extended_header = header.extended_header == 1
|
53
|
+
@experimental = header.experimental == 1
|
54
|
+
if header.footer == 1
|
55
|
+
raise InvalidID3v2TagError, "Invalid flag set in ID3v2.3 header"
|
56
|
+
end
|
57
|
+
elsif @version >= ID3V2_4_0
|
58
|
+
@unsynchronized = header.unsynchronized == 1
|
59
|
+
@extended_header = header.extended_header == 1
|
60
|
+
@experimental = header.experimental == 1
|
61
|
+
@footer = header.footer == 1
|
62
|
+
end
|
63
|
+
|
64
|
+
@tag_size = BitPaddedInt.unpad_number(header.size_padded)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Mp3file::ID3v2
|
4
|
+
class Tag
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegators(:@header, :version, :unsynchronized, :extended_header,
|
8
|
+
:compression, :experimental, :footer)
|
9
|
+
|
10
|
+
attr_reader(:header, :frames)
|
11
|
+
|
12
|
+
def initialize(io)
|
13
|
+
@header = Header.new(io)
|
14
|
+
@frames = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_frames
|
18
|
+
@frames = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def size
|
22
|
+
@header.tag_size + 10
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Mp3file::ID3v2
|
2
|
+
class Version
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader(:vbig, :vmaj, :vmin)
|
6
|
+
|
7
|
+
def initialize(vmaj, vmin, vbig = 2)
|
8
|
+
@vbig = vbig.to_i
|
9
|
+
@vmaj = vmaj.to_i
|
10
|
+
@vmin = vmin.to_i
|
11
|
+
end
|
12
|
+
|
13
|
+
def <=>(other)
|
14
|
+
c = vbig <=> other.vbig
|
15
|
+
return c if c != 0
|
16
|
+
|
17
|
+
c = vmaj <=> other.vmaj
|
18
|
+
return c if c != 0
|
19
|
+
|
20
|
+
vmin <=> other.vmin
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"ID3v%d.%d.%d" % [ vbig, vmaj, vmin ]
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_byte_string
|
28
|
+
[ vmaj, vmin ].pack("cc")
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"<%p vbig = %p vmaj = %p vmin = %p>" %
|
33
|
+
[ self.class, @vbig, @vmaj, @vmin ]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
ID3V2_4_0 = Version.new(4, 0)
|
38
|
+
ID3V2_3_0 = Version.new(3, 0)
|
39
|
+
ID3V2_2_0 = Version.new(2, 0)
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Mp3file
|
2
|
+
module ID3v2
|
3
|
+
class InvalidID3v2TagError < Mp3fileError; end
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'mp3file/id3v2/bit_padded_int'
|
8
|
+
require 'mp3file/id3v2/version'
|
9
|
+
require 'mp3file/id3v2/header'
|
10
|
+
require 'mp3file/id3v2/tag'
|
11
|
+
require 'mp3file/id3v2/frame_header'
|
12
|
+
require 'mp3file/id3v2/text_frame'
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Mp3file
|
2
|
+
class MP3File
|
3
|
+
attr_reader(:file, :file_size, :audio_size)
|
4
|
+
attr_reader(:first_header_offset, :first_header)
|
5
|
+
attr_reader(:xing_header_offset, :xing_header)
|
6
|
+
attr_reader(:vbri_header_offset, :vbri_header)
|
7
|
+
attr_reader(:mpeg_version, :layer, :bitrate, :samplerate, :mode)
|
8
|
+
attr_reader(:num_frames, :total_samples, :length)
|
9
|
+
|
10
|
+
attr_accessor(:id3v1_tag)
|
11
|
+
|
12
|
+
def initialize(file_path)
|
13
|
+
file_path = Pathname.new(file_path).expand_path if file_path.is_a?(String)
|
14
|
+
load_file(file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def vbr?
|
18
|
+
@vbr
|
19
|
+
end
|
20
|
+
|
21
|
+
def id3v1tag?
|
22
|
+
!@id3v1_tag.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def id3v2tag?
|
26
|
+
!@id3v2_tag.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def title
|
30
|
+
value_from_tags(:title)
|
31
|
+
end
|
32
|
+
|
33
|
+
def artist
|
34
|
+
value_from_tags(:artist)
|
35
|
+
end
|
36
|
+
|
37
|
+
def album
|
38
|
+
value_from_tags(:album)
|
39
|
+
end
|
40
|
+
|
41
|
+
def track
|
42
|
+
value_from_tags(:track)
|
43
|
+
end
|
44
|
+
|
45
|
+
def year
|
46
|
+
value_from_tags(:year)
|
47
|
+
end
|
48
|
+
|
49
|
+
def comment
|
50
|
+
value_from_tags(:comment)
|
51
|
+
end
|
52
|
+
|
53
|
+
def genre
|
54
|
+
value_from_tags(:genre)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def value_from_tags(v1_field)
|
60
|
+
if @id3v1_tag
|
61
|
+
@id3v1_tag.send(v1_field)
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_file(file_path)
|
68
|
+
@file = file_path.open('rb')
|
69
|
+
@file.seek(0, IO::SEEK_END)
|
70
|
+
@file_size = @file.tell
|
71
|
+
|
72
|
+
# Try to read an ID3v1 tag.
|
73
|
+
@id3v1_tag = nil
|
74
|
+
@file.seek(-128, IO::SEEK_END)
|
75
|
+
begin
|
76
|
+
@id3v1_tag = ID3v1Tag.new(@file)
|
77
|
+
rescue InvalidID3v1TagError => e
|
78
|
+
@id3v1_tag = nil
|
79
|
+
end
|
80
|
+
@file.seek(0, IO::SEEK_SET)
|
81
|
+
|
82
|
+
# Try to detect an ID3v2 header.
|
83
|
+
@id3v2_header = nil
|
84
|
+
begin
|
85
|
+
@id3v2_header = ID3v2::Header.new(@file)
|
86
|
+
rescue ID3v2::InvalidID3v2TagError => e
|
87
|
+
@id3v2_header = nil
|
88
|
+
@file.seek(0, IO::SEEK_SET)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Skip past the ID3v2 header if it's present.
|
92
|
+
if @id3v2_header
|
93
|
+
@file.seek(@id3v2_header.tag_size + 10, IO::SEEK_SET)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Try to find the first MP3 header.
|
97
|
+
@first_header_offset, @first_header = get_next_header(@file)
|
98
|
+
|
99
|
+
@mpeg_version = @first_header.version
|
100
|
+
@layer = @first_header.layer
|
101
|
+
@bitrate = @first_header.bitrate / 1000
|
102
|
+
@samplerate = @first_header.samplerate
|
103
|
+
@mode = @first_header.mode
|
104
|
+
@audio_size = @file_size
|
105
|
+
if @id3v1_tag
|
106
|
+
@audio_size -= 128
|
107
|
+
end
|
108
|
+
if @id3v2_header
|
109
|
+
@audio_size -= (@id3v2_header.tag_size + 10)
|
110
|
+
end
|
111
|
+
|
112
|
+
# If it's VBR, there should be an Xing header after the
|
113
|
+
# side_bytes.
|
114
|
+
@xing_header = nil
|
115
|
+
@file.seek(@first_header.side_bytes, IO::SEEK_CUR)
|
116
|
+
begin
|
117
|
+
@xing_header = XingHeader.new(@file)
|
118
|
+
rescue InvalidXingHeaderError => ve
|
119
|
+
@file.seek(@first_header_offset + 4, IO::SEEK_CUR)
|
120
|
+
end
|
121
|
+
|
122
|
+
if @xing_header
|
123
|
+
@vbr = true
|
124
|
+
# Do the VBR length calculation. What to do if we don't have
|
125
|
+
# both of these pieces of information?
|
126
|
+
if @xing_header.frames && @xing_header.bytes
|
127
|
+
@num_frames = @xing_header.frames
|
128
|
+
@total_samples = @xing_header.frames * @first_header.samples
|
129
|
+
@length = total_samples / @samplerate
|
130
|
+
@bitrate = ((@xing_header.bytes.to_f / @length.to_f) * 8 / 1000).to_i
|
131
|
+
end
|
132
|
+
else
|
133
|
+
# Do the CBR length calculation.
|
134
|
+
@vbr = false
|
135
|
+
@num_frames = @audio_size / @first_header.frame_size
|
136
|
+
@total_samples = @num_frames * @first_header.samples
|
137
|
+
@length = @total_samples / @samplerate
|
138
|
+
end
|
139
|
+
|
140
|
+
@file.close
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_next_header(file, offset = nil)
|
144
|
+
if offset && offset != file.tell
|
145
|
+
file.seek(offset, IO::SEEK_SET)
|
146
|
+
end
|
147
|
+
|
148
|
+
header = nil
|
149
|
+
header_offset = file.tell
|
150
|
+
|
151
|
+
while header.nil?
|
152
|
+
begin
|
153
|
+
header = MP3Header.new(file)
|
154
|
+
header_offset = file.tell - 4
|
155
|
+
rescue InvalidMP3HeaderError => e
|
156
|
+
header_offset += 1
|
157
|
+
file.seek(header_offset, IO::SEEK_SET)
|
158
|
+
retry
|
159
|
+
end
|
160
|
+
|
161
|
+
# byte = file.readbyte
|
162
|
+
# while byte != 0xFF
|
163
|
+
# byte = file.readbyte
|
164
|
+
# end
|
165
|
+
# header_bytes = [ byte ] + file.read(3).bytes.to_a
|
166
|
+
# if header_bytes[1] & 0xE0 != 0xE0
|
167
|
+
# file.seek(-3, IO::SEEK_CUR)
|
168
|
+
# else
|
169
|
+
# header = MP3Header.new(header_bytes)
|
170
|
+
# if !header.valid?
|
171
|
+
# header = nil
|
172
|
+
# file.seek(-3, IO::SEEK_CUR)
|
173
|
+
# else
|
174
|
+
# header_offset = file.tell - 4
|
175
|
+
# end
|
176
|
+
# end
|
177
|
+
end
|
178
|
+
|
179
|
+
[ header_offset, header ]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Mp3file
|
2
|
+
class InvalidMP3HeaderError < Mp3fileError; end
|
3
|
+
|
4
|
+
class MP3Header
|
5
|
+
attr_reader(:version, :layer, :has_crc, :bitrate,
|
6
|
+
:samplerate, :has_padding, :mode, :mode_extension,
|
7
|
+
:copyright, :original, :emphasis, :samples, :frame_size,
|
8
|
+
:side_bytes)
|
9
|
+
|
10
|
+
class MP3HeaderFormat < BinData::Record
|
11
|
+
uint8(:sync1, :value => 255, :check_value => lambda { value == 255 })
|
12
|
+
|
13
|
+
bit3(:sync2, :value => 7, :check_value => lambda { value == 7 })
|
14
|
+
bit2(:version, :check_value => lambda { value != 1 })
|
15
|
+
bit2(:layer, :check_value => lambda { value != 0 })
|
16
|
+
bit1(:crc)
|
17
|
+
|
18
|
+
bit4(:bitrate, :check_value => lambda { value != 15 && value != 0 })
|
19
|
+
bit2(:samplerate, :check_value => lambda { value != 3 })
|
20
|
+
bit1(:padding)
|
21
|
+
bit1(:private)
|
22
|
+
|
23
|
+
bit2(:mode)
|
24
|
+
bit2(:mode_extension)
|
25
|
+
bit1(:copyright)
|
26
|
+
bit1(:original)
|
27
|
+
bit2(:emphasis, :check_value => lambda { value != 2 })
|
28
|
+
end
|
29
|
+
|
30
|
+
MPEG_VERSIONS = [ 'MPEG 2.5', nil, 'MPEG 2', 'MPEG 1' ]
|
31
|
+
LAYERS = [ nil, 'Layer III', 'Layer II', 'Layer I' ]
|
32
|
+
BITRATES = [
|
33
|
+
# MPEG 2.5
|
34
|
+
[ nil,
|
35
|
+
[ 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ], # Layer III
|
36
|
+
[ 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ], # Layer II
|
37
|
+
[ 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256 ], ], # Layer I
|
38
|
+
# reserved
|
39
|
+
nil,
|
40
|
+
# MPEG 2
|
41
|
+
[ nil,
|
42
|
+
[ 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ], # Layer III
|
43
|
+
[ 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ], # Layer II
|
44
|
+
[ 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256 ], ], # Layer I
|
45
|
+
# MPEG 1
|
46
|
+
[ nil,
|
47
|
+
[ 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 ], # Layer III
|
48
|
+
[ 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384 ], # Layer II
|
49
|
+
[ 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 ], ] # Layer I
|
50
|
+
]
|
51
|
+
SAMPLERATES = [
|
52
|
+
[ 11025, 12000, 8000 ], # MPEG 2.5
|
53
|
+
nil,
|
54
|
+
[ 22050, 24000, 16000 ], # MPEG 2
|
55
|
+
[ 44100, 48000, 32000 ], # MPEG 1
|
56
|
+
]
|
57
|
+
MODES = [ 'Stereo', 'Joint Stereo', 'Dual Channel', 'Mono' ]
|
58
|
+
SAMPLE_COUNTS = [
|
59
|
+
[ nil, 576, 1152, 384 ], # MPEG 2.5, III / II / I
|
60
|
+
nil,
|
61
|
+
[ nil, 576, 1152, 384 ], # MPEG 2, III / II / I
|
62
|
+
[ nil, 1152, 1152, 384 ], # MPEG 1, III / II / I
|
63
|
+
]
|
64
|
+
SIDE_BYTES = [
|
65
|
+
[ 17, 17, 17, 9 ], # MPEG 2.5, Stereo, J-Stereo, Dual Channel, Mono
|
66
|
+
nil,
|
67
|
+
[ 17, 17, 17, 9 ], # MPEG 2, Stereo, J-Stereo, Dual Channel, Mono
|
68
|
+
[ 32, 32, 32, 17 ], # MPEG 1, Stereo, J-Stereo, Dual Channel, Mono
|
69
|
+
]
|
70
|
+
MODE_EXTENSIONS_LAYER_I_II = [ 'bands 4 to 31', 'bands 8 to 31', 'bands 12 to 31', 'bands 16 to 31' ]
|
71
|
+
MODE_EXTENSIONS_LAYER_III = [ nil, 'Intensity Stereo', 'M/S Stereo', [ 'Intensity Stereo', 'M/S Stereo' ] ]
|
72
|
+
EMPHASES = [ 'none', '50/15 ms', nil, 'CCIT J.17' ]
|
73
|
+
|
74
|
+
def initialize(io)
|
75
|
+
begin
|
76
|
+
head = MP3HeaderFormat.read(io)
|
77
|
+
rescue BinData::ValidityError => ve
|
78
|
+
raise InvalidMP3HeaderError, ve.message
|
79
|
+
end
|
80
|
+
|
81
|
+
@version = MPEG_VERSIONS[head.version]
|
82
|
+
@layer = LAYERS[head.layer]
|
83
|
+
@has_crc = head.crc == 0
|
84
|
+
@bitrate = BITRATES[head.version][head.layer][head.bitrate - 1] * 1000
|
85
|
+
@samplerate = SAMPLERATES[head.version][head.samplerate]
|
86
|
+
@has_padding = head.padding == 1
|
87
|
+
@mode = MODES[head.mode]
|
88
|
+
|
89
|
+
@mode_extension = nil
|
90
|
+
if @mode == 'Joint Stereo'
|
91
|
+
if [ 'Layer I', 'Layer II' ].include?(@layer)
|
92
|
+
@mode_extension = MODE_EXTENSIONS_LAYER_I_II[head.mode_extension]
|
93
|
+
elsif @layer == 'Layer III'
|
94
|
+
@mode_extension = MODE_EXTENSIONS_LAYER_III[head.mode_extension]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
@copyright = head.copyright == 1
|
99
|
+
@original = head.original == 1
|
100
|
+
@emphasis = EMPHASES[head.emphasis]
|
101
|
+
@samples = SAMPLE_COUNTS[head.version][head.layer]
|
102
|
+
|
103
|
+
slot_size = layer == 'Layer I' ? 4 : 1
|
104
|
+
pad_slots = has_padding ? 1 : 0
|
105
|
+
@frame_size = (((samples.to_f * bitrate.to_f) / (8 * slot_size.to_f * samplerate.to_f)) + pad_slots).to_i * slot_size
|
106
|
+
@side_bytes = SIDE_BYTES[head.version][head.mode]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mp3file
|
2
|
+
class InvalidXingHeaderError < Mp3fileError; end
|
3
|
+
|
4
|
+
class XingHeader
|
5
|
+
attr_reader(:frames, :bytes, :toc, :quality)
|
6
|
+
|
7
|
+
class XingHeaderFormat < BinData::Record
|
8
|
+
string(:vbr_id, :length => 4, :check_value => lambda { value == 'Xing' })
|
9
|
+
|
10
|
+
uint8(:unused1, :check_value => lambda { value == 0 })
|
11
|
+
uint8(:unused2, :check_value => lambda { value == 0 })
|
12
|
+
uint8(:unused3, :check_value => lambda { value == 0 })
|
13
|
+
bit4(:unused4, :check_value => lambda { value == 0 })
|
14
|
+
bit1(:quality_present)
|
15
|
+
bit1(:toc_present)
|
16
|
+
bit1(:bytes_present)
|
17
|
+
bit1(:frames_present)
|
18
|
+
|
19
|
+
uint32be(:frames, :onlyif => lambda { frames_present == 1 })
|
20
|
+
uint32be(:bytes, :onlyif => lambda { bytes_present == 1 })
|
21
|
+
array(:toc, :type => :uint8, :read_until => lambda { index == 99 }, :onlyif => lambda { toc_present == 1 })
|
22
|
+
uint32be(:quality, :onlyif => lambda { quality_present == 1 })
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(io)
|
26
|
+
head = nil
|
27
|
+
begin
|
28
|
+
head = XingHeaderFormat.read(io)
|
29
|
+
rescue BinData::ValidityError => ve
|
30
|
+
raise InvalidXingHeaderError, ve.message
|
31
|
+
end
|
32
|
+
|
33
|
+
@frames = head.frames if head.frames_present == 1
|
34
|
+
@bytes = head.bytes if head.bytes_present == 1
|
35
|
+
@toc = head.toc.dup if head.toc_present == 1
|
36
|
+
@quality = head.quality if head.quality_present == 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/mp3file.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bindata'
|
5
|
+
|
6
|
+
class Mp3fileError < StandardError; end
|
7
|
+
|
8
|
+
require 'mp3file/mp3_file'
|
9
|
+
require 'mp3file/mp3_header'
|
10
|
+
require 'mp3file/xing_header'
|
11
|
+
require 'mp3file/id3v1_tag'
|
12
|
+
require 'mp3file/id3v2'
|
data/mp3file.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- mode: ruby; encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$:.push File.expand_path("../lib", __FILE__)
|
4
|
+
require "mp3file/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "mp3file"
|
8
|
+
s.version = Mp3file::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Andrew Watts"]
|
11
|
+
s.email = ["ahwatts@gmail.com"]
|
12
|
+
s.homepage = "http://rubygems.org/gems/mp3file"
|
13
|
+
s.summary = %q{Reads MP3 headers and returns their information.}
|
14
|
+
s.description = %q{Reads MP3 headers and returns their information.}
|
15
|
+
|
16
|
+
s.rubyforge_project = "mp3file"
|
17
|
+
|
18
|
+
s.add_development_dependency('rspec')
|
19
|
+
s.add_development_dependency('rake')
|
20
|
+
s.add_dependency('bindata')
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
24
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
25
|
+
s.require_paths = ["lib"]
|
26
|
+
end
|