mp3info 0.6.17

Sign up to get free protection for your applications and to get access to all the features.
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