In-progress flake8 fixes

This commit is contained in:
Jamie Hardt
2023-11-08 20:43:56 -08:00
parent f978927648
commit 5b1e4ab631
12 changed files with 485 additions and 483 deletions

View File

@@ -1,11 +1,13 @@
from optparse import OptionParser, OptionGroup
import datetime import datetime
from . import WavInfoReader from . import WavInfoReader
from . import __version__ from . import __version__
from optparse import OptionParser
import sys import sys
import json import json
from enum import Enum from enum import Enum
class MyJSONEncoder(json.JSONEncoder): class MyJSONEncoder(json.JSONEncoder):
def default(self, o): def default(self, o):
if isinstance(o, Enum): if isinstance(o, Enum):
@@ -13,23 +15,25 @@ class MyJSONEncoder(json.JSONEncoder):
else: else:
return super().default(o) return super().default(o)
class MissingDataError(RuntimeError): class MissingDataError(RuntimeError):
pass pass
def main(): def main():
parser = OptionParser() parser = OptionParser()
parser.usage = 'wavinfo (--adm | --ixml) <FILE> +' parser.usage = 'wavinfo (--adm | --ixml) <FILE> +'
# parser.add_option('-f', dest='output_format', help='Set the output format', parser.add_option('--adm', dest='adm',
# default='json', help='Output ADM XML',
# metavar='FORMAT') default=False,
action='store_true')
parser.add_option('--adm', dest='adm', help='Output ADM XML', parser.add_option('--ixml', dest='ixml',
default=False, action='store_true') help='Output iXML',
default=False,
parser.add_option('--ixml', dest='ixml', help='Output iXML', action='store_true')
default=False, action='store_true')
(options, args) = parser.parse_args(sys.argv) (options, args) = parser.parse_args(sys.argv)
for arg in args[1:]: for arg in args[1:]:
@@ -48,7 +52,7 @@ def main():
else: else:
ret_dict = { ret_dict = {
'filename': arg, 'filename': arg,
'run_date': datetime.datetime.now().isoformat() , 'run_date': datetime.datetime.now().isoformat(),
'application': "wavinfo " + __version__, 'application': "wavinfo " + __version__,
'scopes': {} 'scopes': {}
} }
@@ -60,8 +64,8 @@ def main():
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2) json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
except MissingDataError as e: except MissingDataError as e:
print("MissingDataError: Missing metadata (%s) in file %s" % \ print("MissingDataError: Missing metadata (%s) in file %s" %
(e, arg), file=sys.stderr) (e, arg), file=sys.stderr)
continue continue
except Exception as e: except Exception as e:
raise e raise e

View File

