1# -*- coding: utf-8 -*- 2# Copyright (C) 2006 Lukas Lalinsky 3# Copyright (C) 2012 Christoph Reiter 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9 10"""Musepack audio streams with APEv2 tags. 11 12Musepack is an audio format originally based on the MPEG-1 Layer-2 13algorithms. Stream versions 4 through 7 are supported. 14 15For more information, see http://www.musepack.net/. 16""" 17 18__all__ = ["Musepack", "Open", "delete"] 19 20import struct 21 22from ._compat import endswith, xrange 23from mutagen import StreamInfo 24from mutagen.apev2 import APEv2File, error, delete 25from mutagen.id3._util import BitPaddedInt 26from mutagen._util import cdata, convert_error, intround 27 28 29class MusepackHeaderError(error): 30 pass 31 32 33RATES = [44100, 48000, 37800, 32000] 34 35 36def _parse_sv8_int(fileobj, limit=9): 37 """Reads (max limit) bytes from fileobj until the MSB is zero. 38 All 7 LSB will be merged to a big endian uint. 39 40 Raises ValueError in case not MSB is zero, or EOFError in 41 case the file ended before limit is reached. 42 43 Returns (parsed number, number of bytes read) 44 """ 45 46 num = 0 47 for i in xrange(limit): 48 c = fileobj.read(1) 49 if len(c) != 1: 50 raise EOFError 51 c = bytearray(c) 52 num = (num << 7) | (c[0] & 0x7F) 53 if not c[0] & 0x80: 54 return num, i + 1 55 if limit > 0: 56 raise ValueError 57 return 0, 0 58 59 60def _calc_sv8_gain(gain): 61 # 64.82 taken from mpcdec 62 return 64.82 - gain / 256.0 63 64 65def _calc_sv8_peak(peak): 66 return (10 ** (peak / (256.0 * 20.0)) / 65535.0) 67 68 69class MusepackInfo(StreamInfo): 70 """MusepackInfo() 71 72 Musepack stream information. 73 74 Attributes: 75 channels (`int`): number of audio channels 76 length (`float`): file length in seconds, as a float 77 sample_rate (`int`): audio sampling rate in Hz 78 bitrate (`int`): audio bitrate, in bits per second 79 version (`int`) Musepack stream version 80 81 Optional Attributes: 82 83 Attributes: 84 title_gain (`float`): Replay Gain for this song 85 title_peak (`float`): Peak data for this song 86 album_gain (`float`): Replay Gain for this album 87 album_peak (`float`): Peak data for this album 88 89 These attributes are only available in stream version 7/8. The 90 gains are a float, +/- some dB. The peaks are a percentage [0..1] of 91 the maximum amplitude. This means to get a number comparable to 92 VorbisGain, you must multiply the peak by 2. 93 """ 94 95 @convert_error(IOError, MusepackHeaderError) 96 def __init__(self, fileobj): 97 """Raises MusepackHeaderError""" 98 99 header = fileobj.read(4) 100 if len(header) != 4: 101 raise MusepackHeaderError("not a Musepack file") 102 103 # Skip ID3v2 tags 104 if header[:3] == b"ID3": 105 header = fileobj.read(6) 106 if len(header) != 6: 107 raise MusepackHeaderError("not a Musepack file") 108 size = 10 + BitPaddedInt(header[2:6]) 109 fileobj.seek(size) 110 header = fileobj.read(4) 111 if len(header) != 4: 112 raise MusepackHeaderError("not a Musepack file") 113 114 if header.startswith(b"MPCK"): 115 self.__parse_sv8(fileobj) 116 else: 117 self.__parse_sv467(fileobj) 118 119 if not self.bitrate and self.length != 0: 120 fileobj.seek(0, 2) 121 self.bitrate = intround(fileobj.tell() * 8 / self.length) 122 123 def __parse_sv8(self, fileobj): 124 # SV8 http://trac.musepack.net/trac/wiki/SV8Specification 125 126 key_size = 2 127 mandatory_packets = [b"SH", b"RG"] 128 129 def check_frame_key(key): 130 if ((len(frame_type) != key_size) or 131 (not b'AA' <= frame_type <= b'ZZ')): 132 raise MusepackHeaderError("Invalid frame key.") 133 134 frame_type = fileobj.read(key_size) 135 check_frame_key(frame_type) 136 137 while frame_type not in (b"AP", b"SE") and mandatory_packets: 138 try: 139 frame_size, slen = _parse_sv8_int(fileobj) 140 except (EOFError, ValueError): 141 raise MusepackHeaderError("Invalid packet size.") 142 data_size = frame_size - key_size - slen 143 # packets can be at maximum data_size big and are padded with zeros 144 145 if frame_type == b"SH": 146 mandatory_packets.remove(frame_type) 147 self.__parse_stream_header(fileobj, data_size) 148 elif frame_type == b"RG": 149 mandatory_packets.remove(frame_type) 150 self.__parse_replaygain_packet(fileobj, data_size) 151 else: 152 fileobj.seek(data_size, 1) 153 154 frame_type = fileobj.read(key_size) 155 check_frame_key(frame_type) 156 157 if mandatory_packets: 158 raise MusepackHeaderError("Missing mandatory packets: %s." % 159 ", ".join(map(repr, mandatory_packets))) 160 161 self.length = float(self.samples) / self.sample_rate 162 self.bitrate = 0 163 164 def __parse_stream_header(self, fileobj, data_size): 165 # skip CRC 166 fileobj.seek(4, 1) 167 remaining_size = data_size - 4 168 169 try: 170 self.version = bytearray(fileobj.read(1))[0] 171 except (TypeError, IndexError): 172 raise MusepackHeaderError("SH packet ended unexpectedly.") 173 174 remaining_size -= 1 175 176 try: 177 samples, l1 = _parse_sv8_int(fileobj) 178 samples_skip, l2 = _parse_sv8_int(fileobj) 179 except (EOFError, ValueError): 180 raise MusepackHeaderError( 181 "SH packet: Invalid sample counts.") 182 183 self.samples = samples - samples_skip 184 remaining_size -= l1 + l2 185 186 data = fileobj.read(remaining_size) 187 if len(data) != remaining_size: 188 raise MusepackHeaderError("SH packet ended unexpectedly.") 189 self.sample_rate = RATES[bytearray(data)[0] >> 5] 190 self.channels = (bytearray(data)[1] >> 4) + 1 191 192 def __parse_replaygain_packet(self, fileobj, data_size): 193 data = fileobj.read(data_size) 194 if data_size < 9: 195 raise MusepackHeaderError("Invalid RG packet size.") 196 if len(data) != data_size: 197 raise MusepackHeaderError("RG packet ended unexpectedly.") 198 title_gain = cdata.short_be(data[1:3]) 199 title_peak = cdata.short_be(data[3:5]) 200 album_gain = cdata.short_be(data[5:7]) 201 album_peak = cdata.short_be(data[7:9]) 202 if title_gain: 203 self.title_gain = _calc_sv8_gain(title_gain) 204 if title_peak: 205 self.title_peak = _calc_sv8_peak(title_peak) 206 if album_gain: 207 self.album_gain = _calc_sv8_gain(album_gain) 208 if album_peak: 209 self.album_peak = _calc_sv8_peak(album_peak) 210 211 def __parse_sv467(self, fileobj): 212 fileobj.seek(-4, 1) 213 header = fileobj.read(32) 214 if len(header) != 32: 215 raise MusepackHeaderError("not a Musepack file") 216 217 # SV7 218 if header.startswith(b"MP+"): 219 self.version = bytearray(header)[3] & 0xF 220 if self.version < 7: 221 raise MusepackHeaderError("not a Musepack file") 222 frames = cdata.uint_le(header[4:8]) 223 flags = cdata.uint_le(header[8:12]) 224 225 self.title_peak, self.title_gain = struct.unpack( 226 "<Hh", header[12:16]) 227 self.album_peak, self.album_gain = struct.unpack( 228 "<Hh", header[16:20]) 229 self.title_gain /= 100.0 230 self.album_gain /= 100.0 231 self.title_peak /= 65535.0 232 self.album_peak /= 65535.0 233 234 self.sample_rate = RATES[(flags >> 16) & 0x0003] 235 self.bitrate = 0 236 # SV4-SV6 237 else: 238 header_dword = cdata.uint_le(header[0:4]) 239 self.version = (header_dword >> 11) & 0x03FF 240 if self.version < 4 or self.version > 6: 241 raise MusepackHeaderError("not a Musepack file") 242 self.bitrate = (header_dword >> 23) & 0x01FF 243 self.sample_rate = 44100 244 if self.version >= 5: 245 frames = cdata.uint_le(header[4:8]) 246 else: 247 frames = cdata.ushort_le(header[6:8]) 248 if self.version < 6: 249 frames -= 1 250 self.channels = 2 251 self.length = float(frames * 1152 - 576) / self.sample_rate 252 253 def pprint(self): 254 rg_data = [] 255 if hasattr(self, "title_gain"): 256 rg_data.append(u"%+0.2f (title)" % self.title_gain) 257 if hasattr(self, "album_gain"): 258 rg_data.append(u"%+0.2f (album)" % self.album_gain) 259 rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or "" 260 261 return u"Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % ( 262 self.version, self.length, self.sample_rate, self.bitrate, rg_data) 263 264 265class Musepack(APEv2File): 266 """Musepack(filething) 267 268 Arguments: 269 filething (filething) 270 271 Attributes: 272 info (`MusepackInfo`) 273 """ 274 275 _Info = MusepackInfo 276 _mimes = ["audio/x-musepack", "audio/x-mpc"] 277 278 @staticmethod 279 def score(filename, fileobj, header): 280 filename = filename.lower() 281 282 return (header.startswith(b"MP+") + header.startswith(b"MPCK") + 283 endswith(filename, b".mpc")) 284 285 286Open = Musepack 287