1# -*- coding: utf-8 -*-
2# Copyright (C) 2014 Christoph Reiter
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9"""
10* ADTS - Audio Data Transport Stream
11* ADIF - Audio Data Interchange Format
12* See ISO/IEC 13818-7 / 14496-03
13"""
14
15from mutagen import StreamInfo
16from mutagen._file import FileType
17from mutagen._util import BitReader, BitReaderError, MutagenError, loadfile, \
18    convert_error
19from mutagen.id3._util import BitPaddedInt
20from mutagen._compat import endswith, xrange
21
22
23_FREQS = [
24    96000, 88200, 64000, 48000,
25    44100, 32000, 24000, 22050,
26    16000, 12000, 11025, 8000,
27    7350,
28]
29
30
31class _ADTSStream(object):
32    """Represents a series of frames belonging to the same stream"""
33
34    parsed_frames = 0
35    """Number of successfully parsed frames"""
36
37    offset = 0
38    """offset in bytes at which the stream starts (the first sync word)"""
39
40    @classmethod
41    def find_stream(cls, fileobj, max_bytes):
42        """Returns a possibly valid _ADTSStream or None.
43
44        Args:
45            max_bytes (int): maximum bytes to read
46        """
47
48        r = BitReader(fileobj)
49        stream = cls(r)
50        if stream.sync(max_bytes):
51            stream.offset = (r.get_position() - 12) // 8
52            return stream
53
54    def sync(self, max_bytes):
55        """Find the next sync.
56        Returns True if found."""
57
58        # at least 2 bytes for the sync
59        max_bytes = max(max_bytes, 2)
60
61        r = self._r
62        r.align()
63        while max_bytes > 0:
64            try:
65                b = r.bytes(1)
66                if b == b"\xff":
67                    if r.bits(4) == 0xf:
68                        return True
69                    r.align()
70                    max_bytes -= 2
71                else:
72                    max_bytes -= 1
73            except BitReaderError:
74                return False
75        return False
76
77    def __init__(self, r):
78        """Use _ADTSStream.find_stream to create a stream"""
79
80        self._fixed_header_key = None
81        self._r = r
82        self.offset = -1
83        self.parsed_frames = 0
84
85        self._samples = 0
86        self._payload = 0
87        self._start = r.get_position() / 8
88        self._last = self._start
89
90    @property
91    def bitrate(self):
92        """Bitrate of the raw aac blocks, excluding framing/crc"""
93
94        assert self.parsed_frames, "no frame parsed yet"
95
96        if self._samples == 0:
97            return 0
98
99        return (8 * self._payload * self.frequency) // self._samples
100
101    @property
102    def samples(self):
103        """samples so far"""
104
105        assert self.parsed_frames, "no frame parsed yet"
106
107        return self._samples
108
109    @property
110    def size(self):
111        """bytes read in the stream so far (including framing)"""
112
113        assert self.parsed_frames, "no frame parsed yet"
114
115        return self._last - self._start
116
117    @property
118    def channels(self):
119        """0 means unknown"""
120
121        assert self.parsed_frames, "no frame parsed yet"
122
123        b_index = self._fixed_header_key[6]
124        if b_index == 7:
125            return 8
126        elif b_index > 7:
127            return 0
128        else:
129            return b_index
130
131    @property
132    def frequency(self):
133        """0 means unknown"""
134
135        assert self.parsed_frames, "no frame parsed yet"
136
137        f_index = self._fixed_header_key[4]
138        try:
139            return _FREQS[f_index]
140        except IndexError:
141            return 0
142
143    def parse_frame(self):
144        """True if parsing was successful.
145        Fails either because the frame wasn't valid or the stream ended.
146        """
147
148        try:
149            return self._parse_frame()
150        except BitReaderError:
151            return False
152
153    def _parse_frame(self):
154        r = self._r
155        # start == position of sync word
156        start = r.get_position() - 12
157
158        # adts_fixed_header
159        id_ = r.bits(1)
160        layer = r.bits(2)
161        protection_absent = r.bits(1)
162
163        profile = r.bits(2)
164        sampling_frequency_index = r.bits(4)
165        private_bit = r.bits(1)
166        # TODO: if 0 we could parse program_config_element()
167        channel_configuration = r.bits(3)
168        original_copy = r.bits(1)
169        home = r.bits(1)
170
171        # the fixed header has to be the same for every frame in the stream
172        fixed_header_key = (
173            id_, layer, protection_absent, profile, sampling_frequency_index,
174            private_bit, channel_configuration, original_copy, home,
175        )
176
177        if self._fixed_header_key is None:
178            self._fixed_header_key = fixed_header_key
179        else:
180            if self._fixed_header_key != fixed_header_key:
181                return False
182
183        # adts_variable_header
184        r.skip(2)  # copyright_identification_bit/start
185        frame_length = r.bits(13)
186        r.skip(11)  # adts_buffer_fullness
187        nordbif = r.bits(2)
188        # adts_variable_header end
189
190        crc_overhead = 0
191        if not protection_absent:
192            crc_overhead += (nordbif + 1) * 16
193            if nordbif != 0:
194                crc_overhead *= 2
195
196        left = (frame_length * 8) - (r.get_position() - start)
197        if left < 0:
198            return False
199        r.skip(left)
200        assert r.is_aligned()
201
202        self._payload += (left - crc_overhead) / 8
203        self._samples += (nordbif + 1) * 1024
204        self._last = r.get_position() / 8
205
206        self.parsed_frames += 1
207        return True
208
209
210class ProgramConfigElement(object):
211
212    element_instance_tag = None
213    object_type = None
214    sampling_frequency_index = None
215    channels = None
216
217    def __init__(self, r):
218        """Reads the program_config_element()
219
220        Raises BitReaderError
221        """
222
223        self.element_instance_tag = r.bits(4)
224        self.object_type = r.bits(2)
225        self.sampling_frequency_index = r.bits(4)
226        num_front_channel_elements = r.bits(4)
227        num_side_channel_elements = r.bits(4)
228        num_back_channel_elements = r.bits(4)
229        num_lfe_channel_elements = r.bits(2)
230        num_assoc_data_elements = r.bits(3)
231        num_valid_cc_elements = r.bits(4)
232
233        mono_mixdown_present = r.bits(1)
234        if mono_mixdown_present == 1:
235            r.skip(4)
236        stereo_mixdown_present = r.bits(1)
237        if stereo_mixdown_present == 1:
238            r.skip(4)
239        matrix_mixdown_idx_present = r.bits(1)
240        if matrix_mixdown_idx_present == 1:
241            r.skip(3)
242
243        elms = num_front_channel_elements + num_side_channel_elements + \
244            num_back_channel_elements
245        channels = 0
246        for i in xrange(elms):
247            channels += 1
248            element_is_cpe = r.bits(1)
249            if element_is_cpe:
250                channels += 1
251            r.skip(4)
252        channels += num_lfe_channel_elements
253        self.channels = channels
254
255        r.skip(4 * num_lfe_channel_elements)
256        r.skip(4 * num_assoc_data_elements)
257        r.skip(5 * num_valid_cc_elements)
258        r.align()
259        comment_field_bytes = r.bits(8)
260        r.skip(8 * comment_field_bytes)
261
262
263class AACError(MutagenError):
264    pass
265
266
267class AACInfo(StreamInfo):
268    """AACInfo()
269
270    AAC stream information.
271    The length of the stream is just a guess and might not be correct.
272
273    Attributes:
274        channels (`int`): number of audio channels
275        length (`float`): file length in seconds, as a float
276        sample_rate (`int`): audio sampling rate in Hz
277        bitrate (`int`): audio bitrate, in bits per second
278    """
279
280    channels = 0
281    length = 0
282    sample_rate = 0
283    bitrate = 0
284
285    @convert_error(IOError, AACError)
286    def __init__(self, fileobj):
287        """Raises AACError"""
288
289        # skip id3v2 header
290        start_offset = 0
291        header = fileobj.read(10)
292        if header.startswith(b"ID3"):
293            size = BitPaddedInt(header[6:])
294            start_offset = size + 10
295
296        fileobj.seek(start_offset)
297        adif = fileobj.read(4)
298        if adif == b"ADIF":
299            self._parse_adif(fileobj)
300            self._type = "ADIF"
301        else:
302            self._parse_adts(fileobj, start_offset)
303            self._type = "ADTS"
304
305    def _parse_adif(self, fileobj):
306        r = BitReader(fileobj)
307        try:
308            copyright_id_present = r.bits(1)
309            if copyright_id_present:
310                r.skip(72)  # copyright_id
311            r.skip(1 + 1)  # original_copy, home
312            bitstream_type = r.bits(1)
313            self.bitrate = r.bits(23)
314            npce = r.bits(4)
315            if bitstream_type == 0:
316                r.skip(20)  # adif_buffer_fullness
317
318            pce = ProgramConfigElement(r)
319            try:
320                self.sample_rate = _FREQS[pce.sampling_frequency_index]
321            except IndexError:
322                pass
323            self.channels = pce.channels
324
325            # other pces..
326            for i in xrange(npce):
327                ProgramConfigElement(r)
328            r.align()
329        except BitReaderError as e:
330            raise AACError(e)
331
332        # use bitrate + data size to guess length
333        start = fileobj.tell()
334        fileobj.seek(0, 2)
335        length = fileobj.tell() - start
336        if self.bitrate != 0:
337            self.length = (8.0 * length) / self.bitrate
338
339    def _parse_adts(self, fileobj, start_offset):
340        max_initial_read = 512
341        max_resync_read = 10
342        max_sync_tries = 10
343
344        frames_max = 100
345        frames_needed = 3
346
347        # Try up to X times to find a sync word and read up to Y frames.
348        # If more than Z frames are valid we assume a valid stream
349        offset = start_offset
350        for i in xrange(max_sync_tries):
351            fileobj.seek(offset)
352            s = _ADTSStream.find_stream(fileobj, max_initial_read)
353            if s is None:
354                raise AACError("sync not found")
355            # start right after the last found offset
356            offset += s.offset + 1
357
358            for i in xrange(frames_max):
359                if not s.parse_frame():
360                    break
361                if not s.sync(max_resync_read):
362                    break
363
364            if s.parsed_frames >= frames_needed:
365                break
366        else:
367            raise AACError(
368                "no valid stream found (only %d frames)" % s.parsed_frames)
369
370        self.sample_rate = s.frequency
371        self.channels = s.channels
372        self.bitrate = s.bitrate
373
374        # size from stream start to end of file
375        fileobj.seek(0, 2)
376        stream_size = fileobj.tell() - (offset + s.offset)
377        # approx
378        self.length = float(s.samples * stream_size) / (s.size * s.frequency)
379
380    def pprint(self):
381        return u"AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % (
382            self._type, self.sample_rate, self.length, self.channels,
383            self.bitrate)
384
385
386class AAC(FileType):
387    """AAC(filething)
388
389    Arguments:
390        filething (filething)
391
392    Load ADTS or ADIF streams containing AAC.
393
394    Tagging is not supported.
395    Use the ID3/APEv2 classes directly instead.
396
397    Attributes:
398        info (`AACInfo`)
399    """
400
401    _mimes = ["audio/x-aac"]
402
403    @loadfile()
404    def load(self, filething):
405        self.info = AACInfo(filething.fileobj)
406
407    def add_tags(self):
408        raise AACError("doesn't support tags")
409
410    @staticmethod
411    def score(filename, fileobj, header):
412        filename = filename.lower()
413        s = endswith(filename, ".aac") or endswith(filename, ".adts") or \
414            endswith(filename, ".adif")
415        s += b"ADIF" in header
416        return s
417
418
419Open = AAC
420error = AACError
421
422__all__ = ["AAC", "Open"]
423