@@ -1,14 +1,18 @@
import struct import struct
from collections import namedtuple # from collections import namedtuple
from typing import NamedTuple, Dict
from . import riff_parser from . import riff_parser
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
class RF64Context(NamedTuple):
sample_count: int
bigchunk_table: Dict[str, int]
def parse_rf64(stream, signature = b'RF64') -> RF64Context: def parse_rf64(stream, signature=b'RF64') -> RF64Context:
start = stream.tell() start = stream.tell()
assert( stream.read(4) == b'WAVE' ) assert stream.read(4) == b'WAVE'
ds64_chunk = riff_parser.parse_chunk(stream) ds64_chunk = riff_parser.parse_chunk(stream)
assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \ assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \
@@ -16,10 +20,10 @@ def parse_rf64(stream, signature = b'RF64') -> RF64Context:
ds64_field_spec = "<QQQI" ds64_field_spec = "<QQQI"
ds64_fields_size = struct.calcsize(ds64_field_spec) ds64_fields_size = struct.calcsize(ds64_field_spec)
assert(ds64_chunk.ident == b'ds64') assert ds64_chunk.ident == b'ds64'
ds64_data = ds64_chunk.read_data(stream) ds64_data = ds64_chunk.read_data(stream)
assert(len(ds64_data) >= ds64_fields_size) assert len(ds64_data) >= ds64_fields_size
riff_size, data_size, sample_count, length_lookup_table = struct.unpack( riff_size, data_size, sample_count, length_lookup_table = struct.unpack(
ds64_field_spec, ds64_data[0:ds64_fields_size] ds64_field_spec, ds64_data[0:ds64_fields_size]
@@ -30,14 +34,14 @@ def parse_rf64(stream, signature = b'RF64') -> RF64Context:
# chunksize64size = struct.calcsize(chunksize64format) # chunksize64size = struct.calcsize(chunksize64format)
for _ in range(length_lookup_table): for _ in range(length_lookup_table):
bigname, bigsize = struct.unpack_from(chunksize64format, ds64_data, bigname, bigsize = struct.unpack_from(chunksize64format,
offset= ds64_fields_size) ds64_data,
offset=ds64_fields_size)
bigchunk_table[bigname] = bigsize bigchunk_table[bigname] = bigsize
bigchunk_table[b'data'] = data_size bigchunk_table[b'data'] = data_size
bigchunk_table[signature] = riff_size bigchunk_table[signature] = riff_size
stream.seek(start, 0) stream.seek(start, 0)
return RF64Context( sample_count=sample_count, return RF64Context(sample_count=sample_count,
bigchunk_table=bigchunk_table) bigchunk_table=bigchunk_table)

View File

@@ -1,5 +1,4 @@
# from optparse import Option
from optparse import Option
import struct import struct
from .rf64_parser import parse_rf64, RF64Context from .rf64_parser import parse_rf64, RF64Context
from typing import NamedTuple, Union, List, Optional from typing import NamedTuple, Union, List, Optional
@@ -56,7 +55,7 @@ def parse_chunk(stream, rf64_context=None):
rf64_context = parse_rf64(stream=stream, signature=ident) rf64_context = parse_rf64(stream=stream, signature=ident)
assert rf64_context is not None, \ assert rf64_context is not None, \
f"Sentinel data size 0xFFFFFFFF found outside of RF64 context" "Sentinel data size 0xFFFFFFFF found outside of RF64 context"
data_size = rf64_context.bigchunk_table[ident] data_size = rf64_context.bigchunk_table[ident]

View File

@@ -2,8 +2,8 @@
# def binary_to_string(binary_value): # def binary_to_string(binary_value):
# return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '') # return reduce(lambda val, el: val + "{:02x}".format(el),
# binary_value, '')
# class UMIDParser: # class UMIDParser:
# """ # """
@@ -13,109 +13,109 @@
# """ # """
# def __init__(self, raw_umid: bytes): # def __init__(self, raw_umid: bytes):
# self.raw_umid = raw_umid # self.raw_umid = raw_umid
# #
# @property # @property
# def universal_label(self) -> bytearray: # def universal_label(self) -> bytearray:
# return self.raw_umid[0:12] # return self.raw_umid[0:12]
# #
# @property # @property
# def basic_umid(self): # def basic_umid(self):
# return self.raw_umid[0:32] # return self.raw_umid[0:32]
# def basic_umid_to_str(self): # def basic_umid_to_str(self):
# return binary_to_string(self.raw_umid[0:32]) # return binary_to_string(self.raw_umid[0:32])
# #
# @property # @property
# def universal_label_is_valid(self) -> bool: # def universal_label_is_valid(self) -> bool:
# valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01' # valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
# return self.universal_label[0:len(valid_preamble)] == valid_preamble # return self.universal_label[0:len(valid_preamble)] == valid_preamble
# #
# @property # @property
# def material_type(self) -> str: # def material_type(self) -> str:
# material_byte = self.raw_umid[10] # material_byte = self.raw_umid[10]
# if material_byte == 0x1: # if material_byte == 0x1:
# return 'picture' # return 'picture'
# elif material_byte == 0x2: # elif material_byte == 0x2:
# return 'audio' # return 'audio'
# elif material_byte == 0x3: # elif material_byte == 0x3:
# return 'data' # return 'data'
# elif material_byte == 0x4: # elif material_byte == 0x4:
# return 'other' # return 'other'
# elif material_byte == 0x5: # elif material_byte == 0x5:
# return 'picture_single_component' # return 'picture_single_component'
# elif material_byte == 0x6: # elif material_byte == 0x6:
# return 'picture_multiple_component' # return 'picture_multiple_component'
# elif material_byte == 0x7: # elif material_byte == 0x7:
# return 'audio_single_component' # return 'audio_single_component'
# elif material_byte == 0x9: # elif material_byte == 0x9:
# return 'audio_multiple_component' # return 'audio_multiple_component'
# elif material_byte == 0xb: # elif material_byte == 0xb:
# return 'auxiliary_single_component' # return 'auxiliary_single_component'
# elif material_byte == 0xc: # elif material_byte == 0xc:
# return 'auxiliary_multiple_component' # return 'auxiliary_multiple_component'
# elif material_byte == 0xd: # elif material_byte == 0xd:
# return 'mixed_components' # return 'mixed_components'
# elif material_byte == 0xf: # elif material_byte == 0xf:
# return 'not_identified' # return 'not_identified'
# else: # else:
# return 'not_recognized' # return 'not_recognized'
# #
# @property # @property
# def material_number_creation_method(self) -> str: # def material_number_creation_method(self) -> str:
# method_byte = self.raw_umid[11] # method_byte = self.raw_umid[11]
# method_byte = (method_byte << 4) & 0xf # method_byte = (method_byte << 4) & 0xf
# if method_byte == 0x0: # if method_byte == 0x0:
# return 'undefined' # return 'undefined'
# elif method_byte == 0x1: # elif method_byte == 0x1:
# return 'smpte' # return 'smpte'
# elif method_byte == 0x2: # elif method_byte == 0x2:
# return 'uuid' # return 'uuid'
# elif method_byte == 0x3: # elif method_byte == 0x3:
# return 'masked' # return 'masked'
# elif method_byte == 0x4: # elif method_byte == 0x4:
# return 'ieee1394' # return 'ieee1394'
# elif 0x5 <= method_byte <= 0x7: # elif 0x5 <= method_byte <= 0x7:
# return 'reserved_undefined' # return 'reserved_undefined'
# else: # else:
# return 'unrecognized' # return 'unrecognized'
# #
# @property # @property
# def instance_number_creation_method(self) -> str: # def instance_number_creation_method(self) -> str:
# method_byte = self.raw_umid[11] # method_byte = self.raw_umid[11]
# method_byte = method_byte & 0xf # method_byte = method_byte & 0xf
# if method_byte == 0x0: # if method_byte == 0x0:
# return 'undefined' # return 'undefined'
# elif method_byte == 0x01: # elif method_byte == 0x01:
# return 'local_registration' # return 'local_registration'
# elif method_byte == 0x02: # elif method_byte == 0x02:
# return '24_bit_prs' # return '24_bit_prs'
# elif method_byte == 0x03: # elif method_byte == 0x03:
# return 'copy_number_and_16_bit_prs' # return 'copy_number_and_16_bit_prs'
# elif 0x04 <= method_byte <= 0x0e: # elif 0x04 <= method_byte <= 0x0e:
# return 'reserved_undefined' # return 'reserved_undefined'
# elif method_byte == 0x0f: # elif method_byte == 0x0f:
# return 'live_stream' # return 'live_stream'
# else: # else:
# return 'unrecognized' # return 'unrecognized'
# #
# @property # @property
# def indicated_length(self) -> str: # def indicated_length(self) -> str:
# if self.raw_umid[12] == 0x13: # if self.raw_umid[12] == 0x13:
# return 'basic' # return 'basic'
# elif self.raw_umid[12] == 0x33: # elif self.raw_umid[12] == 0x33:
# return 'extended' # return 'extended'
# #
# @property # @property
# def instance_number(self) -> bytearray: # def instance_number(self) -> bytearray:
# return self.raw_umid[13:3] # return self.raw_umid[13:3]
# #
# @property # @property
# def material_number(self) -> bytearray: # def material_number(self) -> bytearray:
# return self.raw_umid[16:16] # return self.raw_umid[16:16]
# #
# @property # @property
# def source_pack(self) -> Union[bytearray, None]: # def source_pack(self) -> Union[bytearray, None]:
# if self.indicated_length == 'extended': # if self.indicated_length == 'extended':
# return self.raw_umid[32:32] # return self.raw_umid[32:32]
# else: # else:
# return None # return None

View File

@@ -9,8 +9,10 @@ from typing import Optional
from lxml import etree as ET from lxml import etree as ET
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref") ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
class WavADMReader: class WavADMReader:
""" """
Reads XML data from an EBU ADM (Audio Definiton Model) WAV File. Reads XML data from an EBU ADM (Audio Definiton Model) WAV File.
@@ -37,9 +39,13 @@ class WavADMReader:
# these values are either ascii or all null # these values are either ascii or all null
self.channel_uids.append(ChannelEntry(track_index - 1, self.channel_uids.append(
uid.decode('ascii') , track_ref.decode('ascii'), ChannelEntry(track_index - 1,
pack_ref.decode('ascii'))) uid.decode('ascii'),
track_ref.decode('ascii'),
pack_ref.decode('ascii')
)
)
offset += calcsize(uid_fmt) offset += calcsize(uid_fmt)
@@ -105,8 +111,8 @@ class WavADMReader:
*object_name*, *object_id*, *object_name*, *object_id*,
*pack_format_name*, *pack_type*, *channel_format_name* *pack_format_name*, *pack_type*, *channel_format_name*
""" """
channel_info = next((x for x in self.channel_uids \ channel_info = next((x for x in self.channel_uids
if x.track_index == index), None) if x.track_index == index), None)
if channel_info is None: if channel_info is None:
return None return None
@@ -115,60 +121,60 @@ class WavADMReader:
nsmap = self.axml.getroot().nsmap nsmap = self.axml.getroot().nsmap
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap) afext = self.axml.find(".//audioFormatExtended",
namespaces=nsmap)
trackformat_elem = afext.find( trackformat_elem = afext.find(
"audioTrackFormat[@audioTrackFormatID='%s']" \ "audioTrackFormat[@audioTrackFormatID='%s']"
% channel_info.track_ref, % channel_info.track_ref, namespaces=nsmap)
namespaces=nsmap)
stream_id = trackformat_elem[0].text stream_id = trackformat_elem[0].text
channelformatref_elem = afext.find( channelformatref_elem = afext.find(
("audioStreamFormat[@audioStreamFormatID='%s']" ("audioStreamFormat[@audioStreamFormatID='%s']"
"/audioChannelFormatIDRef") % stream_id, "/audioChannelFormatIDRef") % stream_id,
namespaces=nsmap) namespaces=nsmap)
channelformat_id = channelformatref_elem.text channelformat_id = channelformatref_elem.text
packformatref_elem = afext\ packformatref_elem = afext.find(
.find(("audioStreamFormat[@audioStreamFormatID='%s']" ("audioStreamFormat[@audioStreamFormatID='%s']"
"/audioPackFormatIDRef") % stream_id, "/audioPackFormatIDRef") % stream_id,
namespaces=nsmap) namespaces=nsmap)
packformat_id = packformatref_elem.text packformat_id = packformatref_elem.text
channelformat_elem = afext\ channelformat_elem = afext\
.find("audioChannelFormat[@audioChannelFormatID='%s']" \ .find("audioChannelFormat[@audioChannelFormatID='%s']"
% channelformat_id, % channelformat_id,
namespaces=nsmap) namespaces=nsmap)
ret_dict['channel_format_name'] = channelformat_elem.get( ret_dict['channel_format_name'] = channelformat_elem.get(
"audioChannelFormatName") "audioChannelFormatName")
packformat_elem = afext.find( packformat_elem = afext.find(
"audioPackFormat[@audioPackFormatID='%s']" % packformat_id, "audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
namespaces=nsmap) namespaces=nsmap)
ret_dict['pack_type'] = packformat_elem.get( ret_dict['pack_type'] = packformat_elem.get(
"typeDefinition") "typeDefinition")
ret_dict['pack_format_name'] = packformat_elem.get( ret_dict['pack_format_name'] = packformat_elem.get(
"audioPackFormatName") "audioPackFormatName")
object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']" \ object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']"
% packformat_id, % packformat_id,
namespaces=nsmap) namespaces=nsmap)
ret_dict['audio_object_name'] = object_elem.get("audioObjectName") ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
object_id = object_elem.get("audioObjectID") object_id = object_elem.get("audioObjectID")
ret_dict['object_id'] = object_id ret_dict['object_id'] = object_id
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']" \ content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']"
% object_id, % object_id,
namespaces=nsmap) namespaces=nsmap)
ret_dict['content_name'] = content_elem.get("audioContentName") ret_dict['content_name'] = content_elem.get("audioContentName")
ret_dict['content_id'] = content_elem.get("audioContentID") ret_dict['content_id'] = content_elem.get("audioContentID")
return ret_dict return ret_dict
def to_dict(self) -> dict: #FIXME should be "asdict" def to_dict(self) -> dict: # FIXME should be "asdict"
""" """
Get ADM metadata as a dictionary. Get ADM metadata as a dictionary.
""" """
@@ -180,4 +186,4 @@ class WavADMReader:
return dict(channel_entries=list(map(lambda z: make_entry(z), return dict(channel_entries=list(map(lambda z: make_entry(z),
self.channel_uids)), self.channel_uids)),
programme=self.programme()) programme=self.programme())

