mp3info 0.6.17

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 ADDED
@@ -0,0 +1,12 @@
1
+ /pkg/
2
+ /*.gem
3
+
4
+ /doc/
5
+ /rdoc/
6
+ /.yardoc/
7
+ /coverage/
8
+
9
+ Makefile
10
+ *.o
11
+ *.bundle
12
+ /tmp/
data/History.txt ADDED
@@ -0,0 +1,153 @@
1
+ === 0.6.17 / 2012-01-15
2
+
3
+ * fixed stringio related problems
4
+ * cleanup project
5
+
6
+ === 0.6.16 / 2011-11-10
7
+
8
+ * fixed type error when inspecting mp3info (thanks to Jacob Lichner)
9
+
10
+ === 0.6.15 / 2011-07-18
11
+
12
+ * support for StringIO as input (thanks to Edd Parris)
13
+
14
+ === 0.6.14 / 2011-06-17
15
+
16
+ * Added a check for nil that was seen causing problems when processing files. (thanks to Carl Hall)
17
+ * Fixed reading on win32, requires binary flag. (thanks to Jonas Tingeborn)
18
+ * Fixed white spaces. Replaced tabs with spaces to make the source readable on for users other than the original author. (thanks to Jonas Tingeborn)
19
+ * Add :parse_mp3 flag to new/open. (thanks to Dave Lee)
20
+ * Add benchmark for parsing performance. (thanks to Dave Lee)
21
+ * fixed ID3v2#io_position computing, so Mp3Info#audio_content() is correct now
22
+
23
+ === 0.6.13 / 2009-05-26
24
+
25
+ * fixed bad mapping of artist inside id3 2.2
26
+ * adding fusil fuzzer tests
27
+ * Improved support for id3v2.2 to id3v2.3 field mapping
28
+ * each_frame() iterator
29
+ * removed @bitrate & @length computation based on @tag2[TLEN]
30
+
31
+ === 0.6.12 / 2009-02-23
32
+
33
+ * fixed bug when @tag2["TLEN"] == 0
34
+
35
+ === 0.6.11 / 2009-01-27
36
+
37
+ * the library doesn't raise an ID3v2Error anymore when a id3v2 tag size is incorrect, but just output a warning. (Fixes bug #23619)
38
+
39
+ === 0.6.10 / 2008-11-27
40
+
41
+ * processing of tags (read and write) can be disabled with :parse_tags => false
42
+
43
+ === 0.6.9 / 2008-09-16
44
+
45
+ * now correctly remove trailing whitespaces form tag1 values
46
+ * bugfix #21687 included: 'tweaked the MP3 frame synch code to parse certain mp3 files'
47
+
48
+ === 0.6.8 / 2008-08-20
49
+
50
+ * support for MPEG 2.5 (thanks to Oleguer Huguet Ibars)
51
+ * support for vbr files without Xing header
52
+
53
+ === 0.6.7 / 2008-06-26
54
+
55
+ * Mp3Info#header hash now gives access to additional mpeg attributes (thanks to Andrew Kuklewicz)
56
+
57
+ === 0.6.6 / 2008-05-27
58
+
59
+ * avoid reading tag that are too big (> 50Mb)
60
+ * ruby 1.9 support (thanks to Dave Thomas)
61
+ * FIXED: bug #20311 'Multiple APIC frames may be stored incorrectly'
62
+ * FIXED: bug #20312 'doesn't use v2.2 frames for extracting meta data'
63
+
64
+ === 0.6.5 / 2008-04-19
65
+
66
+ * added Mp3Info#audio_content method, to return "audio-only" boundaries from mp3, i.e. data without tags (closes feature request #17230)
67
+ * bugfix on reading id3 2.4 (size not syncsafed)
68
+ * more robust tag decoding with bad tags
69
+
70
+ === 0.6.4 / 2008-04-16
71
+
72
+ * added @tag2["disc_number"] and @tag2["disc_total"] mirroring TPOS attribute (thanks to Harry Ohlsen)
73
+
74
+ === 0.6.3 / 2008-03-28
75
+
76
+ * some internals modifications for the compatibility with ruby-audioinfo
77
+
78
+ === 0.6.2 / 2008-03-02
79
+
80
+ * better handling of frames: decode and encode as raw string by default, or handle charset decoding/encoding for /^T/ and COMM frames
81
+
82
+ === 0.6.1 / 2008-02-28
83
+
84
+ * FIXED: fails to read id3v2 tags when iconv fails
85
+
86
+ === 0.6 / 2008-02-24
87
+
88
+ * FIXED: correct handling of encoding in id3v2 tags
89
+
90
+ === 0.5.1 / 2007-09-10
91
+
92
+ * ADDED: Mp3Info#reload method to reload the file from the disk
93
+ * FIXED: bug [#2604] Not able to delete tag1
94
+ * FIXED: bug #3401 'id3v2.rb dies when trying to read a certain mp3'
95
+ * FIXED: bug #2957 'Error message "Can't define singleton"'
96
+ * FIXED: bug #3068 'require_gem ("ruby-mp3info") doesn't works'
97
+ * FIXED: bug #11967 "Leading 'h' from 'http://' gets chopped on URL fields"
98
+ * PATCHED: with patch #3157 'Fix for 64 bit Ruby'
99
+
100
+ === 0.5 / 2005-12-06
101
+
102
+ * id3v2 writing and removing support added. tag2 attribute is r/w now
103
+ * max guess size to find a valid frame set to 2Mb
104
+ * implemented a new class ID3v2, ID2TAGS moved into it
105
+ * Mp3Info.tag is r/w now and has priority over @tag1 and @tag2 when writing
106
+ * added Mp3Info#rename() method to change the filename written at close
107
+ * clean up: all overloaded standards classes replaced by including modules
108
+ * FIXED bug in reading id3v2 tags tagged with olds versions of "mp3ext" ( http://www.mutschler.de/mp3ext/ )
109
+ * FIXED bug on calculating id3v2 frame size
110
+ * FIXED bug when multiple TLEN tags
111
+ * FIXED bug when converting text tag from Unicode
112
+ * FIXED bug: file was not closed, causing too many opened files and test failure on win32
113
+
114
+ === 0.4 / 2005-04-26
115
+
116
+ * fixes in vbr mode
117
+ * removed extract_info_from_head() function
118
+ * now try several times to find a good header frame before giving up
119
+ * correct handling of unicode in v2 tags. Require standard "iconv" library if such tags are used
120
+ * FIXED if a tag appears more than one time, create an array with every value found for this tag
121
+
122
+ === 0.3 / 2004-05-04
123
+
124
+ * massive changes of most of the code to make it easier to read & hopefully run faster
125
+ * ID2TAGS hash is just informative now, no use of it in the code. id3v2 tag fields are read in directly
126
+ * added support for id3 v2.2 and v2.4 (0.2.1 only supported v2.3)
127
+ * much improved vbr duration guessing
128
+ * made Mp3Info#to_s output to be prettier
129
+ * moved hastag1? and hastag2? to be class booleans instead of functions (now named hastag1 and hastag2)
130
+ * fixed a bug on computing "error_protection" attribute
131
+ * new attribute "tag", which is a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
132
+ * new method hastag?, which test the presence of any tag
133
+
134
+ === 0.2.1 / 2003-09-04
135
+
136
+ * filename attribute added
137
+ * mp3 files are opened read-only now [Alan Davies <alan__DOT_davies__AT__thomson.com>]
138
+ * Mp3Info#initialize: bugfixes [Alan Davies <alan__DOT_davies__AT__thomson.com>]
139
+ * put NULLs in year field in id3v1 tags instead of zeros [Alan Davies <alan__DOT_davies__AT__thomson.com>]
140
+ * Mp3Info#gettag1: remove null at end of strings [Alan Davies <alan__DOT_davies__AT__thomson.com>]
141
+ * Mp3Info#extract_infos_from_head(): some brackets missed [Alan Davies <alan__DOT_davies__AT__thomson.com>]
142
+
143
+ === 0.2 / 2003-08-18
144
+
145
+ * writing, reading and removing of id3v1 tags
146
+ * reading of id3v2 tags
147
+ * test suite improved
148
+ * to_s method added
149
+ * length attribute is a Float now
150
+
151
+ === 0.1 / 2003-03-17
152
+
153
+ * Initial version
data/README.rdoc ADDED
@@ -0,0 +1,68 @@
1
+ = mp3info
2
+
3
+ * http://github.com/toy/mp3info
4
+
5
+ == DESCRIPTION:
6
+
7
+ mp3info read low-level informations and manipulate tags on mp3 files.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * written in pure ruby
12
+ * read low-level informations like bitrate, length, samplerate, etc...
13
+ * read, write, remove id3v1 and id3v2 tags
14
+ * correctly read VBR files (with or without Xing header)
15
+ * only 2.3 version is supported for writings id3v2 tags
16
+
17
+ == SYNOPSIS:
18
+
19
+ require "mp3info"
20
+ # read and display infos & tags
21
+ Mp3Info.open("myfile.mp3") do |mp3info|
22
+ puts mp3info
23
+ end
24
+
25
+ # read/write tag1 and tag2 with Mp3Info#tag attribute
26
+ # when reading tag2 have priority over tag1
27
+ # when writing, each tag is written.
28
+ Mp3Info.open("myfile.mp3") do |mp3|
29
+ puts mp3.tag.title
30
+ puts mp3.tag.artist
31
+ puts mp3.tag.album
32
+ puts mp3.tag.tracknum
33
+ mp3.tag.title = "track title"
34
+ mp3.tag.artist = "artist name"
35
+ end
36
+
37
+ Mp3Info.open("myfile.mp3") do |mp3|
38
+ # you can access four letter v2 tags like this
39
+ puts mp3.tag2.TIT2
40
+ mp3.tag2.TIT2 = "new TIT2"
41
+ # or like that
42
+ mp3.tag2["TIT2"]
43
+ # at this time, only COMM tag is processed after reading and before writing
44
+ # according to ID3v2#options hash
45
+ mp3.tag2.options[:lang] = "FRE"
46
+ mp3.tag2.COMM = "my comment in french, correctly handled when reading and writing"
47
+ end
48
+
49
+ # tags v2 will be read and written according to the :encoding settings
50
+ mp3 = Mp3Info.open("myfile.mp3", :encoding => 'utf-8')
51
+
52
+ == REQUIREMENTS:
53
+
54
+ * iconv
55
+
56
+ == INSTALL:
57
+
58
+ * gem install mp3info
59
+
60
+ == LICENSE:
61
+
62
+ ruby
63
+
64
+ == TODO:
65
+
66
+ * encoder detection
67
+ * support for more tags in id3v2
68
+ * generalize id3v2 with other audio formats (APE, MPC, OGG, etc...)
data/lib/mp3info.rb ADDED
@@ -0,0 +1,714 @@
1
+ # coding:utf-8
2
+ # License:: Ruby
3
+ # Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
4
+ # Website:: http://ruby-mp3info.rubyforge.org/
5
+
6
+ require 'stringio'
7
+ require "fileutils"
8
+ require "mp3info/extension_modules"
9
+ require "mp3info/id3v2"
10
+
11
+ # ruby -d to display debugging infos
12
+
13
+ # Raised on any kind of error related to ruby-mp3info
14
+ class Mp3InfoError < StandardError ; end
15
+
16
+ class Mp3InfoInternalError < StandardError #:nodoc:
17
+ end
18
+
19
+ class Mp3Info
20
+
21
+ VERSION = "0.6.16"
22
+
23
+ LAYER = [ nil, 3, 2, 1]
24
+ BITRATE = {
25
+ 1 =>
26
+ [
27
+ [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
28
+ [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
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 =>
37
+ [
38
+ [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
39
+ [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
40
+ [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
41
+ ]
42
+ }
43
+ SAMPLERATE = {
44
+ 1 => [ 44100, 48000, 32000 ],
45
+ 2 => [ 22050, 24000, 16000 ],
46
+ 2.5 => [ 11025, 12000, 8000 ]
47
+ }
48
+ CHANNEL_MODE = [ "Stereo", "JStereo", "Dual Channel", "Single Channel"]
49
+
50
+ GENRES = [
51
+ "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
52
+ "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
53
+ "Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
54
+ "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
55
+ "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
56
+ "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
57
+ "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
58
+ "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
59
+ "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
60
+ "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
61
+ "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
62
+ "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
63
+ "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
64
+ "Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
65
+ "Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
66
+ "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
67
+ "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
68
+ "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
69
+ "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
70
+ "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
71
+ "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
72
+ "Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
73
+ "Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
74
+ "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
75
+ "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
76
+ "SynthPop" ]
77
+
78
+ TAG1_SIZE = 128
79
+ #MAX_FRAME_COUNT = 6 #number of frame to read for encoder detection
80
+
81
+ # map to fill the "universal" tag (#tag attribute)
82
+ # for id3v2.2
83
+ TAG_MAPPING_2_2 = {
84
+ "title" => "TT2",
85
+ "artist" => "TP1",
86
+ "album" => "TAL",
87
+ "year" => "TYE",
88
+ "tracknum" => "TRK",
89
+ "comments" => "COM",
90
+ "genre_s" => "TCO"
91
+ }
92
+
93
+ # for id3v2.3 and 2.4
94
+ TAG_MAPPING_2_3 = {
95
+ "title" => "TIT2",
96
+ "artist" => "TPE1",
97
+ "album" => "TALB",
98
+ "year" => "TYER",
99
+ "tracknum" => "TRCK",
100
+ "comments" => "COMM",
101
+ "genre_s" => "TCON"
102
+ }
103
+
104
+ # http://www.codeproject.com/audio/MPEGAudioInfo.asp
105
+ SAMPLES_PER_FRAME = [
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
110
+ ]
111
+
112
+ # mpeg version = 1 or 2
113
+ attr_reader(:mpeg_version)
114
+
115
+ # layer = 1, 2, or 3
116
+ attr_reader(:layer)
117
+
118
+ # bitrate in kbps
119
+ attr_reader(:bitrate)
120
+
121
+ # samplerate in Hz
122
+ attr_reader(:samplerate)
123
+
124
+ # channel mode => "Stereo", "JStereo", "Dual Channel" or "Single Channel"
125
+ attr_reader(:channel_mode)
126
+
127
+ # variable bitrate => true or false
128
+ attr_reader(:vbr)
129
+
130
+ # Hash representing values in the MP3 frame header. Keys are one of the following:
131
+ # - :private (boolean)
132
+ # - :copyright (boolean)
133
+ # - :original (boolean)
134
+ # - :padding (boolean)
135
+ # - :error_protection (boolean)
136
+ # - :mode_extension (integer in the 0..3 range)
137
+ # - :emphasis (integer in the 0..3 range)
138
+ # detailled explanation can be found here: http://www.mp3-tech.org/programmer/frame_header.html
139
+ attr_reader(:header)
140
+
141
+ # length in seconds as a Float
142
+ attr_reader(:length)
143
+
144
+ # error protection => true or false
145
+ attr_reader(:error_protection)
146
+
147
+ #a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
148
+ #this tag has priority over @tag1 and @tag2 when writing the tag with #close
149
+ attr_reader(:tag)
150
+
151
+ # id3v1 tag as a Hash. You can modify it, it will be written when calling
152
+ # "close" method.
153
+ attr_accessor(:tag1)
154
+
155
+ # id3v2 tag attribute as an ID3v2 object. You can modify it, it will be written when calling
156
+ # "close" method.
157
+ attr_accessor(:tag2)
158
+
159
+ # the original filename unless used with a StringIO
160
+ attr_reader(:filename)
161
+
162
+ # Test the presence of an id3v1 tag in file or StringIO +filename_or_io+
163
+ def self.hastag1?(filename_or_io)
164
+ if filename_or_io.is_a?(StringIO)
165
+ io = filename_or_io
166
+ io.rewind
167
+ else
168
+ io = File.new(filename_or_io, "rb")
169
+ end
170
+
171
+ hastag1 = false
172
+ begin
173
+ io.seek(-TAG1_SIZE, File::SEEK_END)
174
+ hastag1 = io.read(3) == "TAG"
175
+ ensure
176
+ io.close if io.is_a?(File)
177
+ end
178
+ hastag1
179
+ end
180
+
181
+ # Test the presence of an id3v2 tag in file or StringIO +filename_or_io+
182
+ def self.hastag2?(filename_or_io)
183
+ if filename_or_io.is_a?(StringIO)
184
+ io = filename_or_io
185
+ io.rewind
186
+ else
187
+ io = File.new(filename_or_io,"rb")
188
+ end
189
+
190
+ hastag2 = false
191
+
192
+ begin
193
+ hastag2 = io.read(3) == "ID3"
194
+ ensure
195
+ io.close if io.is_a?(File)
196
+ end
197
+ hastag2
198
+ end
199
+
200
+ # Remove id3v1 tag from +filename+
201
+ def self.removetag1(filename)
202
+ if self.hastag1?(filename)
203
+ newsize = File.size(filename) - TAG1_SIZE
204
+ File.open(filename, "rb+") { |f| f.truncate(newsize) }
205
+ end
206
+ end
207
+
208
+ # Remove id3v2 tag from +filename+
209
+ def self.removetag2(filename)
210
+ self.open(filename) do |mp3|
211
+ mp3.tag2.clear
212
+ end
213
+ end
214
+
215
+ # Instantiate Mp3Info object with name +filename+.
216
+ # options hash is used for ID3v2#new.
217
+ # Specify :parse_tags => false to disable the processing
218
+ # of the tags (read and write).
219
+ # Specify :parse_mp3 => false to disable processing of the mp3
220
+ def initialize(filename_or_io, options = {})
221
+ warn("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
222
+ @filename_or_io = filename_or_io
223
+ options = {:parse_mp3 => true, :parse_tags => true}.update(options)
224
+ @tag_parsing_enabled = options.delete(:parse_tags)
225
+ @mp3_parsing_enabled = options.delete(:parse_mp3)
226
+ @id3v2_options = options
227
+ reload
228
+ end
229
+
230
+ # reload (or load for the first time) the file from disk
231
+ def reload
232
+ @header = {}
233
+
234
+ if @filename_or_io.is_a?(StringIO)
235
+ @io_is_a_file = false
236
+ @io = @filename_or_io
237
+ @io_size = @io.size
238
+ @filename = nil
239
+ else
240
+ @io_is_a_file = true
241
+ @io = File.new(@filename_or_io, "rb")
242
+ @io_size = @io.stat.size
243
+ @filename = @filename_or_io
244
+ end
245
+
246
+ if @io_size == 0
247
+ raise(Mp3InfoError, "empty file or IO")
248
+ end
249
+
250
+
251
+ @io.extend(Mp3FileMethods)
252
+ @tag1 = @tag = @tag1_orig = @tag_orig = {}
253
+ @tag1.extend(HashKeys)
254
+ @tag2 = ID3v2.new(@id3v2_options)
255
+
256
+ begin
257
+ if @tag_parsing_enabled
258
+ parse_tags
259
+ @tag1_orig = @tag1.dup
260
+
261
+ if hastag1?
262
+ @tag = @tag1.dup
263
+ end
264
+
265
+ if hastag2?
266
+ @tag = {}
267
+ # creation of a sort of "universal" tag, regardless of the tag version
268
+ tag2_mapping = @tag2.version =~ /^2\.2/ ? TAG_MAPPING_2_2 : TAG_MAPPING_2_3
269
+ tag2_mapping.each do |key, tag2_name|
270
+ tag_value = (@tag2[tag2_name].is_a?(Array) ? @tag2[tag2_name].first : @tag2[tag2_name])
271
+ next unless tag_value
272
+ @tag[key] = tag_value.is_a?(Array) ? tag_value.first : tag_value
273
+
274
+ if %w{year tracknum}.include?(key)
275
+ @tag[key] = tag_value.to_i
276
+ end
277
+ # this is a special case with id3v2.2, which uses
278
+ # old fashionned id3v1 genres
279
+ if tag2_name == "TCO" && tag_value =~ /^\((\d+)\)$/
280
+ @tag["genre_s"] = GENRES[$1.to_i]
281
+ end
282
+ end
283
+ end
284
+
285
+ @tag.extend(HashKeys)
286
+ @tag_orig = @tag.dup
287
+ end
288
+
289
+ if @mp3_parsing_enabled
290
+ parse_mp3
291
+ end
292
+
293
+ ensure
294
+ if @io_is_a_file
295
+ @io.close
296
+ end
297
+ end
298
+ end
299
+
300
+ # "block version" of Mp3Info::new()
301
+ def self.open(*params)
302
+ m = self.new(*params)
303
+ ret = nil
304
+ if block_given?
305
+ begin
306
+ ret = yield(m)
307
+ ensure
308
+ m.close
309
+ end
310
+ else
311
+ ret = m
312
+ end
313
+ ret
314
+ end
315
+
316
+ # Remove id3v1 from mp3
317
+ def removetag1
318
+ @tag1.clear
319
+ self
320
+ end
321
+
322
+ # Remove id3v2 from mp3
323
+ def removetag2
324
+ @tag2.clear
325
+ self
326
+ end
327
+
328
+ # Does the file has an id3v1 or v2 tag?
329
+ def hastag?
330
+ hastag1? || hastag2?
331
+ end
332
+
333
+ # Does the file has an id3v1 tag?
334
+ def hastag1?
335
+ !@tag1.empty?
336
+ end
337
+
338
+ # Does the file has an id3v2 tag?
339
+ def hastag2?
340
+ @tag2.parsed?
341
+ end
342
+
343
+ # write to another filename at close()
344
+ def rename(new_filename)
345
+ raise(Mp3InfoError, "cannot rename an IO") unless @io_is_a_file
346
+ @filename = new_filename
347
+ end
348
+
349
+ # this method returns the "audio-only" data boundaries of the file,
350
+ # i.e. content stripped form tags. Useful to compare 2 files with the same
351
+ # audio content but with differents tags. Returned value is an array
352
+ # [position_in_the_file, length_of_the_data]
353
+ def audio_content
354
+ pos = 0
355
+ length = @io_size
356
+ if hastag1?
357
+ length -= TAG1_SIZE
358
+ end
359
+ if hastag2?
360
+ pos = @tag2.io_position
361
+ length -= @tag2.io_position
362
+ end
363
+ [pos, length]
364
+ end
365
+
366
+ # return the length in seconds of one frame
367
+ def frame_length
368
+ SAMPLES_PER_FRAME[@layer][@mpeg_version] / Float(@samplerate)
369
+ end
370
+
371
+ # Flush pending modifications to tags and close the file
372
+ # not used when source IO is a StringIO
373
+ def close
374
+ puts "close" if $DEBUG
375
+ return unless @io_is_a_file
376
+ if !@tag_parsing_enabled
377
+ return
378
+ end
379
+ if @tag != @tag_orig
380
+ puts "@tag has changed" if $DEBUG
381
+
382
+ # @tag1 has precedence over @tag
383
+ if @tag1 == @tag1_orig
384
+ @tag.each do |k, v|
385
+ @tag1[k] = v
386
+ end
387
+ end
388
+
389
+ # ruby-mp3info can only write v2.3 tags
390
+ TAG_MAPPING_2_3.each do |key, tag2_name|
391
+ @tag2.delete(TAG_MAPPING_2_2[key])
392
+ @tag2[tag2_name] = @tag[key] if @tag[key]
393
+ end
394
+ end
395
+
396
+ if @tag1 != @tag1_orig
397
+ puts "@tag1 has changed" if $DEBUG
398
+ raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename_or_io)
399
+ #@tag1_orig.update(@tag1)
400
+ @tag1_orig = @tag1.dup
401
+ File.open(@filename_or_io, 'rb+') do |file|
402
+ if @tag1_orig.empty?
403
+ newsize = @io_size - TAG1_SIZE
404
+ file.truncate(newsize)
405
+ else
406
+ file.seek(-TAG1_SIZE, File::SEEK_END)
407
+ t = file.read(3)
408
+ if t != 'TAG'
409
+ #append new tag
410
+ file.seek(0, File::SEEK_END)
411
+ file.write('TAG')
412
+ end
413
+ str = [
414
+ @tag1_orig["title"]||"",
415
+ @tag1_orig["artist"]||"",
416
+ @tag1_orig["album"]||"",
417
+ ((@tag1_orig["year"] != 0) ? ("%04d" % @tag1_orig["year"].to_i) : "\0\0\0\0"),
418
+ @tag1_orig["comments"]||"",
419
+ 0,
420
+ @tag1_orig["tracknum"]||0,
421
+ @tag1_orig["genre"]||255
422
+ ].pack("Z30Z30Z30Z4Z28CCC")
423
+ file.write(str)
424
+ end
425
+ end
426
+ end
427
+
428
+ if @tag2.changed?
429
+ puts "@tag2 has changed" if $DEBUG
430
+ raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename_or_io)
431
+ tempfile_name = nil
432
+ File.open(@filename_or_io, 'rb+') do |file|
433
+ #if tag2 already exists, seek to end of it
434
+ if @tag2.parsed?
435
+ file.seek(@tag2.io_position)
436
+ end
437
+ # if @io.read(3) == "ID3"
438
+ # version_maj, version_min, flags = @io.read(3).unpack("CCB4")
439
+ # unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
440
+ # tag2_len = @io.get_syncsafe
441
+ # @io.seek(@io.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
442
+ # @io.seek(tag2_len, IO::SEEK_CUR)
443
+ # end
444
+ tempfile_name = @filename_or_io + ".tmp"
445
+ File.open(tempfile_name, "wb") do |tempfile|
446
+ unless @tag2.empty?
447
+ tempfile.write(@tag2.to_bin)
448
+ end
449
+
450
+ bufsiz = file.stat.blksize || 4096
451
+ while buf = file.read(bufsiz)
452
+ tempfile.write(buf)
453
+ end
454
+ end
455
+ end
456
+ File.rename(tempfile_name, @filename_or_io)
457
+ end
458
+ end
459
+
460
+ # close and reopen the file, i.e. commit changes to disk and
461
+ # reload it (only works with "true" files, not StringIO ones)
462
+ def flush
463
+ return unless @io_is_a_file
464
+ close
465
+ reload
466
+ end
467
+
468
+ # inspect inside Mp3Info
469
+ def to_s
470
+ s = "MPEG #{@mpeg_version} Layer #{@layer} #{@vbr ? "VBR" : "CBR"} #{@bitrate} Kbps #{@channel_mode} #{@samplerate} Hz length #{@length} sec. header #{@header.inspect} "
471
+ s << "tag1: "+@tag1.to_hash.inspect+"\n" if hastag1?
472
+ s << "tag2: "+@tag2.to_hash.inspect+"\n" if hastag2?
473
+ s
474
+ end
475
+
476
+ # iterates over each mpeg frame over the file, allowing you to
477
+ # write some funny things, like an mpeg lossless cutter, or frame
478
+ # counter, or whatever you like ;) +frame+ is a hash with the following keys:
479
+ # :layer, :bitrate, :samplerate, :mpeg_version, :padding and :size (in bytes)
480
+ def each_frame
481
+ @io.seek(@first_frame_pos, File::SEEK_SET)
482
+ loop do
483
+ head = @io.read(4).unpack("N").first
484
+ frame = Mp3Info.get_frames_infos(head)
485
+ @io.seek(frame[:size] -4, File::SEEK_CUR)
486
+ yield frame
487
+ #puts "frame #{frame_count} len #{frame[:length]} br #{frame[:bitrate]} @io.pos #{@io.pos}"
488
+ break if @io.eof?
489
+ end
490
+ end
491
+
492
+ private
493
+
494
+ def Mp3Info.get_frames_infos(head)
495
+ # be sure we are in sync
496
+ if ((head & 0xffe00000) != 0xffe00000) || # 11 bit MPEG frame sync
497
+ ((head & 0x00060000) == 0x00060000) || # 2 bit layer type
498
+ ((head & 0x0000f000) == 0x0000f000) || # 4 bit bitrate
499
+ ((head & 0x0000f000) == 0x00000000) || # free format bitstream
500
+ ((head & 0x00000c00) == 0x00000c00) || # 2 bit frequency
501
+ ((head & 0xffff0000) == 0xfffe0000)
502
+ raise Mp3InfoInternalError
503
+ end
504
+ mpeg_version = [2.5, nil, 2, 1][bits(head, 20,19)]
505
+
506
+ layer = LAYER[bits(head, 18,17)]
507
+ raise Mp3InfoInternalError if layer == nil || mpeg_version == nil
508
+
509
+ bitrate = BITRATE[mpeg_version][layer-1][bits(head, 15,12)-1]
510
+ samplerate = SAMPLERATE[mpeg_version][bits(head, 11,10)]
511
+ padding = (head[9] == 1)
512
+ if layer == 1
513
+ size = (12 * bitrate*1000.0 / samplerate + (padding ? 1 : 0))*4
514
+ else # layer 2 and 3
515
+ size = 144 * (bitrate*1000.0 / samplerate) + (padding ? 1 : 0)
516
+ end
517
+ size = size.to_i
518
+ { :layer => layer,
519
+ :bitrate => bitrate,
520
+ :samplerate => samplerate,
521
+ :mpeg_version => mpeg_version,
522
+ :padding => padding,
523
+ :size => size }
524
+ end
525
+
526
+ ### parses the id3 tags of the currently open @io
527
+ def parse_tags
528
+ return if @io_size < TAG1_SIZE # file is too small
529
+
530
+ @tag1_parsed = false
531
+ @io.seek(0)
532
+ f3 = @io.read(3)
533
+ # v1 tag at beginning
534
+ if f3 == "TAG"
535
+ gettag1
536
+ @tag1_parsed = true
537
+ end
538
+
539
+ @tag2.from_io(@io) if f3 == "ID3" # v2 tag at beginning
540
+
541
+ unless @tag1_parsed # v1 tag at end
542
+ # this preserves the file pos if tag2 found, since gettag2 leaves
543
+ # the file at the best guess as to the first MPEG frame
544
+ pos = (@tag2.io_position || 0)
545
+ # seek to where id3v1 tag should be
546
+ @io.seek(-TAG1_SIZE, IO::SEEK_END)
547
+ if @io.read(3) == "TAG"
548
+ gettag1
549
+ end
550
+ @io.seek(pos)
551
+ end
552
+ end
553
+
554
+ ### gets id3v1 tag information from @io
555
+ ### assumes @io is pointing to char after "TAG" id
556
+ def gettag1
557
+ @tag1_parsed = true
558
+ @tag1["title"] = @io.read(30).unpack("A*").first
559
+ @tag1["artist"] = @io.read(30).unpack("A*").first
560
+ @tag1["album"] = @io.read(30).unpack("A*").first
561
+ year_t = @io.read(4).to_i
562
+ @tag1["year"] = year_t unless year_t == 0
563
+ comments = @io.read(30)
564
+ if comments.getbyte(-2) == 0
565
+ @tag1["tracknum"] = comments.getbyte(-1).to_i
566
+ comments.chop! #remove the last char
567
+ end
568
+ @tag1["comments"] = comments.unpack("A*").first
569
+ @tag1["genre"] = @io.getbyte
570
+ @tag1["genre_s"] = GENRES[@tag1["genre"]] || ""
571
+
572
+ # clear empty tags
573
+ @tag1.delete_if { |k, v| v.respond_to?(:empty?) && v.empty? }
574
+ @tag1.delete("genre") if @tag1["genre"] == 255
575
+ @tag1.delete("tracknum") if @tag1["tracknum"] == 0
576
+ end
577
+
578
+ ### reads through @io from current pos until it finds a valid MPEG header
579
+ ### returns the MPEG header as FixNum
580
+ def find_next_frame
581
+ # @io will now be sitting at the best guess for where the MPEG frame is.
582
+ # It should be at byte 0 when there's no id3v2 tag.
583
+ # It should be at the end of the id3v2 tag or the zero padding if there
584
+ # is a id3v2 tag.
585
+ #dummyproof = @io.stat.size - @io.pos => WAS TOO MUCH
586
+
587
+ dummyproof = [ @io_size - @io.pos, 2000000 ].min
588
+ dummyproof.times do |i|
589
+ if @io.getbyte == 0xff
590
+ data = @io.read(3)
591
+ raise(Mp3InfoError, "end of file reached") if @io.eof?
592
+ head = 0xff000000 + (data.getbyte(0) << 16) + (data.getbyte(1) << 8) + data.getbyte(2)
593
+ begin
594
+ Mp3Info.get_frames_infos(head)
595
+ return head
596
+ rescue Mp3InfoInternalError
597
+ @io.seek(-3, IO::SEEK_CUR)
598
+ end
599
+ end
600
+ end
601
+ if @io.eof?
602
+ raise Mp3InfoError, "cannot find a valid frame: got EOF"
603
+ else
604
+ raise Mp3InfoError, "cannot find a valid frame after reading #{dummyproof} bytes"
605
+ end
606
+ end
607
+
608
+ def frame_scan(frame_limit = nil)
609
+ frame_count = bitrate_sum = 0
610
+ each_frame do |frame|
611
+ bitrate_sum += frame[:bitrate]
612
+ frame_count += 1
613
+ break if frame_limit && (frame_count >= frame_limit)
614
+ end
615
+
616
+ average_bitrate = bitrate_sum/frame_count.to_f
617
+ length = (frame_count-1) * frame_length
618
+ [average_bitrate, length]
619
+ end
620
+
621
+ def parse_mp3
622
+ ### extracts MPEG info from MPEG header and stores it in the hash @mpeg
623
+ ### head (fixnum) = valid 4 byte MPEG header
624
+
625
+ found = false
626
+
627
+ head = nil
628
+ 5.times do
629
+ head = find_next_frame()
630
+ @first_frame_pos = @io.pos - 4
631
+ current_frame = Mp3Info.get_frames_infos(head)
632
+ @mpeg_version = current_frame[:mpeg_version]
633
+ @layer = current_frame[:layer]
634
+ @header[:error_protection] = head[16] == 0 ? true : false
635
+ @bitrate = current_frame[:bitrate]
636
+ @samplerate = current_frame[:samplerate]
637
+ @header[:padding] = current_frame[:padding]
638
+ @header[:private] = head[8] == 0 ? true : false
639
+ @channel_mode = CHANNEL_MODE[@channel_num = Mp3Info.bits(head, 7,6)]
640
+ @header[:mode_extension] = Mp3Info.bits(head, 5,4)
641
+ @header[:copyright] = (head[3] == 1 ? true : false)
642
+ @header[:original] = (head[2] == 1 ? true : false)
643
+ @header[:emphasis] = Mp3Info.bits(head, 1,0)
644
+ @vbr = false
645
+ found = true
646
+ break
647
+ end
648
+
649
+ raise(Mp3InfoError, "Cannot find good frame") unless found
650
+
651
+ seek = @mpeg_version == 1 ?
652
+ (@channel_num == 3 ? 17 : 32) :
653
+ (@channel_num == 3 ? 9 : 17)
654
+
655
+ @io.seek(seek, IO::SEEK_CUR)
656
+
657
+ vbr_head = @io.read(4)
658
+ if vbr_head == "Xing"
659
+ puts "Xing header (VBR) detected" if $DEBUG
660
+ flags = @io.get32bits
661
+ stream_size = frame_count = 0
662
+ flags[1] == 1 and frame_count = @io.get32bits
663
+ flags[2] == 1 and stream_size = @io.get32bits
664
+ puts "#{frame_count} frames" if $DEBUG
665
+ raise(Mp3InfoError, "bad VBR header") if frame_count.zero?
666
+ # currently this just skips the TOC entries if they're found
667
+ @io.seek(100, IO::SEEK_CUR) if flags[0] == 1
668
+ #@vbr_quality = @io.get32bits if flags[3] == 1
669
+
670
+ samples_per_frame = SAMPLES_PER_FRAME[@layer][@mpeg_version]
671
+ @length = frame_count * samples_per_frame / Float(@samplerate)
672
+
673
+ @bitrate = (((stream_size/frame_count)*@samplerate)/144) / 1024
674
+ @vbr = true
675
+ else
676
+ # for cbr, calculate duration with the given bitrate
677
+
678
+ stream_size = @io_size - (hastag1? ? TAG1_SIZE : 0) - (@tag2.io_position || 0)
679
+ @length = ((stream_size << 3)/1000.0)/@bitrate
680
+ # read the first 100 frames and decide if the mp3 is vbr and needs full scan
681
+ begin
682
+ bitrate, length = frame_scan(100)
683
+ if @bitrate != bitrate
684
+ @vbr = true
685
+ @bitrate, @length = frame_scan
686
+ end
687
+ rescue Mp3InfoInternalError
688
+ end
689
+ end
690
+ end
691
+
692
+ ### returns the selected bit range (b, a) as a number
693
+ ### NOTE: b > a if not, returns 0
694
+ def self.bits(number, b, a)
695
+ t = 0
696
+ b.downto(a) { |i| t += t + number[i] }
697
+ t
698
+ end
699
+ end
700
+
701
+ if $0 == __FILE__
702
+ while filename = ARGV.shift
703
+ begin
704
+ info = Mp3Info.new(filename)
705
+ puts filename
706
+ #puts "MPEG #{info.mpeg_version} Layer #{info.layer} #{info.vbr ? "VBR" : "CBR"} #{info.bitrate} Kbps \
707
+ #{info.channel_mode} #{info.samplerate} Hz length #{info.length} sec."
708
+ puts info
709
+ rescue Mp3InfoError => e
710
+ puts "#{filename}\nERROR: #{e}"
711
+ end
712
+ puts
713
+ end
714
+ end