Class: Mp3Info

Inherits:
Object
  • Object
show all
Defined in:
lib/mp3info.rb,
lib/mp3info/extension_modules.rb

Defined Under Namespace

Modules: HashKeys, Mp3FileMethods

Constant Summary collapse

VERSION =
"0.6.11"
LAYER =
[ nil, 3, 2, 1]
BITRATE =
{
  1 => 
  [
    [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
    [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
    [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] ],
  2 => 
  [
    [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
    [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
    [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
  ],
  2.5 => 
  [
    [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
    [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
    [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
  ]
}
SAMPLERATE =
{
  1 => [ 44100, 48000, 32000 ],
  2 => [ 22050, 24000, 16000 ],
  2.5 => [ 11025, 12000, 8000 ]
}
CHANNEL_MODE =
[ "Stereo", "JStereo", "Dual Channel", "Single Channel"]
GENRES =
[
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
"Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
"Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
"Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
"Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
"Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
"Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
"Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
"Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
"New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
"Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
"Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
"Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
"Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
"Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
"Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
"Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
"Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
"Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
"Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
"Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
"SynthPop" ]
TAG1_SIZE =
128
TAG_MAPPING_2_2 =

map to fill the “universal” tag (#tag attribute) for id3v2.2

{ 
  "title"    => "TT2",
  "artist"   => "TP2", 
  "album"    => "TAL",
  "year"     => "TYE",
  "tracknum" => "TRK",
  "comments" => "COM",
  "genre_s"  => "TCO"
}
TAG_MAPPING_2_3 =

for id3v2.3 and 2.4

{ 
  "title"    => "TIT2",
  "artist"   => "TPE1", 
  "album"    => "TALB",
  "year"     => "TYER",
  "tracknum" => "TRCK",
  "comments" => "COMM",
  "genre_s"  => "TCON"
}
SAMPLES_PER_FRAME =
[
  nil,
  {1=>384, 2=>384, 2.5=>384},    # Layer I   
  {1=>1152, 2=>1152, 2.5=>1152}, # Layer II
  {1=>1152, 2=>576, 2.5=>576}    # Layer III
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename, options = {}) ⇒ Mp3Info

Instantiate Mp3Info object with name filename. options hash is used for ID3v2#new. You can specify :parse_tags => false to disable the processing of the tags (read and write).



196
197
198
199
200
201
202
203
204
205
# File 'lib/mp3info.rb', line 196

def initialize(filename, options = {})
  warn("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
  @filename = filename
  @tag_parsing_enabled = options.delete(:parse_tags)
  if @tag_parsing_enabled == nil
    @tag_parsing_enabled = true
  end
  @id3v2_options = options
  reload
end

Instance Attribute Details

#bitrateObject (readonly)

bitrate in kbps



119
120
121
# File 'lib/mp3info.rb', line 119

def bitrate
  @bitrate
end

#channel_modeObject (readonly)

channel mode => “Stereo”, “JStereo”, “Dual Channel” or “Single Channel”



125
126
127
# File 'lib/mp3info.rb', line 125

def channel_mode
  @channel_mode
end

#error_protectionObject (readonly)

error protection => true or false



145
146
147
# File 'lib/mp3info.rb', line 145

def error_protection
  @error_protection
end

#filenameObject (readonly)

the original filename



160
161
162
# File 'lib/mp3info.rb', line 160

def filename
  @filename
end

#headerObject (readonly)

Hash representing values in the MP3 frame header. Keys are one of the following:

  • :private (boolean)

  • :copyright (boolean)

  • :original (boolean)

  • :padding (boolean)

  • :error_protection (boolean)

  • :mode_extension (integer in the 0..3 range)

  • :emphasis (integer in the 0..3 range)

detailled explanation can be found here: www.mp3-tech.org/programmer/frame_header.html



139
140
141
# File 'lib/mp3info.rb', line 139

def header
  @header
end

#layerObject (readonly)

layer = 1, 2, or 3



116
117
118
# File 'lib/mp3info.rb', line 116

def layer
  @layer
end

#lengthObject (readonly)

length in seconds as a Float



142
143
144
# File 'lib/mp3info.rb', line 142

def length
  @length
end

#mpeg_versionObject (readonly)

mpeg version = 1 or 2



113
114
115
# File 'lib/mp3info.rb', line 113

def mpeg_version
  @mpeg_version
end

#samplerateObject (readonly)

samplerate in Hz



122
123
124
# File 'lib/mp3info.rb', line 122

def samplerate
  @samplerate
end

#tagObject (readonly)

a sort of “universal” tag, regardless of the tag version, 1 or 2, with the same keys as @tag1 this tag has priority over @tag1 and @tag2 when writing the tag with #close



149
150
151
# File 'lib/mp3info.rb', line 149

def tag
  @tag
end

#tag1Object

id3v1 tag as a Hash. You can modify it, it will be written when calling “close” method.



153
154
155
# File 'lib/mp3info.rb', line 153

def tag1
  @tag1
end

#tag2Object

id3v2 tag attribute as an ID3v2 object. You can modify it, it will be written when calling “close” method.



157
158
159
# File 'lib/mp3info.rb', line 157

def tag2
  @tag2
end

#vbrObject (readonly)

variable bitrate => true or false



128
129
130
# File 'lib/mp3info.rb', line 128

def vbr
  @vbr
end

Class Method Details

.hastag1?(filename) ⇒ Boolean

Test the presence of an id3v1 tag in file filename

Returns:

  • (Boolean)


163
164
165
166
167
168
# File 'lib/mp3info.rb', line 163

def self.hastag1?(filename)
  File.open(filename) { |f|
    f.seek(-TAG1_SIZE, File::SEEK_END)
    f.read(3) == "TAG"
  }
end

.hastag2?(filename) ⇒ Boolean

Test the presence of an id3v2 tag in file filename

Returns:

  • (Boolean)


171
172
173
174
175
# File 'lib/mp3info.rb', line 171

def self.hastag2?(filename)
  File.open(filename) { |f|
    f.read(3) == "ID3"
  }
end

.open(*params) ⇒ Object

“block version” of Mp3Info::new()



360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/mp3info.rb', line 360

def self.open(*params)
  m = self.new(*params)
  ret = nil
  if block_given?
    begin
      ret = yield(m)
    ensure
      m.close
    end
  else
    ret = m
  end
  ret
end

.removetag1(filename) ⇒ Object

Remove id3v1 tag from filename



178
179
180
181
182
183
# File 'lib/mp3info.rb', line 178

def self.removetag1(filename)
  if self.hastag1?(filename)
    newsize = File.size(filename) - TAG1_SIZE
    File.open(filename, "rb+") { |f| f.truncate(newsize) }
  end
end

.removetag2(filename) ⇒ Object

Remove id3v2 tag from filename



186
187
188
189
190
# File 'lib/mp3info.rb', line 186

def self.removetag2(filename)
  self.open(filename) do |mp3|
    mp3.tag2.clear
  end
end

Instance Method Details

#audio_contentObject

this method returns the “audio-only” data boundaries of the file, i.e. content stripped form tags. Useful to compare 2 files with the same audio content but with differents tags. Returned value is an array

position_in_the_file, length_of_the_data


411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/mp3info.rb', line 411

def audio_content
  pos = 0
  length = File.size(@filename)
  if hastag1?
    length -= TAG1_SIZE
  end
  if hastag2?
    pos = @tag2.io_position
    length -= @tag2.io_position
  end
  [pos, length]
end

#closeObject

Flush pending modifications to tags and close the file



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/mp3info.rb', line 425

def close
  puts "close" if $DEBUG
  if !@tag_parsing_enabled
    return 
  end
  if @tag != @tag_orig
    puts "@tag has changed" if $DEBUG

    # @tag1 has precedence over @tag
    if @tag1 == @tag1_orig
	@tag.each do |k, v|
 @tag1[k] = v
	end
    end

    # ruby-mp3info can only write v2.3 tags
    TAG_MAPPING_2_3.each do |key, tag2_name|
      @tag2.delete(TAG_MAPPING_2_2[key])
      @tag2[tag2_name] = @tag[key] if @tag[key]
    end
  end

  if @tag1 != @tag1_orig
    puts "@tag1 has changed" if $DEBUG
    raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename)
    #@tag1_orig.update(@tag1)
    @tag1_orig = @tag1.dup
    File.open(@filename, 'rb+') do |file|
      if @tag1_orig.empty?
        newsize = file.stat.size - TAG1_SIZE
        file.truncate(newsize)
      else
        file.seek(-TAG1_SIZE, File::SEEK_END)
        t = file.read(3)
        if t != 'TAG'
          #append new tag
          file.seek(0, File::SEEK_END)
          file.write('TAG')
        end
        str = [
          @tag1_orig["title"]||"",
          @tag1_orig["artist"]||"",
          @tag1_orig["album"]||"",
          ((@tag1_orig["year"] != 0) ? ("%04d" % @tag1_orig["year"].to_i) : "\0\0\0\0"),
          @tag1_orig["comments"]||"",
          0,
          @tag1_orig["tracknum"]||0,
          @tag1_orig["genre"]||255
          ].pack("Z30Z30Z30Z4Z28CCC")
        file.write(str)
      end
    end
  end

  if @tag2.changed?
    puts "@tag2 has changed" if $DEBUG
    raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename)
    tempfile_name = nil
    File.open(@filename, 'rb+') do |file|
	#if tag2 already exists, seek to end of it
	if @tag2.parsed?
 file.seek(@tag2.io_position)
	end
#      if @file.read(3) == "ID3"
#        version_maj, version_min, flags = @file.read(3).unpack("CCB4")
#        unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
#	tag2_len = @file.get_syncsafe
#        @file.seek(@file.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
#	@file.seek(tag2_len, IO::SEEK_CUR)
#      end
	tempfile_name = @filename + ".tmp"
	File.open(tempfile_name, "wb") do |tempfile|
 unless @tag2.empty?
   tempfile.write(@tag2.to_bin)
 end

 bufsiz = file.stat.blksize || 4096
 while buf = file.read(bufsiz)
   tempfile.write(buf)
 end
	end
    end
    File.rename(tempfile_name, @filename)
  end
end

#flushObject

close and reopen the file, i.e. commit changes to disk and reload it



513
514
515
516
# File 'lib/mp3info.rb', line 513

def flush
  close
  reload
end

#get_frames_infos(head) ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/mp3info.rb', line 338

def get_frames_infos(head)
  # be sure we are in sync
  if ((head & 0xffe00000) != 0xffe00000)    || # 11 bit MPEG frame sync
     ((head & 0x00060000) == 0x00060000)    || #  2 bit layer type
     ((head & 0x0000f000) == 0x0000f000)    || #  4 bit bitrate
     ((head & 0x0000f000) == 0x00000000)    || #        free format bitstream
     ((head & 0x00000c00) == 0x00000c00)    || #  2 bit frequency
     ((head & 0xffff0000) == 0xfffe0000) 
    raise Mp3InfoInternalError 
  end
  mpeg_version = [2.5, nil, 2, 1][bits(head, 20,19)]
  
  layer = LAYER[bits(head, 18,17)]
  raise Mp3InfoInternalError if layer == nil || mpeg_version == nil
  { :layer => layer,
    :bitrate => BITRATE[mpeg_version][layer-1][bits(head, 15,12)-1],
    :samplerate => SAMPLERATE[mpeg_version][bits(head, 11,10)],
    :mpeg_version => mpeg_version,
    :padding => (head[9] == 1) }
end

#hastag1?Boolean

Does the file has an id3v1 tag?

Returns:

  • (Boolean)


393
394
395
# File 'lib/mp3info.rb', line 393

def hastag1?
  !@tag1.empty?
end

#hastag2?Boolean

Does the file has an id3v2 tag?

Returns:

  • (Boolean)


398
399
400
# File 'lib/mp3info.rb', line 398

def hastag2?
  @tag2.parsed?
end

#hastag?Boolean

Does the file has an id3v1 or v2 tag?

Returns:

  • (Boolean)


388
389
390
# File 'lib/mp3info.rb', line 388

def hastag?
  hastag1? || hastag2?
end

#reloadObject

reload (or load for the first time) the file from disk

Raises:



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/mp3info.rb', line 208

def reload
  raise(Mp3InfoError, "empty file") unless File.size?(@filename)

  @header = {}
  
  @file = File.new(filename, "rb")
  @file.extend(Mp3FileMethods)
  @tag1 = @tag = @tag1_orig = @tag_orig = {}
  @tag1.extend(HashKeys)
  @tag2 = ID3v2.new(@id3v2_options)
  
  begin
    if @tag_parsing_enabled
      parse_tags
      @tag1_orig = @tag1.dup

      if hastag1?
        @tag = @tag1.dup
      end

      if hastag2?
        @tag = {}
        # creation of a sort of "universal" tag, regardless of the tag version
        tag2_mapping = @tag2.version =~ /^2\.2/ ? TAG_MAPPING_2_2 : TAG_MAPPING_2_3
        tag2_mapping.each do |key, tag2_name| 
          tag_value = (@tag2[tag2_name].is_a?(Array) ? @tag2[tag2_name].first : @tag2[tag2_name])
          next unless tag_value
          @tag[key] = tag_value.is_a?(Array) ? tag_value.first : tag_value

          if %w{year tracknum}.include?(key)
            @tag[key] = tag_value.to_i
          end
          # this is a special case with id3v2.2, which uses
          # old fashionned id3v1 genres
          if tag2_name == "TCO" && tag_value =~ /^\((\d+)\)$/
            @tag["genre_s"] = GENRES[$1.to_i]
          end
        end
      end

      @tag.extend(HashKeys)
      @tag_orig = @tag.dup
    end


    ### extracts MPEG info from MPEG header and stores it in the hash @mpeg
    ###  head (fixnum) = valid 4 byte MPEG header
    
    found = false

    head = nil
    5.times do
	head = find_next_frame() 
      @first_frame_pos = @file.pos - 4
      current_frame = get_frames_infos(head)
	@mpeg_version = current_frame[:mpeg_version]
	@layer = current_frame[:layer]
	@header[:error_protection] = head[16] == 0 ? true : false
	@bitrate = current_frame[:bitrate]
	@samplerate = current_frame[:samplerate]
	@header[:padding] = current_frame[:padding]
	@header[:private] = head[8] == 0 ? true : false
	@channel_mode = CHANNEL_MODE[@channel_num = bits(head, 7,6)]
	@header[:mode_extension] = bits(head, 5,4)
	@header[:copyright] = (head[3] == 1 ? true : false)
	@header[:original] = (head[2] == 1 ? true : false)
	@header[:emphasis] = bits(head, 1,0)
	@vbr = false
	found = true
      break
    end

    raise(Mp3InfoError, "Cannot find good frame") unless found

    seek = @mpeg_version == 1 ? 
	(@channel_num == 3 ? 17 : 32) :       
	(@channel_num == 3 ?  9 : 17)

    @file.seek(seek, IO::SEEK_CUR)
    
    vbr_head = @file.read(4)
    if vbr_head == "Xing"
	puts "Xing header (VBR) detected" if $DEBUG
	flags = @file.get32bits
	stream_size = frame_count = 0
	flags[1] == 1 and frame_count = @file.get32bits
	flags[2] == 1 and stream_size = @file.get32bits 
	puts "#{frame_count} frames" if $DEBUG
	raise(Mp3InfoError, "bad VBR header") if frame_count.zero?
	# currently this just skips the TOC entries if they're found
	@file.seek(100, IO::SEEK_CUR) if flags[0] == 1
	#@vbr_quality = @file.get32bits if flags[3] == 1

      samples_per_frame = SAMPLES_PER_FRAME[@layer][@mpeg_version] 
	@length = frame_count * samples_per_frame / Float(@samplerate)

	@bitrate = (((stream_size/frame_count)*@samplerate)/144) >> 10
	@vbr = true
    else
	# for cbr, calculate duration with the given bitrate
	stream_size = @file.stat.size - (hastag1? ? TAG1_SIZE : 0) - (@tag2.io_position || 0)
	@length = ((stream_size << 3)/1000.0)/@bitrate
      # read the first 100 frames and decide if the mp3 is vbr and needs full scan
      begin
        bitrate, length = frame_scan(100)
        if @bitrate != bitrate
          @vbr = true
          @bitrate, @length = frame_scan
        end
      rescue Mp3InfoInternalError
      end
	if @tag2["TLEN"]
 # but if another duration is given and it isn't close (within 5%)
 #  assume the mp3 is vbr and go with the given duration
 tlen = (@tag2["TLEN"].is_a?(Array) ? @tag2["TLEN"].last : @tag2["TLEN"]).to_i/1000
 percent_diff = ((@length.to_i-tlen)/tlen.to_f)
 if percent_diff.abs > 0.05
   # without the xing header, this is the best guess without reading
   # every single frame
   @vbr = true
   @length = @tag2["TLEN"].to_i/1000
   @bitrate = (stream_size / @bitrate) >> 10
 end
	end
    end
  ensure
    @file.close
  end
end

#removetag1Object

Remove id3v1 from mp3



376
377
378
379
# File 'lib/mp3info.rb', line 376

def removetag1
  @tag1.clear
  self
end

#removetag2Object

Remove id3v2 from mp3



382
383
384
385
# File 'lib/mp3info.rb', line 382

def removetag2
  @tag2.clear
  self
end

#rename(new_filename) ⇒ Object

write to another filename at close()



403
404
405
# File 'lib/mp3info.rb', line 403

def rename(new_filename)
  @filename = new_filename
end

#to_sObject

inspect inside Mp3Info



519
520
521
522
523
524
# File 'lib/mp3info.rb', line 519

def to_s
  s = "MPEG #{@mpeg_version} Layer #{@layer} #{@vbr ? "VBR" : "CBR"} #{@bitrate} Kbps #{@channel_mode} #{@samplerate} Hz length #{@length} sec. header #{@header.inspect} "
  s << "tag1: "+@tag1.inspect+"\n" if hastag1?
  s << "tag2: "+@tag2.inspect+"\n" if hastag2?
  s
end