View File

@@ -3,6 +3,7 @@ import struct
from typing import Optional from typing import Optional
class WavBextReader: class WavBextReader:
def __init__(self, bext_data, encoding): def __init__(self, bext_data, encoding):
""" """
@@ -13,64 +14,64 @@ class WavBextReader:
ASCII. ASCII.
""" """
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + \ packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + \
"hhhhh" + "180s" "hhhhh" + "180s"
rest_starts = struct.calcsize(packstring) rest_starts = struct.calcsize(packstring)
unpacked = struct.unpack(packstring, bext_data[:rest_starts]) unpacked = struct.unpack(packstring, bext_data[:rest_starts])
def sanitize_bytes(b : bytes) -> str: def sanitize_bytes(b: bytes) -> str:
# honestly can't remember why I'm stripping nulls this way # honestly can't remember why I'm stripping nulls this way
first_null = next((index for index, byte in enumerate(b) \ first_null = next((index for index, byte in enumerate(b)
if byte == 0), None) if byte == 0), None)
trimmed = b if first_null is None else b[:first_null] trimmed = b if first_null is None else b[:first_null]
decoded = trimmed.decode(encoding) decoded = trimmed.decode(encoding)
return decoded return decoded
#: Description. A free-text field up to 256 characters long. #: Description. A free-text field up to 256 characters long.
self.description : str = sanitize_bytes(unpacked[0]) self.description: str = sanitize_bytes(unpacked[0])
#: Originator. Usually the name of the encoding application, sometimes #: Originator. Usually the name of the encoding application, sometimes
#: an artist name. #: an artist name.
self.originator : str = sanitize_bytes(unpacked[1]) self.originator: str = sanitize_bytes(unpacked[1])
#: A unique identifier for the file, a serial number. #: A unique identifier for the file, a serial number.
self.originator_ref : str = sanitize_bytes(unpacked[2]) self.originator_ref: str = sanitize_bytes(unpacked[2])
#: Date of the recording, in the format YYYY-MM-DD. #: Date of the recording, in the format YYYY-MM-DD.
self.originator_date : str = sanitize_bytes(unpacked[3]) self.originator_date: str = sanitize_bytes(unpacked[3])
#: Time of the recording, in the format HH:MM:SS. #: Time of the recording, in the format HH:MM:SS.
self.originator_time : str = sanitize_bytes(unpacked[4]) self.originator_time: str = sanitize_bytes(unpacked[4])
#: The sample offset of the start, usually relative #: The sample offset of the start, usually relative
#: to midnight. #: to midnight.
self.time_reference : int = unpacked[5] self.time_reference: int = unpacked[5]
#: A variable-length text field containing a list of processes and #: A variable-length text field containing a list of processes and
#: and conversions performed on the file. #: and conversions performed on the file.
self.coding_history : str = sanitize_bytes(bext_data[rest_starts:]) self.coding_history: str = sanitize_bytes(bext_data[rest_starts:])
#: BEXT version. #: BEXT version.
self.version : int = unpacked[6] self.version: int = unpacked[6]
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though #: SMPTE 330M UMID of this audio file, 64 bytes are allocated though
#: the UMID may only be 32 bytes long. #: the UMID may only be 32 bytes long.
self.umid : Optional[bytes] = None self.umid: Optional[bytes] = None
#: EBU R128 Integrated loudness, in LUFS. #: EBU R128 Integrated loudness, in LUFS.
self.loudness_value : Optional[float] = None self.loudness_value: Optional[float] = None
#: EBU R128 Loudness range, in LUFS. #: EBU R128 Loudness range, in LUFS.
self.loudness_range : Optional[float] = None self.loudness_range: Optional[float] = None
#: True peak level, in dBFS TP #: True peak level, in dBFS TP
self.max_true_peak : Optional[float] = None self.max_true_peak: Optional[float] = None
#: EBU R128 Maximum momentary loudness, in LUFS #: EBU R128 Maximum momentary loudness, in LUFS
self.max_momentary_loudness : Optional[float] = None self.max_momentary_loudness: Optional[float] = None
#: EBU R128 Maximum short-term loudness, in LUFS. #: EBU R128 Maximum short-term loudness, in LUFS.
self.max_shortterm_loudness : Optional[float] = None self.max_shortterm_loudness: Optional[float] = None
if self.version > 0: if self.version > 0:
self.umid = unpacked[7] self.umid = unpacked[7]

View File

