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