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.
@@ -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,4 @@
1
+ module Mp3file::ID3v2
2
+ class TextFrame
3
+ end
4
+ 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,3 @@
1
+ module Mp3file
2
+ VERSION = "0.0.2"
3
+ 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