@@ -8,7 +8,6 @@ IBM Corporation and Microsoft Corporation
https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
""" """
from dataclasses import dataclass from dataclasses import dataclass
import encodings
from .riff_parser import ChunkDescriptor from .riff_parser import ChunkDescriptor
from struct import unpack, calcsize from struct import unpack, calcsize
@@ -122,7 +121,7 @@ class CueEntry(NamedTuple):
def read(cls, data: bytes) -> 'CueEntry': def read(cls, data: bytes) -> 'CueEntry':
assert len(data) == cls.format_size(), \ assert len(data) == cls.format_size(), \
(f"cue data size incorrect, expected {calcsize(cls.Format)} " (f"cue data size incorrect, expected {calcsize(cls.Format)} "
"found {len(data)}") "found {len(data)}")
parsed = unpack(cls.Format, data) parsed = unpack(cls.Format, data)
@@ -192,11 +191,11 @@ class WavCuesReader:
@classmethod @classmethod
def read_all(cls, f, def read_all(cls, f,
cues: Optional[ChunkDescriptor], cues: Optional[ChunkDescriptor],
labls: List[ChunkDescriptor], labls: List[ChunkDescriptor],
ltxts: List[ChunkDescriptor], ltxts: List[ChunkDescriptor],
notes: List[ChunkDescriptor], notes: List[ChunkDescriptor],
fallback_encoding: str) -> 'WavCuesReader': fallback_encoding: str) -> 'WavCuesReader':
cue_list = [] cue_list = []
if cues is not None: if cues is not None:
@@ -252,8 +251,8 @@ class WavCuesReader:
:returns: a tuple of the the cue's label (if present) and note (if :returns: a tuple of the the cue's label (if present) and note (if
present) present)
""" """
label = next((l.text for l in self.labels label = next((label.text for label in self.labels
if l.name == cue_ident), None) if label.name == cue_ident), None)
note = next((n.text for n in self.notes note = next((n.text for n in self.notes
if n.name == cue_ident), None) if n.name == cue_ident), None)
return (label, note) return (label, note)
@@ -285,4 +284,3 @@ class WavCuesReader:
retval[n]['length'] = r retval[n]['length'] = r
return retval return retval

View File

