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