mp3file 0.0.2
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/.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
|