@@ -14,6 +14,7 @@ from typing import List, Tuple, Any, Union
from io import BytesIO from io import BytesIO
class SegmentType(IntEnum): class SegmentType(IntEnum):
""" """
Metadata segment type. Metadata segment type.
@@ -31,7 +32,7 @@ class SegmentType(IntEnum):
DolbyAtmosSupplemental = 0xa DolbyAtmosSupplemental = 0xa
@classmethod @classmethod
def _missing_(cls,val): def _missing_(cls, val):
return val return val
@@ -77,7 +78,6 @@ class DolbyDigitalPlusMetadata:
MUTE = 0b111 MUTE = 0b111
"-∞ dB" "-∞ dB"
class DolbySurroundEncodingMode(Enum): class DolbySurroundEncodingMode(Enum):
""" """
Dolby surround endcoding mode. Dolby surround endcoding mode.
@@ -87,7 +87,6 @@ class DolbyDigitalPlusMetadata:
NOT_IN_USE = 0b01 NOT_IN_USE = 0b01
NOT_INDICATED = 0b00 NOT_INDICATED = 0b00
class BitStreamMode(Enum): class BitStreamMode(Enum):
""" """
Dolby Digital Plus `bsmod` field Dolby Digital Plus `bsmod` field
@@ -122,7 +121,6 @@ class DolbyDigitalPlusMetadata:
should be interpreted as karaoke. should be interpreted as karaoke.
""" """
class AudioCodingMode(Enum): class AudioCodingMode(Enum):
""" """
Dolby Digital Plus `acmod` field Dolby Digital Plus `acmod` field
@@ -144,7 +142,6 @@ class DolbyDigitalPlusMetadata:
CH_ORD_3_2 = 0b111 CH_ORD_3_2 = 0b111
"LCR + LR surround" "LCR + LR surround"
class CenterDownMixLevel(Enum): class CenterDownMixLevel(Enum):
""" """
§ 4.3.3.1 § 4.3.3.1
@@ -161,7 +158,6 @@ class DolbyDigitalPlusMetadata:
RESERVED = 0b11 RESERVED = 0b11
class SurroundDownMixLevel(Enum): class SurroundDownMixLevel(Enum):
""" """
Dolby Digital Plus `surmixlev` field Dolby Digital Plus `surmixlev` field
@@ -172,7 +168,6 @@ class DolbyDigitalPlusMetadata:
MUTE = 0b10 MUTE = 0b10
RESERVED = 0b11 RESERVED = 0b11
class LanguageCode(int): class LanguageCode(int):
""" """
§ 4.3.4.1 § 4.3.4.1
@@ -181,21 +176,18 @@ class DolbyDigitalPlusMetadata:
""" """
pass pass
class MixLevel(int): class MixLevel(int):
""" """
§ 4.3.6.2 § 4.3.6.2
""" """
pass pass
class DialnormLevel(int): class DialnormLevel(int):
""" """
§ 4.3.4.4 § 4.3.4.4
""" """
pass pass
class RoomType(Enum): class RoomType(Enum):
""" """
`roomtyp` 4.3.6.3 `roomtyp` 4.3.6.3
@@ -205,7 +197,6 @@ class DolbyDigitalPlusMetadata:
SMALL_ROOM_FLAT_CURVE = 0b10 SMALL_ROOM_FLAT_CURVE = 0b10
RESERVED = 0b11 RESERVED = 0b11
class PreferredDownMixMode(Enum): class PreferredDownMixMode(Enum):
""" """
Indicates the creating engineer's preference of what the receiver Indicates the creating engineer's preference of what the receiver
@@ -217,7 +208,6 @@ class DolbyDigitalPlusMetadata:
STEREO = 0b10 STEREO = 0b10
PRO_LOGIC_2 = 0b11 PRO_LOGIC_2 = 0b11
class SurroundEXMode(IntEnum): class SurroundEXMode(IntEnum):
""" """
Dolby Surround-EX mode. Dolby Surround-EX mode.
@@ -228,7 +218,6 @@ class DolbyDigitalPlusMetadata:
SEX = 0b10 SEX = 0b10
PRO_LOGIC_2 = 0b11 PRO_LOGIC_2 = 0b11
class HeadphoneMode(IntEnum): class HeadphoneMode(IntEnum):
""" """
`dheadphonmod` § 4.3.9.2 `dheadphonmod` § 4.3.9.2
@@ -238,12 +227,10 @@ class DolbyDigitalPlusMetadata:
DOLBY_HEADPHONE = 0b10 DOLBY_HEADPHONE = 0b10
RESERVED = 0b11 RESERVED = 0b11
class ADConverterType(Enum): class ADConverterType(Enum):
STANDARD = 0 STANDARD = 0
HDCD = 1 HDCD = 1
class StreamDependency(Enum): class StreamDependency(Enum):
""" """
Encodes `ddplus_info1.stream_type` field § 4.3.12.1 Encodes `ddplus_info1.stream_type` field § 4.3.12.1
@@ -254,7 +241,6 @@ class DolbyDigitalPlusMetadata:
INDEPENDENT_FROM_DOLBY_DIGITAL = 2 INDEPENDENT_FROM_DOLBY_DIGITAL = 2
RESERVED = 3 RESERVED = 3
class RFCompressionProfile(Enum): class RFCompressionProfile(Enum):
""" """
`compr1` RF compression profile `compr1` RF compression profile
@@ -363,12 +349,15 @@ class DolbyDigitalPlusMetadata:
pass pass
def surround_config(b): def surround_config(b):
return DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4), \ return DolbyDigitalPlusMetadata\
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2), \ .CenterDownMixLevel(b & 0x30 >> 4),\
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3) DolbyDigitalPlusMetadata\
.SurroundDownMixLevel(b & 0xc >> 2), \
DolbyDigitalPlusMetadata\
.DolbySurroundEncodingMode(b & 0x3)
def dialnorm_info(b): def dialnorm_info(b):
return (b & 0x80) > 0 , b & 0x40 > 0, b & 0x20 > 0, \ return (b & 0x80) > 0, b & 0x40 > 0, b & 0x20 > 0, \
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f) DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
def langcod(b) -> int: def langcod(b) -> int:
@@ -387,15 +376,15 @@ class DolbyDigitalPlusMetadata:
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level # downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
def ext_bsi1_word2(b): def ext_bsi1_word2(b):
return DolbyDigitalPlusMetadata\ return DolbyDigitalPlusMetadata\
.PreferredDownMixMode(b & 0xC0 >> 6), \ .PreferredDownMixMode(b & 0xC0 >> 6), \
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \ DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7) DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
#surround_ex_mode, dolby_headphone_encoded, ad_converter_type # surround_ex_mode, dolby_headphone_encoded, ad_converter_type
def ext_bsi2_word1(b): def ext_bsi2_word1(b):
return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \ return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \ DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \
DolbyDigitalPlusMetadata.ADConverterType( b & 0x4 >> 2) DolbyDigitalPlusMetadata.ADConverterType(b & 0x4 >> 2)
def ddplus_reserved2(_): def ddplus_reserved2(_):
pass pass
@@ -425,18 +414,18 @@ class DolbyDigitalPlusMetadata:
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1]) lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
ddplus_reserved1(buffer[2:2]) ddplus_reserved1(buffer[2:2])
center_downmix_level, surround_downmix_level, \ center_downmix_level, surround_downmix_level, \
dolby_surround_encoded = surround_config(buffer[4]) dolby_surround_encoded = surround_config(buffer[4])
langcode_present, copyright_bitstream, original_bitstream, \ langcode_present, copyright_bitstream, original_bitstream, \
dialnorm = dialnorm_info(buffer[5]) dialnorm = dialnorm_info(buffer[5])
langcode = langcod(buffer[6]) langcode = langcod(buffer[6])
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7]) prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
loro_center_downmix_level, \ loro_center_downmix_level, \
loro_surround_downmix_level = ext_bsi1_word1(buffer[8]) loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
downmix_mode, ltrt_center_downmix_level, \ downmix_mode, ltrt_center_downmix_level, \
ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9]) ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9])
surround_ex_mode, dolby_headphone_encoded, \ surround_ex_mode, dolby_headphone_encoded, \
ad_converter_type = ext_bsi2_word1(buffer[10]) ad_converter_type = ext_bsi2_word1(buffer[10])
ddplus_reserved2(buffer[11:14]) ddplus_reserved2(buffer[11:14])
compression = compr1(buffer[14]) compression = compr1(buffer[14])
@@ -448,32 +437,32 @@ class DolbyDigitalPlusMetadata:
reserved(buffer[27:69]) reserved(buffer[27:69])
return DolbyDigitalPlusMetadata(program_id=pid, return DolbyDigitalPlusMetadata(program_id=pid,
lfe_on=lfe_on, lfe_on=lfe_on,
bitstream_mode=bitstream_mode, bitstream_mode=bitstream_mode,
audio_coding_mode=audio_coding_mode, audio_coding_mode=audio_coding_mode,
center_downmix_level=center_downmix_level, center_downmix_level=center_downmix_level,
surround_downmix_level=surround_downmix_level, surround_downmix_level=surround_downmix_level,
dolby_surround_encoded=dolby_surround_encoded, dolby_surround_encoded=dolby_surround_encoded,
langcode_present=langcode_present, langcode_present=langcode_present,
copyright_bitstream=copyright_bitstream, copyright_bitstream=copyright_bitstream,
original_bitstream=original_bitstream, original_bitstream=original_bitstream,
dialnorm=dialnorm, dialnorm=dialnorm,
langcode=langcode, langcode=langcode,
prod_info_exists=prod_info_exists, prod_info_exists=prod_info_exists,
mixlevel=mixlevel, mixlevel=mixlevel,
roomtype=roomtype, roomtype=roomtype,
loro_center_downmix_level=loro_center_downmix_level, loro_center_downmix_level=loro_center_downmix_level,
loro_surround_downmix_level=loro_surround_downmix_level, loro_surround_downmix_level=loro_surround_downmix_level,
downmix_mode=downmix_mode, downmix_mode=downmix_mode,
ltrt_center_downmix_level=ltrt_center_downmix_level, ltrt_center_downmix_level=ltrt_center_downmix_level,
ltrt_surround_downmix_level=ltrt_surround_downmix_level, ltrt_surround_downmix_level=ltrt_surround_downmix_level,
surround_ex_mode=surround_ex_mode, surround_ex_mode=surround_ex_mode,
dolby_headphone_encoded=dolby_headphone_encoded, dolby_headphone_encoded=dolby_headphone_encoded,
ad_converter_type=ad_converter_type, ad_converter_type=ad_converter_type,
compression_profile=compression, compression_profile=compression,
dynamic_range=dynamic_range, dynamic_range=dynamic_range,
stream_dependency=stream_info, stream_dependency=stream_info,
datarate_kbps=data_rate) datarate_kbps=data_rate)
@dataclass @dataclass
@@ -492,7 +481,7 @@ class DolbyAtmosMetadata:
NOT_INDICATED = 0x04 NOT_INDICATED = 0x04
tool_name: str tool_name: str
tool_version: Tuple[int,int,int] tool_version: Tuple[int, int, int]
warp_mode: WarpMode warp_mode: WarpMode
SEGMENT_LENGTH = 248 SEGMENT_LENGTH = 248
@@ -501,8 +490,8 @@ class DolbyAtmosMetadata:
@classmethod @classmethod
def load(cls, data: bytes): def load(cls, data: bytes):
assert len(data) == cls.SEGMENT_LENGTH, ("DolbyAtmosMetadata segment " assert len(data) == cls.SEGMENT_LENGTH, ("DolbyAtmosMetadata segment "
"is incorrect length, " "is incorrect length, "
"expected %i actual was %i") % (cls.SEGMENT_LENGTH, len(data)) "expected %i actual was %i") % (cls.SEGMENT_LENGTH, len(data))
h = BytesIO(data) h = BytesIO(data)
@@ -520,9 +509,9 @@ class DolbyAtmosMetadata:
warp_mode = a_val & 0x7 warp_mode = a_val & 0x7
return DolbyAtmosMetadata(tool_name=toolname, return DolbyAtmosMetadata(tool_name=toolname,
tool_version=(major, minor, fix), tool_version=(major, minor, fix),
warp_mode=DolbyAtmosMetadata\ warp_mode=DolbyAtmosMetadata
.WarpMode(warp_mode)) .WarpMode(warp_mode))
@dataclass @dataclass
@@ -541,12 +530,10 @@ class DolbyAtmosSupplementalMetadata:
MID = 0x03 MID = 0x03
NOT_INDICATED = 0x04 NOT_INDICATED = 0x04
object_count: int object_count: int
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode'] render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
trim_modes: List[int] trim_modes: List[int]
MAGIC = 0xf8726fbd MAGIC = 0xf8726fbd
TRIM_CONFIG_COUNT = 9 TRIM_CONFIG_COUNT = 9
@@ -562,15 +549,15 @@ class DolbyAtmosSupplementalMetadata:
object_count = unpack("<H", h.read(2))[0] object_count = unpack("<H", h.read(2))[0]
h.read(1) #skip 1 h.read(1) # skip 1
for _ in range(cls.TRIM_CONFIG_COUNT): for _ in range(cls.TRIM_CONFIG_COUNT):
auto_trim = unpack("B", h.read(1)) auto_trim = unpack("B", h.read(1))
trim_modes.append(auto_trim) trim_modes.append(auto_trim)
h.read(14) #skip 14 h.read(14) # skip 14
h.read(object_count) # skip object_count bytes h.read(object_count) # skip object_count bytes
for _ in range(object_count): for _ in range(object_count):
binaural_mode = unpack("B", h.read(1))[0] binaural_mode = unpack("B", h.read(1))[0]
@@ -578,7 +565,7 @@ class DolbyAtmosSupplementalMetadata:
render_modes.append(binaural_mode) render_modes.append(binaural_mode)
return DolbyAtmosSupplementalMetadata(object_count=object_count, return DolbyAtmosSupplementalMetadata(object_count=object_count,
render_modes=render_modes,trim_modes=trim_modes) render_modes=render_modes, trim_modes=trim_modes)
class WavDolbyMetadataReader: class WavDolbyMetadataReader:
@@ -594,7 +581,7 @@ class WavDolbyMetadataReader:
#: not recognized). #: not recognized).
segment_list: List[Tuple[Union[SegmentType, int], bool, Any]] segment_list: List[Tuple[Union[SegmentType, int], bool, Any]]
version: Tuple[int,int,int,int] version: Tuple[int, int, int, int]
@staticmethod @staticmethod
def segment_checksum(bs: bytes, size: int): def segment_checksum(bs: bytes, size: int):
@@ -607,7 +594,6 @@ class WavDolbyMetadataReader:
return retval return retval
def __init__(self, dbmd_data): def __init__(self, dbmd_data):
self.segment_list = [] self.segment_list = []
@@ -616,19 +602,19 @@ class WavDolbyMetadataReader:
v_vec = [] v_vec = []
for _ in range(4): for _ in range(4):
b = h.read(1) b = h.read(1)
v_vec.insert(0, unpack("B",b)[0]) v_vec.insert(0, unpack("B", b)[0])
self.version = tuple(v_vec) self.version = tuple(v_vec)
while True: while True:
stype= SegmentType(unpack("B", h.read(1))[0]) stype = SegmentType(unpack("B", h.read(1))[0])
if stype == SegmentType.EndMarker: if stype == SegmentType.EndMarker:
break break
else: else:
seg_size = unpack("<H", h.read(2))[0] seg_size = unpack("<H", h.read(2))[0]
seg_payload = h.read(seg_size) seg_payload = h.read(seg_size)
expected_checksum = WavDolbyMetadataReader\ expected_checksum = WavDolbyMetadataReader\
.segment_checksum(seg_payload, seg_size) .segment_checksum(seg_payload, seg_size)
checksum = unpack("B", h.read(1))[0] checksum = unpack("B", h.read(1))[0]
segment = seg_payload segment = seg_payload
@@ -640,34 +626,34 @@ class WavDolbyMetadataReader:
segment = DolbyAtmosSupplementalMetadata.load(segment) segment = DolbyAtmosSupplementalMetadata.load(segment)
self.segment_list\ self.segment_list\
.append((stype, checksum == expected_checksum, segment)) .append((stype, checksum == expected_checksum, segment))
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]: def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
""" """
Every valid Dolby Digital Plus metadata segment in the file. Every valid Dolby Digital Plus metadata segment in the file.
""" """
return [x[2] for x in self.segment_list \ return [x[2] for x in self.segment_list
if x[0] == SegmentType.DolbyDigitalPlus and x[1]] if x[0] == SegmentType.DolbyDigitalPlus and x[1]]
def dolby_atmos(self) -> List[DolbyAtmosMetadata]: def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
""" """
Every valid Dolby Atmos metadata segment in the file. Every valid Dolby Atmos metadata segment in the file.
""" """
return [x[2] for x in self.segment_list \ return [x[2] for x in self.segment_list
if x[0] == SegmentType.DolbyAtmos and x[1]] if x[0] == SegmentType.DolbyAtmos and x[1]]
def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]: def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
""" """
Every valid Dolby Atmos Supplemental metadata segment in the file. Every valid Dolby Atmos Supplemental metadata segment in the file.
""" """
return [x[2] for x in self.segment_list \ return [x[2] for x in self.segment_list
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]] if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
def to_dict(self) -> dict: def to_dict(self) -> dict:
ddp = map(lambda x: asdict(x), self.dolby_digital_plus()) ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
atmos = map(lambda x: asdict(x), self.dolby_atmos()) atmos = map(lambda x: asdict(x), self.dolby_atmos())
#atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental()) # atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
return dict(dolby_digital_plus=list(ddp), return dict(dolby_digital_plus=list(ddp),
dolby_atmos=list(atmos)) dolby_atmos=list(atmos))

