mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-16 02:02:58 -07:00
Initial Commit
This commit is contained in:
commit
88daa3fb91
1311 changed files with 256240 additions and 0 deletions
407
lib/mutagen/aac.py
Normal file
407
lib/mutagen/aac.py
Normal file
|
@ -0,0 +1,407 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014 Christoph Reiter
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of version 2 of the GNU General Public License as
|
||||
# published by the Free Software Foundation.
|
||||
|
||||
"""
|
||||
* ADTS - Audio Data Transport Stream
|
||||
* ADIF - Audio Data Interchange Format
|
||||
* See ISO/IEC 13818-7 / 14496-03
|
||||
"""
|
||||
|
||||
from mutagen import StreamInfo
|
||||
from mutagen._file import FileType
|
||||
from mutagen._util import BitReader, BitReaderError, MutagenError
|
||||
from mutagen._compat import endswith, xrange
|
||||
|
||||
|
||||
_FREQS = [
|
||||
96000, 88200, 64000, 48000,
|
||||
44100, 32000, 24000, 22050,
|
||||
16000, 12000, 11025, 8000,
|
||||
7350,
|
||||
]
|
||||
|
||||
|
||||
class _ADTSStream(object):
|
||||
"""Represents a series of frames belonging to the same stream"""
|
||||
|
||||
parsed_frames = 0
|
||||
"""Number of successfully parsed frames"""
|
||||
|
||||
offset = 0
|
||||
"""offset in bytes at which the stream starts (the first sync word)"""
|
||||
|
||||
@classmethod
|
||||
def find_stream(cls, fileobj, max_bytes):
|
||||
"""Returns a possibly valid _ADTSStream or None.
|
||||
|
||||
Args:
|
||||
max_bytes (int): maximum bytes to read
|
||||
"""
|
||||
|
||||
r = BitReader(fileobj)
|
||||
stream = cls(r)
|
||||
if stream.sync(max_bytes):
|
||||
stream.offset = (r.get_position() - 12) // 8
|
||||
return stream
|
||||
|
||||
def sync(self, max_bytes):
|
||||
"""Find the next sync.
|
||||
Returns True if found."""
|
||||
|
||||
# at least 2 bytes for the sync
|
||||
max_bytes = max(max_bytes, 2)
|
||||
|
||||
r = self._r
|
||||
r.align()
|
||||
while max_bytes > 0:
|
||||
try:
|
||||
b = r.bytes(1)
|
||||
if b == b"\xff":
|
||||
if r.bits(4) == 0xf:
|
||||
return True
|
||||
r.align()
|
||||
max_bytes -= 2
|
||||
else:
|
||||
max_bytes -= 1
|
||||
except BitReaderError:
|
||||
return False
|
||||
return False
|
||||
|
||||
def __init__(self, r):
|
||||
"""Use _ADTSStream.find_stream to create a stream"""
|
||||
|
||||
self._fixed_header_key = None
|
||||
self._r = r
|
||||
self.offset = -1
|
||||
self.parsed_frames = 0
|
||||
|
||||
self._samples = 0
|
||||
self._payload = 0
|
||||
self._start = r.get_position() / 8
|
||||
self._last = self._start
|
||||
|
||||
@property
|
||||
def bitrate(self):
|
||||
"""Bitrate of the raw aac blocks, excluding framing/crc"""
|
||||
|
||||
assert self.parsed_frames, "no frame parsed yet"
|
||||
|
||||
if self._samples == 0:
|
||||
return 0
|
||||
|
||||
return (8 * self._payload * self.frequency) // self._samples
|
||||
|
||||
@property
|
||||
def samples(self):
|
||||
"""samples so far"""
|
||||
|
||||
assert self.parsed_frames, "no frame parsed yet"
|
||||
|
||||
return self._samples
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""bytes read in the stream so far (including framing)"""
|
||||
|
||||
assert self.parsed_frames, "no frame parsed yet"
|
||||
|
||||
return self._last - self._start
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
"""0 means unknown"""
|
||||
|
||||
assert self.parsed_frames, "no frame parsed yet"
|
||||
|
||||
b_index = self._fixed_header_key[6]
|
||||
if b_index == 7:
|
||||
return 8
|
||||
elif b_index > 7:
|
||||
return 0
|
||||
else:
|
||||
return b_index
|
||||
|
||||
@property
|
||||
def frequency(self):
|
||||
"""0 means unknown"""
|
||||
|
||||
assert self.parsed_frames, "no frame parsed yet"
|
||||
|
||||
f_index = self._fixed_header_key[4]
|
||||
try:
|
||||
return _FREQS[f_index]
|
||||
except IndexError:
|
||||
return 0
|
||||
|
||||
def parse_frame(self):
|
||||
"""True if parsing was successful.
|
||||
Fails either because the frame wasn't valid or the stream ended.
|
||||
"""
|
||||
|
||||
try:
|
||||
return self._parse_frame()
|
||||
except BitReaderError:
|
||||
return False
|
||||
|
||||
def _parse_frame(self):
|
||||
r = self._r
|
||||
# start == position of sync word
|
||||
start = r.get_position() - 12
|
||||
|
||||
# adts_fixed_header
|
||||
id_ = r.bits(1)
|
||||
layer = r.bits(2)
|
||||
protection_absent = r.bits(1)
|
||||
|
||||
profile = r.bits(2)
|
||||
sampling_frequency_index = r.bits(4)
|
||||
private_bit = r.bits(1)
|
||||
# TODO: if 0 we could parse program_config_element()
|
||||
channel_configuration = r.bits(3)
|
||||
original_copy = r.bits(1)
|
||||
home = r.bits(1)
|
||||
|
||||
# the fixed header has to be the same for every frame in the stream
|
||||
fixed_header_key = (
|
||||
id_, layer, protection_absent, profile, sampling_frequency_index,
|
||||
private_bit, channel_configuration, original_copy, home,
|
||||
)
|
||||
|
||||
if self._fixed_header_key is None:
|
||||
self._fixed_header_key = fixed_header_key
|
||||
else:
|
||||
if self._fixed_header_key != fixed_header_key:
|
||||
return False
|
||||
|
||||
# adts_variable_header
|
||||
r.skip(2) # copyright_identification_bit/start
|
||||
frame_length = r.bits(13)
|
||||
r.skip(11) # adts_buffer_fullness
|
||||
nordbif = r.bits(2)
|
||||
# adts_variable_header end
|
||||
|
||||
crc_overhead = 0
|
||||
if not protection_absent:
|
||||
crc_overhead += (nordbif + 1) * 16
|
||||
if nordbif != 0:
|
||||
crc_overhead *= 2
|
||||
|
||||
left = (frame_length * 8) - (r.get_position() - start)
|
||||
if left < 0:
|
||||
return False
|
||||
r.skip(left)
|
||||
assert r.is_aligned()
|
||||
|
||||
self._payload += (left - crc_overhead) / 8
|
||||
self._samples += (nordbif + 1) * 1024
|
||||
self._last = r.get_position() / 8
|
||||
|
||||
self.parsed_frames += 1
|
||||
return True
|
||||
|
||||
|
||||
class ProgramConfigElement(object):
|
||||
|
||||
element_instance_tag = None
|
||||
object_type = None
|
||||
sampling_frequency_index = None
|
||||
channels = None
|
||||
|
||||
def __init__(self, r):
|
||||
"""Reads the program_config_element()
|
||||
|
||||
Raises BitReaderError
|
||||
"""
|
||||
|
||||
self.element_instance_tag = r.bits(4)
|
||||
self.object_type = r.bits(2)
|
||||
self.sampling_frequency_index = r.bits(4)
|
||||
num_front_channel_elements = r.bits(4)
|
||||
num_side_channel_elements = r.bits(4)
|
||||
num_back_channel_elements = r.bits(4)
|
||||
num_lfe_channel_elements = r.bits(2)
|
||||
num_assoc_data_elements = r.bits(3)
|
||||
num_valid_cc_elements = r.bits(4)
|
||||
|
||||
mono_mixdown_present = r.bits(1)
|
||||
if mono_mixdown_present == 1:
|
||||
r.skip(4)
|
||||
stereo_mixdown_present = r.bits(1)
|
||||
if stereo_mixdown_present == 1:
|
||||
r.skip(4)
|
||||
matrix_mixdown_idx_present = r.bits(1)
|
||||
if matrix_mixdown_idx_present == 1:
|
||||
r.skip(3)
|
||||
|
||||
elms = num_front_channel_elements + num_side_channel_elements + \
|
||||
num_back_channel_elements
|
||||
channels = 0
|
||||
for i in xrange(elms):
|
||||
channels += 1
|
||||
element_is_cpe = r.bits(1)
|
||||
if element_is_cpe:
|
||||
channels += 1
|
||||
r.skip(4)
|
||||
channels += num_lfe_channel_elements
|
||||
self.channels = channels
|
||||
|
||||
r.skip(4 * num_lfe_channel_elements)
|
||||
r.skip(4 * num_assoc_data_elements)
|
||||
r.skip(5 * num_valid_cc_elements)
|
||||
r.align()
|
||||
comment_field_bytes = r.bits(8)
|
||||
r.skip(8 * comment_field_bytes)
|
||||
|
||||
|
||||
class AACError(MutagenError):
|
||||
pass
|
||||
|
||||
|
||||
class AACInfo(StreamInfo):
|
||||
"""AAC stream information.
|
||||
|
||||
Attributes:
|
||||
|
||||
* channels -- number of audio channels
|
||||
* length -- file length in seconds, as a float
|
||||
* sample_rate -- audio sampling rate in Hz
|
||||
* bitrate -- audio bitrate, in bits per second
|
||||
|
||||
The length of the stream is just a guess and might not be correct.
|
||||
"""
|
||||
|
||||
channels = 0
|
||||
length = 0
|
||||
sample_rate = 0
|
||||
bitrate = 0
|
||||
|
||||
def __init__(self, fileobj):
|
||||
# skip id3v2 header
|
||||
start_offset = 0
|
||||
header = fileobj.read(10)
|
||||
from mutagen.id3 import BitPaddedInt
|
||||
if header.startswith(b"ID3"):
|
||||
size = BitPaddedInt(header[6:])
|
||||
start_offset = size + 10
|
||||
|
||||
fileobj.seek(start_offset)
|
||||
adif = fileobj.read(4)
|
||||
if adif == b"ADIF":
|
||||
self._parse_adif(fileobj)
|
||||
self._type = "ADIF"
|
||||
else:
|
||||
self._parse_adts(fileobj, start_offset)
|
||||
self._type = "ADTS"
|
||||
|
||||
def _parse_adif(self, fileobj):
|
||||
r = BitReader(fileobj)
|
||||
try:
|
||||
copyright_id_present = r.bits(1)
|
||||
if copyright_id_present:
|
||||
r.skip(72) # copyright_id
|
||||
r.skip(1 + 1) # original_copy, home
|
||||
bitstream_type = r.bits(1)
|
||||
self.bitrate = r.bits(23)
|
||||
npce = r.bits(4)
|
||||
if bitstream_type == 0:
|
||||
r.skip(20) # adif_buffer_fullness
|
||||
|
||||
pce = ProgramConfigElement(r)
|
||||
try:
|
||||
self.sample_rate = _FREQS[pce.sampling_frequency_index]
|
||||
except IndexError:
|
||||
pass
|
||||
self.channels = pce.channels
|
||||
|
||||
# other pces..
|
||||
for i in xrange(npce):
|
||||
ProgramConfigElement(r)
|
||||
r.align()
|
||||
except BitReaderError as e:
|
||||
raise AACError(e)
|
||||
|
||||
# use bitrate + data size to guess length
|
||||
start = fileobj.tell()
|
||||
fileobj.seek(0, 2)
|
||||
length = fileobj.tell() - start
|
||||
if self.bitrate != 0:
|
||||
self.length = (8.0 * length) / self.bitrate
|
||||
|
||||
def _parse_adts(self, fileobj, start_offset):
|
||||
max_initial_read = 512
|
||||
max_resync_read = 10
|
||||
max_sync_tries = 10
|
||||
|
||||
frames_max = 100
|
||||
frames_needed = 3
|
||||
|
||||
# Try up to X times to find a sync word and read up to Y frames.
|
||||
# If more than Z frames are valid we assume a valid stream
|
||||
offset = start_offset
|
||||
for i in xrange(max_sync_tries):
|
||||
fileobj.seek(offset)
|
||||
s = _ADTSStream.find_stream(fileobj, max_initial_read)
|
||||
if s is None:
|
||||
raise AACError("sync not found")
|
||||
# start right after the last found offset
|
||||
offset += s.offset + 1
|
||||
|
||||
for i in xrange(frames_max):
|
||||
if not s.parse_frame():
|
||||
break
|
||||
if not s.sync(max_resync_read):
|
||||
break
|
||||
|
||||
if s.parsed_frames >= frames_needed:
|
||||
break
|
||||
else:
|
||||
raise AACError(
|
||||
"no valid stream found (only %d frames)" % s.parsed_frames)
|
||||
|
||||
self.sample_rate = s.frequency
|
||||
self.channels = s.channels
|
||||
self.bitrate = s.bitrate
|
||||
|
||||
# size from stream start to end of file
|
||||
fileobj.seek(0, 2)
|
||||
stream_size = fileobj.tell() - (offset + s.offset)
|
||||
# approx
|
||||
self.length = float(s.samples * stream_size) / (s.size * s.frequency)
|
||||
|
||||
def pprint(self):
|
||||
return "AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % (
|
||||
self._type, self.sample_rate, self.length, self.channels,
|
||||
self.bitrate)
|
||||
|
||||
|
||||
class AAC(FileType):
|
||||
"""Load ADTS or ADIF streams containing AAC.
|
||||
|
||||
Tagging is not supported.
|
||||
Use the ID3/APEv2 classes directly instead.
|
||||
"""
|
||||
|
||||
_mimes = ["audio/x-aac"]
|
||||
|
||||
def load(self, filename):
|
||||
self.filename = filename
|
||||
with open(filename, "rb") as h:
|
||||
self.info = AACInfo(h)
|
||||
|
||||
@staticmethod
|
||||
def score(filename, fileobj, header):
|
||||
filename = filename.lower()
|
||||
s = endswith(filename, ".aac") or endswith(filename, ".adts") or \
|
||||
endswith(filename, ".adif")
|
||||
s += b"ADIF" in header
|
||||
return s
|
||||
|
||||
|
||||
Open = AAC
|
||||
error = AACError
|
||||
|
||||
__all__ = ["AAC", "Open"]
|
Loading…
Add table
Add a link
Reference in a new issue