#!/usr/bin/env ruby require "stringio" require "mp3info/id3v2" class MpcInfoError < StandardError; end class MpcInfo PROFILES_NAMES = [ 'no profile', 'Experimental', 'unused', 'unused', 'unused', 'below Telephone (q = 0.0)', 'below Telephone (q = 1.0)', 'Telephone (q = 2.0)', 'Thumb (q = 3.0)', 'Radio (q = 4.0)', 'Standard (q = 5.0)', 'Extreme (q = 6.0)', 'Insane (q = 7.0)', 'BrainDead (q = 8.0)', 'above BrainDead (q = 9.0)', 'above BrainDead (q = 10.0)' ] FREQUENCIES = [ 44100, 48000, 37800, 32000 ] SV4_6_HEADER = Regexp.new('^[\x00\x01\x10\x11\x40\x41\x50\x51\x80\x81\x90\x91\xC0\xC1\xD0\xD1][\x20-37][\x00\x20\x40\x60\x80\xA0\xC0\xE0]/', nil, 'n') attr_reader :infos attr_reader :id3v2_tag def initialize(filename) @file = File.open(filename, "rb") @infos = {} @infos['raw'] = {} parse_infos end private def parse_infos mpc_header = @file.read(3) if mpc_header == "MP+" # this is SV7+ header = StringIO.new(@file.read(25)) header_size = 28 #stream_version_byte = header.read(4).unpack("V").first stream_version_byte = header.read(1)[0] #.unpack("c").first @infos['stream_major_version'] = (stream_version_byte & 0x0F) @infos['stream_minor_version'] = (stream_version_byte & 0xF0) >> 4 @infos['frame_count'] = read32(header) if @infos['stream_major_version'] != 7 raise(MpcInfoError, "Only Musepack SV7 supported") end flags_dword1 = read32(header) @infos['intensity_stereo'] = ((flags_dword1 & 0x80000000) >> 31) == 1 @infos['mid_side_stereo'] = ((flags_dword1 & 0x40000000) >> 30) == 1 @infos['max_subband'] = (flags_dword1 & 0x3F000000) >> 24 @infos['raw']['profile'] = (flags_dword1 & 0x00F00000) >> 20 @infos['begin_loud'] = ((flags_dword1 & 0x00080000) >> 19) == 1 @infos['end_loud'] = ((flags_dword1 & 0x00040000) >> 18) == 1 @infos['raw']['sample_rate'] = (flags_dword1 & 0x00030000) >> 16 @infos['max_level'] = (flags_dword1 & 0x0000FFFF) @infos['raw']['title_peak'] = read16(header) @infos['raw']['title_gain'] = read16(header) @infos['raw']['album_peak'] = read16(header) @infos['raw']['album_gain'] = read16(header) flags_dword2 = read32(header) @infos['true_gapless'] = ((flags_dword2 & 0x80000000) >> 31) == 1 @infos['last_frame_length'] = (flags_dword2 & 0x7FF00000) >> 20 not_sure_what = read32(header, 3) @infos['raw']['encoder_version'] = read8(header) @infos['profile'] = PROFILES_NAMES[ @infos['raw']['profile'] ] || 'invalid' @infos['sample_rate'] = FREQUENCIES[ @infos['raw']['sample_rate'] ] if @infos['sample_rate'] == 0 raise(MpcInfoError, 'Corrupt MPC file: frequency == zero') end sample_rate = @infos['sample_rate']; channels = 2 #appears to be hardcoded @infos['samples'] = (((@infos['frame_count'] - 1) * 1152) + @infos['last_frame_length']) * channels @infos['length'] = (((@infos['frame_count'] - 1) * 1152) + @infos['last_frame_length']) * channels @infos['length'] = (@infos['samples'] / channels) / @infos['sample_rate'].to_f if @infos['length'] == 0 raise(MpcInfoError, 'Corrupt MPC file: playtime_seconds == zero') end # add size of file header to avdataoffset - calc bitrate correctly + MD5 data avdataoffset = header_size # FIXME is $ThisFileInfo['avdataend'] == File.size ???? @infos['bitrate'] = ((@file.stat.size - avdataoffset) * 8) / @infos['length'] @infos['title_peak'] = @infos['raw']['title_peak'] @infos['title_peak_db'] = @infos['title_peak'].zero? ? 0 : peak_db(@infos['title_peak']) if @infos['raw']['title_gain'] < 0 @infos['title_gain_db'] = (32768 + @infos['raw']['title_gain']) / -100.0 else @infos['title_gain_db'] = @infos['raw']['title_gain'] / 100.0 end @infos['album_peak'] = @infos['raw']['album_peak']; @infos['album_peak_db'] = @infos['album_peak'].zero? ? 0 : peak_db(@infos['album_peak']) if @infos['raw']['album_gain'] < 0 @infos['album_gain_db'] = (32768 + @infos['raw']['album_gain']) / -100.0 else @infos['album_gain_db'] = @infos['raw']['album_gain'] / 100.0 end @infos['encoder_version'] = encoder_version(@infos['raw']['encoder_version']) =begin #FIXME $ThisFileInfo['replay_gain']['track']['adjustment'] = @infos['title_gain_db']; $ThisFileInfo['replay_gain']['album']['adjustment'] = @infos['album_gain_db']; if @infos['title_peak'] > 0 #$ThisFileInfo['replay_gain']['track']['peak'] = @infos['title_peak'] elsif round(@infos['max_level'] * 1.18) > 0) # ThisFileInfo['replay_gain']['track']['peak'] = getid3_lib::CastAsInt(round(@infos['max_level'] * 1.18)); // why? I don't know - see mppdec.c end if @infos['album_peak'] > 0 #$ThisFileInfo['replay_gain']['album']['peak'] = @infos['album_peak']; end #ThisFileInfo['audio']['encoder'] = 'SV'.@infos['stream_major_version'].'.'.@infos['stream_minor_version'].', '.@infos['encoder_version']; #$ThisFileInfo['audio']['encoder'] = @infos['encoder_version']; #$ThisFileInfo['audio']['encoder_options'] = @infos['profile']; =end elsif mpc_header =~ SV4_6_HEADER # this is SV4 - SV6, handle seperately header_size = 8 elsif mpc_header == "ID3" @id3v2_tag = ID3v2.new @id3v2_tag.from_io(@file) @file.seek(@id3v2_tag.io_position) # very dirty hack to allow parsing of mpc infos after id3v2 tag while @file.read(1) != "M"; end if @file.read(2) == "P+" @file.seek(-3, IO::SEEK_CUR) # we need to reparse the tag, since we have the beggining of the mpc file parse_infos else raise(MpcInfoError, "cannot find MPC header after id3 tag") end else raise(MpcInfoError, "cannot find MPC header") end end def read8(io) io.read(1)[0] end def read16(io) io.read(2).unpack("v").first end def read32(io, size = 4) io.read(size).unpack("V").first end def peak_db(i) ((Math.log10(i) / Math.log10(2)) - 15) * 6 end def encoder_version(encoderversion) # Encoder version * 100 (106 = 1.06) # EncoderVersion % 10 == 0 Release (1.0) # EncoderVersion % 2 == 0 Beta (1.06) # EncoderVersion % 2 == 1 Alpha (1.05a...z) if encoderversion == 0 # very old version, not known exactly which 'Buschmann v1.7.0-v1.7.9 or Klemm v0.90-v1.05'; elsif encoderversion % 10 == 0 # release version sprintf("%.2f", encoderversion/100.0) elsif encoderversion % 2 == 0 sprintf("%.2f beta", encoderversion / 100.0) else sprintf("%.2f alpha", encoderversion/100.0) end end end if __FILE__ == $0 require "pp" mpcinfo = MpcInfo.new(ARGV[0]) pp mpcinfo.infos.sort end