View File

@@ -2,6 +2,7 @@ from .riff_parser import parse_chunk, ListChunkDescriptor
from typing import Optional from typing import Optional
class WavInfoChunkReader: class WavInfoChunkReader:
def __init__(self, f, encoding): def __init__(self, f, encoding):
@@ -12,49 +13,49 @@ class WavInfoChunkReader:
assert type(parsed_chunks) == ListChunkDescriptor assert type(parsed_chunks) == ListChunkDescriptor
list_chunks = [chunk for chunk in parsed_chunks.children list_chunks = [chunk for chunk in parsed_chunks.children
if type(chunk) is ListChunkDescriptor] if type(chunk) is ListChunkDescriptor]
self.info_chunk = next((chunk for chunk in list_chunks self.info_chunk = next((chunk for chunk in list_chunks
if chunk.signature == b'INFO'), None) if chunk.signature == b'INFO'), None)
#: 'ICOP' Copyright #: 'ICOP' Copyright
self.copyright : Optional[str] = self._get_field(f, b'ICOP') self.copyright: Optional[str] = self._get_field(f, b'ICOP')
#: 'IPRD' Product #: 'IPRD' Product
self.product : Optional[str]= self._get_field(f, b'IPRD') self.product: Optional[str] = self._get_field(f, b'IPRD')
self.album : Optional[str] = self.product self.album: Optional[str] = self.product
#: 'IGNR' Genre #: 'IGNR' Genre
self.genre : Optional[str] = self._get_field(f, b'IGNR') self.genre: Optional[str] = self._get_field(f, b'IGNR')
#: 'ISBJ' Subject #: 'ISBJ' Subject
self.subject : Optional[str] = self._get_field(f, b'ISBJ') self.subject: Optional[str] = self._get_field(f, b'ISBJ')
#: 'IART' Artist, composer, author #: 'IART' Artist, composer, author
self.artist : Optional[str] = self._get_field(f, b'IART') self.artist: Optional[str] = self._get_field(f, b'IART')
#: 'ICMT' Comment #: 'ICMT' Comment
self.comment : Optional[str] = self._get_field(f, b'ICMT') self.comment: Optional[str] = self._get_field(f, b'ICMT')
#: 'ISFT' Software, encoding application #: 'ISFT' Software, encoding application
self.software : Optional[str] = self._get_field(f, b'ISFT') self.software: Optional[str] = self._get_field(f, b'ISFT')
#: 'ICRD' Created date #: 'ICRD' Created date
self.created_date : Optional[str] = self._get_field(f, b'ICRD') self.created_date: Optional[str] = self._get_field(f, b'ICRD')
#: 'IENG' Engineer #: 'IENG' Engineer
self.engineer : Optional[str] = self._get_field(f, b'IENG') self.engineer: Optional[str] = self._get_field(f, b'IENG')
#: 'ITCH' Technician #: 'ITCH' Technician
self.technician : Optional[str] = self._get_field(f, b'ITCH') self.technician: Optional[str] = self._get_field(f, b'ITCH')
#: 'IKEY' Keywords, keyword list #: 'IKEY' Keywords, keyword list
self.keywords : Optional[str] = self._get_field(f, b'IKEY') self.keywords: Optional[str] = self._get_field(f, b'IKEY')
#: 'INAM' Name, title #: 'INAM' Name, title
self.title : Optional[str] = self._get_field(f, b'INAM') self.title: Optional[str] = self._get_field(f, b'INAM')
#: 'ISRC' Source #: 'ISRC' Source
self.source : Optional[str] = self._get_field(f, b'ISRC') self.source: Optional[str] = self._get_field(f, b'ISRC')
#: 'TAPE' Tape #: 'TAPE' Tape
self.tape : Optional[str] = self._get_field(f, b'TAPE') self.tape: Optional[str] = self._get_field(f, b'TAPE')
#: 'IARL' Archival Location #: 'IARL' Archival Location
self.archival_location : Optional[str] = self._get_field(f, b'IARL') self.archival_location: Optional[str] = self._get_field(f, b'IARL')
#: 'ICSM' Commissioned #: 'ICSM' Commissioned
self.commissioned : Optional[str] = self._get_field(f, b'ICMS') self.commissioned: Optional[str] = self._get_field(f, b'ICMS')
def _get_field(self, f, field_ident) -> Optional[str]: def _get_field(self, f, field_ident) -> Optional[str]:
search = next(((chunk.start, chunk.length) \ search = next(((chunk.start, chunk.length)
for chunk in self.info_chunk.children \ for chunk in self.info_chunk.children
if chunk.ident == field_ident), if chunk.ident == field_ident),
None) None)
if search is not None: if search is not None:
@@ -64,7 +65,7 @@ class WavInfoChunkReader:
else: else:
return None return None
def to_dict(self) -> dict: #FIXME should be asdict def to_dict(self) -> dict: # FIXME should be asdict
""" """
A dictionary with all of the key/values read from the INFO scope. A dictionary with all of the key/values read from the INFO scope.
""" """

View File

@@ -12,6 +12,7 @@ class IXMLTrack(NamedTuple):
name: str name: str
function: str function: str
class SteinbergMetadata: class SteinbergMetadata:
""" """
Vendor-specific Steinberg metadata. Vendor-specific Steinberg metadata.
@@ -34,7 +35,7 @@ class SteinbergMetadata:
CINE_71 = 27 CINE_71 = 27
SDDS_70 = 24 SDDS_70 = 24
SDDS_71 = 26 SDDS_71 = 26
MUSIC_60 = 21 #?? MUSIC_60 = 21 # ??
MUSIC_61 = 23 MUSIC_61 = 23
ATMOS_712 = 33 ATMOS_712 = 33
ATMOS_504 = 35 ATMOS_504 = 35
@@ -78,7 +79,7 @@ class SteinbergMetadata:
`AudioSpeakerArrangement` property `AudioSpeakerArrangement` property
""" """
val = self.parsed.find( val = self.parsed.find(
"./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE") "./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE")
if val is not None: if val is not None:
return type(self).AudioSpeakerArrangement(int(val.text)) return type(self).AudioSpeakerArrangement(int(val.text))
@@ -88,7 +89,7 @@ class SteinbergMetadata:
AudioSampleFormatSize AudioSampleFormatSize
""" """
val = self.parsed.find( val = self.parsed.find(
"./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE") "./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
if val is not None: if val is not None:
return int(val.text) return int(val.text)
@@ -98,7 +99,7 @@ class SteinbergMetadata:
MediaCompany MediaCompany
""" """
val = self.parsed.find( val = self.parsed.find(
"./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE") "./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
if val is not None: if val is not None:
return val.text return val.text
@@ -108,7 +109,7 @@ class SteinbergMetadata:
MediaDropFrames MediaDropFrames
""" """
val = self.parsed.find( val = self.parsed.find(
"./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE") "./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
if val is not None: if val is not None:
return val.text == "1" return val.text == "1"
@@ -118,7 +119,7 @@ class SteinbergMetadata:
MediaDuration MediaDuration
""" """
val = self.parsed.find( val = self.parsed.find(
"./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE") "./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
if val is not None: if val is not None:
return float(val.text) return float(val.text)
@@ -155,6 +156,7 @@ class WavIXMLFormat:
""" """
iXML recorder metadata. iXML recorder metadata.
""" """
def __init__(self, xml): def __init__(self, xml):
""" """
Parse iXML. Parse iXML.
@@ -163,7 +165,7 @@ class WavIXMLFormat:
self.source = xml self.source = xml
xml_bytes = io.BytesIO(xml) xml_bytes = io.BytesIO(xml)
parser = ET.XMLParser(recover=True) parser = ET.XMLParser(recover=True)
self.parsed : ET.ElementTree = ET.parse(xml_bytes, parser=parser) self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser)
def _get_text_value(self, xpath) -> Optional[str]: def _get_text_value(self, xpath) -> Optional[str]:
e = self.parsed.find("./" + xpath) e = self.parsed.find("./" + xpath)
@@ -192,15 +194,12 @@ class WavIXMLFormat:
for track in self.parsed.find("./TRACK_LIST").iter(): for track in self.parsed.find("./TRACK_LIST").iter():
if track.tag == 'TRACK': if track.tag == 'TRACK':
yield IXMLTrack( yield IXMLTrack(
channel_index= channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
track.xpath('string(CHANNEL_INDEX/text())'), interleave_index=track.xpath(
interleave_index= 'string(INTERLEAVE_INDEX/text())'),
track.xpath('string(INTERLEAVE_INDEX/text())'), name=track.xpath('string(NAME/text())'),
name= function=track.xpath('string(FUNCTION/text())')
track.xpath('string(NAME/text())'), )
function=
track.xpath('string(FUNCTION/text())')
)
@property @property
def project(self) -> Optional[str]: def project(self) -> Optional[str]:
@@ -217,7 +216,7 @@ class WavIXMLFormat:
return self._get_text_value("SCENE") return self._get_text_value("SCENE")
@property @property
def take(self) -> Optional[str]: def take(self) -> Optional[str]:
""" """
Take number. Take number.
""" """
@@ -257,7 +256,7 @@ class WavIXMLFormat:
def to_dict(self): def to_dict(self):
return dict( return dict(
track_list=list(map(lambda x: x._asdict(), self.track_list)), track_list=list(map(lambda x: x._asdict(), self.track_list)),
project=self.project, scene=self.scene, take=self.take, project=self.project, scene=self.scene, take=self.take,
tape=self.tape, family_uid=self.family_uid, tape=self.tape, family_uid=self.family_uid,
family_name=self.family_name ) family_name=self.family_name)

View File

@@ -1,4 +1,4 @@
#-*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import struct import struct
import os import os
from typing import Optional, Generator, Any, NamedTuple from typing import Optional, Generator, Any, NamedTuple
@@ -14,6 +14,8 @@ from .wave_dbmd_reader import WavDolbyMetadataReader
from .wave_cues_reader import WavCuesReader from .wave_cues_reader import WavCuesReader
#: Calculated statistics about the audio data. #: Calculated statistics about the audio data.
class WavDataDescriptor(NamedTuple): class WavDataDescriptor(NamedTuple):
byte_count: int byte_count: int
frame_count: int frame_count: int
@@ -28,6 +30,7 @@ class WavAudioFormat(NamedTuple):
block_align: int block_align: int
bits_per_sample: int bits_per_sample: int
class WavInfoReader: class WavInfoReader:
""" """
Parse a WAV audio file for metadata. Parse a WAV audio file for metadata.
@@ -55,28 +58,28 @@ class WavInfoReader:
self.bext_encoding = bext_encoding self.bext_encoding = bext_encoding
#: Wave audio data format. #: Wave audio data format.
self.fmt :Optional[WavAudioFormat] = None self.fmt: Optional[WavAudioFormat] = None
#: Statistics of the `data` section. #: Statistics of the `data` section.
self.data :Optional[WavDataDescriptor] = None self.data: Optional[WavDataDescriptor] = None
#: Broadcast-Wave metadata. #: Broadcast-Wave metadata.
self.bext :Optional[WavBextReader] = None self.bext: Optional[WavBextReader] = None
#: iXML metadata. #: iXML metadata.
self.ixml :Optional[WavIXMLFormat] = None self.ixml: Optional[WavIXMLFormat] = None
#: ADM Audio Definiton Model metadata. #: ADM Audio Definiton Model metadata.
self.adm :Optional[WavADMReader]= None self.adm: Optional[WavADMReader] = None
#: Dolby bitstream metadata. #: Dolby bitstream metadata.
self.dolby :Optional[WavDolbyMetadataReader] = None self.dolby: Optional[WavDolbyMetadataReader] = None
#: RIFF INFO metadata. #: RIFF INFO metadata.
self.info :Optional[WavInfoChunkReader]= None self.info: Optional[WavInfoChunkReader] = None
#: RIFF cues markers, labels, and notes. #: RIFF cues markers, labels, and notes.
self.cues :Optional[WavCuesReader] = None self.cues: Optional[WavCuesReader] = None
if hasattr(f, 'read'): if hasattr(f, 'read'):
self.get_wav_info(f) self.get_wav_info(f)
@@ -104,7 +107,7 @@ class WavInfoReader:
self.fmt = self._get_format(wavfile) self.fmt = self._get_format(wavfile)
self.bext = self._get_bext(wavfile, encoding=self.bext_encoding) self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
self.ixml = self._get_ixml(wavfile) self.ixml = self._get_ixml(wavfile)
self.adm = self._get_adm(wavfile) self.adm = self._get_adm(wavfile)
self.info = self._get_info(wavfile, encoding=self.info_encoding) self.info = self._get_info(wavfile, encoding=self.info_encoding)
self.dolby = self._get_dbmd(wavfile) self.dolby = self._get_dbmd(wavfile)
self.cues = self._get_cue(wavfile) self.cues = self._get_cue(wavfile)
@@ -112,8 +115,8 @@ class WavInfoReader:
def _find_chunk_data(self, ident, from_stream, def _find_chunk_data(self, ident, from_stream,
default_none=False) -> Optional[bytes]: default_none=False) -> Optional[bytes]:
top_chunks = (chunk for chunk in self.main_list \ top_chunks = (chunk for chunk in self.main_list
if type(chunk) is ChunkDescriptor and chunk.ident == ident) if type(chunk) is ChunkDescriptor and chunk.ident == ident)
chunk_descriptor = next(top_chunks, None) \ chunk_descriptor = next(top_chunks, None) \
if default_none else next(top_chunks) if default_none else next(top_chunks)
@@ -122,15 +125,15 @@ class WavInfoReader:
if chunk_descriptor else None if chunk_descriptor else None
def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]: def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]:
top_chunks = (chunk for chunk in self.main_list \ top_chunks = (chunk for chunk in self.main_list
if type(chunk) is ListChunkDescriptor and \ if type(chunk) is ListChunkDescriptor and
chunk.signature == signature) chunk.signature == signature)
return next(top_chunks, None) return next(top_chunks, None)
def _describe_data(self): def _describe_data(self):
data_chunk = next(c for c in self.main_list \ data_chunk = next(c for c in self.main_list
if type(c) is ChunkDescriptor and c.ident == b'data') if type(c) is ChunkDescriptor and c.ident == b'data')
assert isinstance(self.fmt, WavAudioFormat) assert isinstance(self.fmt, WavAudioFormat)
return WavDataDescriptor( return WavDataDescriptor(
@@ -155,8 +158,8 @@ class WavInfoReader:
) )
def _get_info(self, f, encoding): def _get_info(self, f, encoding):
finder = (chunk.signature for chunk in self.main_list \ finder = (chunk.signature for chunk in self.main_list
if type(chunk) is ListChunkDescriptor) if type(chunk) is ListChunkDescriptor)
if b'INFO' in finder: if b'INFO' in finder:
return WavInfoChunkReader(f, encoding) return WavInfoChunkReader(f, encoding)
@@ -181,9 +184,9 @@ class WavInfoReader:
return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
def _get_cue(self, f): def _get_cue(self, f):
cue = next((cue_chunk for cue_chunk in self.main_list if \ cue = next((cue_chunk for cue_chunk in self.main_list if
type(cue_chunk) is ChunkDescriptor and \ type(cue_chunk) is ChunkDescriptor and
cue_chunk.ident == b'cue '), None) cue_chunk.ident == b'cue '), None)
adtl = self._find_list_chunk(b'adtl') adtl = self._find_list_chunk(b'adtl')
labls = [] labls = []
@@ -195,9 +198,10 @@ class WavInfoReader:
notes = [c for c in adtl.children if c.ident == b'note'] notes = [c for c in adtl.children if c.ident == b'note']
return WavCuesReader.read_all(f, cue, labls, ltxts, notes, return WavCuesReader.read_all(f, cue, labls, ltxts, notes,
fallback_encoding=self.info_encoding) fallback_encoding=self.info_encoding)
def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()" # FIXME: this should probably be named "iter()"
def walk(self) -> Generator[str, str, Any]:
""" """
Walk all of the available metadata fields. Walk all of the available metadata fields.
@@ -207,7 +211,7 @@ class WavInfoReader:
""" """
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues', scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues',
'dolby') 'dolby')
for scope in scopes: for scope in scopes:
if scope in ['fmt', 'data']: if scope in ['fmt', 'data']:
@@ -216,7 +220,8 @@ class WavInfoReader:
yield scope, field, attr.__getattribute__(field) yield scope, field, attr.__getattribute__(field)
else: else:
dict = self.__getattribute__(scope).to_dict() if self.__getattribute__(scope) else {} dict = self.__getattribute__(scope).to_dict(
) if self.__getattribute__(scope) else {}
for key in dict.keys(): for key in dict.keys():
yield scope, key, dict[key] yield scope, key, dict[key]

View File

@@ -12,20 +12,19 @@ def main():
parser.usage = "wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] <PATH> +" parser.usage = "wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] <PATH> +"
primaries = OptionGroup(parser, title="Search Predicates", primaries = OptionGroup(parser, title="Search Predicates",
description="Argument values can be globs, and are logically-AND'ed.") description="Argument values can be globs, and are logically-AND'ed.")
primaries.add_option("--scene", primaries.add_option("--scene",
help='Search for this scene', help='Search for this scene',
metavar='SCENE') metavar='SCENE')
primaries.add_option("--take", primaries.add_option("--take",
help='Search for this take', help='Search for this take',
metavar='TAKE') metavar='TAKE')
primaries.add_option("--desc", primaries.add_option("--desc",
help='Search descriptions', help='Search descriptions',
metavar='DESC') metavar='DESC')
(options, args) = parser.parse_args(sys.argv) (options, args) = parser.parse_args(sys.argv)