From 13d9cd4914e89ce6fe3f5c8acf106ac70dee9bf1 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 17 Sep 2026 09:57:52 -0700 Subject: [PATCH 1/6] Added tests to audioStreamFormat parsing ASF's may have zero or one channelformatrefs or packformatrefs. We were expecting one. --- src/wavinfo/wave_adm_reader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wavinfo/wave_adm_reader.py b/src/wavinfo/wave_adm_reader.py index 665aa73..c0ff3a7 100644 --- a/src/wavinfo/wave_adm_reader.py +++ b/src/wavinfo/wave_adm_reader.py @@ -134,13 +134,17 @@ class WavADMReader: ("audioStreamFormat[@audioStreamFormatID='%s']" "/audioChannelFormatIDRef") % stream_id, namespaces=nsmap) - channelformat_id = channelformatref_elem.text + + if channelformatref_elem: + channelformat_id = channelformatref_elem.text packformatref_elem = afext.find( ("audioStreamFormat[@audioStreamFormatID='%s']" "/audioPackFormatIDRef") % stream_id, namespaces=nsmap) - packformat_id = packformatref_elem.text + + if packformatref_elem: + packformat_id = packformatref_elem.text channelformat_elem = afext\ .find("audioChannelFormat[@audioChannelFormatID='%s']" From e6320e84c6231e5a4e666720966363d41fac825d Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 17 Sep 2026 10:19:11 -0700 Subject: [PATCH 2/6] Ruffification and fixed bug --- docs/source/conf.py | 56 ++++----- examples/demo.ipynb | 13 +- src/wavinfo/__init__.py | 3 +- src/wavinfo/__main__.py | 69 +++++------ src/wavinfo/rf64_parser.py | 23 ++-- src/wavinfo/riff_parser.py | 23 ++-- src/wavinfo/wave_adm_reader.py | 175 +++++++++++++++------------ src/wavinfo/wave_bext_reader.py | 41 ++++--- src/wavinfo/wave_cues_reader.py | 104 +++++++++------- src/wavinfo/wave_dbmd_reader.py | 207 +++++++++++++++++++------------- src/wavinfo/wave_info_reader.py | 98 ++++++++------- src/wavinfo/wave_ixml_reader.py | 38 +++--- src/wavinfo/wave_reader.py | 148 +++++++++++++---------- src/wavinfo/wave_smpl_reader.py | 72 +++++------ src/wavinfo/wavfind.py | 27 ++--- tests/__init__.py | 4 +- tests/test_adm.py | 9 +- tests/test_cue.py | 29 +++-- tests/test_dolby.py | 37 +++--- tests/test_main.py | 18 +-- tests/test_rf64.py | 4 +- tests/test_smpl.py | 1 + tests/test_walk.py | 22 ++-- tests/test_wave_parsing.py | 205 +++++++++++++++++-------------- tests/utils.py | 20 ++- 25 files changed, 787 insertions(+), 659 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 153c318..c0fdb6b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,15 +15,16 @@ # import importlib import os import sys -sys.path.insert(0, os.path.abspath('../../src')) + +sys.path.insert(0, os.path.abspath("../../src")) sys.path.insert(0, os.path.abspath("../../../src")) print(sys.path) # -- Project information ----------------------------------------------------- -project = u'wavinfo' -copyright = u'2018-2025, Jamie Hardt' -author = u'Jamie Hardt' +project = "wavinfo" +copyright = "2018-2025, Jamie Hardt" +author = "Jamie Hardt" # The short X.Y version version = "4.0" @@ -42,34 +43,34 @@ release = "4.0.0" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', + "sphinx.ext.autodoc", + "sphinx.ext.todo", + "sphinx.ext.coverage", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None @@ -80,7 +81,7 @@ pygments_style = None # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -91,7 +92,7 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -107,7 +108,7 @@ html_static_path = ['_static'] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'wavinfodoc' +htmlhelp_basename = "wavinfodoc" # -- Options for LaTeX output ------------------------------------------------ @@ -116,15 +117,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -134,8 +132,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'wavinfo.tex', u'wavinfo Documentation', - u'Jamie Hardt', 'manual'), + (master_doc, "wavinfo.tex", "wavinfo Documentation", "Jamie Hardt", "manual"), ] @@ -143,10 +140,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'wavinfo', u'wavinfo Documentation', - [author], 1) -] +man_pages = [(master_doc, "wavinfo", "wavinfo Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -155,9 +149,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'wavinfo', u'wavinfo Documentation', - author, 'wavinfo', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "wavinfo", + "wavinfo Documentation", + author, + "wavinfo", + "One line description of project.", + "Miscellaneous", + ), ] @@ -176,7 +176,7 @@ epub_title = project # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- diff --git a/examples/demo.ipynb b/examples/demo.ipynb index 7eaf2b6..cb39ae2 100644 --- a/examples/demo.ipynb +++ b/examples/demo.ipynb @@ -26,7 +26,7 @@ "source": [ "from wavinfo import WavInfoReader\n", "\n", - "path = '../tests/test_files/sounddevices/A101_1.WAV'\n", + "path = \"../tests/test_files/sounddevices/A101_1.WAV\"\n", "\n", "info = WavInfoReader(path)" ] @@ -113,7 +113,12 @@ } ], "source": [ - "(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)" + "(\n", + " info.fmt.sample_rate,\n", + " info.fmt.channel_count,\n", + " info.fmt.block_align,\n", + " info.fmt.bits_per_sample,\n", + ")" ] }, { @@ -271,7 +276,9 @@ ], "source": [ "path = \"../tests/test_files/cue_chunks/izotoperx_cues_test.wav\"\n", - "info = WavInfoReader(path, info_encoding=\"utf-8\") # iZotope RX seems to encode marker text as UTF-8\n", + "info = WavInfoReader(\n", + " path, info_encoding=\"utf-8\"\n", + ") # iZotope RX seems to encode marker text as UTF-8\n", "\n", "for cue in info.cues.each_cue():\n", " print(f\"Cue ID: {cue[0]}\")\n", diff --git a/src/wavinfo/__init__.py b/src/wavinfo/__init__.py index 1e4cd8c..f7fc1ad 100644 --- a/src/wavinfo/__init__.py +++ b/src/wavinfo/__init__.py @@ -2,8 +2,7 @@ Probe WAVE Files for iXML, Broadcast-WAVE and other metadata. """ -__all__ = ['WavInfoReader', 'WavInfoEOFError'] +__all__ = ["WavInfoReader", "WavInfoEOFError"] from .wave_reader import WavInfoReader from .riff_parser import WavInfoEOFError - diff --git a/src/wavinfo/__main__.py b/src/wavinfo/__main__.py index fe26f3f..5a57a6e 100644 --- a/src/wavinfo/__main__.py +++ b/src/wavinfo/__main__.py @@ -18,7 +18,7 @@ class MyJSONEncoder(json.JSONEncoder): if isinstance(o, Enum): return o._name_ elif isinstance(o, bytes): - return 'base64:' + b64encode(o).decode('ascii') + return "base64:" + b64encode(o).decode("ascii") else: return super().default(o) @@ -50,7 +50,7 @@ class MetaBrowser(Cmd): if isinstance(val, int): print(f" - {key}: {val}") elif isinstance(val, str): - print(f" - {key}: \"{val}\"") + print(f' - {key}: "{val}"') elif isinstance(val, dict): print(f" - {key}: Dict ({len(val)} keys)") elif isinstance(val, list): @@ -63,7 +63,7 @@ class MetaBrowser(Cmd): print(f" - {key}: Unknown") def do_ls(self, _): - 'List items at the current node: LS' + "List items at the current node: LS" root = self.cwd if isinstance(root, list): @@ -94,7 +94,7 @@ class MetaBrowser(Cmd): if argv[0] in self.cwd.keys(): self.path = self.path + [argv[0]] else: - print(f"Key \"{argv[0]}\" does not exist") + print(f'Key "{argv[0]}" does not exist') if len(self.path) > 0: self.prompt = "(" + "/".join(self.path) + ") " @@ -102,41 +102,40 @@ class MetaBrowser(Cmd): self.prompt = "(wavinfo) " def do_bye(self, _): - 'Exit the interactive browser: BYE' + "Exit the interactive browser: BYE" return True def main(): - version = importlib.metadata.version('wavinfo') + version = importlib.metadata.version("wavinfo") manpath = os.path.dirname(__file__) + "/man" parser = OptionParser() - parser.usage = 'wavinfo (--adm | --ixml) +' + parser.usage = "wavinfo (--adm | --ixml) +" # parser.add_option('--install-manpages', # help="Install manual pages for wavinfo", # default=False, # action='store_true') - parser.add_option('--man', - help="Read the manual and exit.", - default=False, - action='store_true') + parser.add_option( + "--man", help="Read the manual and exit.", default=False, action="store_true" + ) - parser.add_option('--adm', dest='adm', - help='Output ADM XML', - default=False, - action='store_true') + parser.add_option( + "--adm", dest="adm", help="Output ADM XML", default=False, action="store_true" + ) - parser.add_option('--ixml', dest='ixml', - help='Output iXML', - default=False, - action='store_true') + parser.add_option( + "--ixml", dest="ixml", help="Output iXML", default=False, action="store_true" + ) - parser.add_option('-i', - help='Read metadata with an interactive prompt', - default=False, - action='store_true') + parser.add_option( + "-i", + help="Read metadata with an interactive prompt", + default=False, + action="store_true", + ) (options, args) = parser.parse_args(sys.argv) @@ -149,6 +148,7 @@ def main(): if options.man: import shlex + print("Which man page?") print("1) wavinfo usage") print("7) General info on Wave file metadata") @@ -176,26 +176,27 @@ def main(): raise MissingDataError("ixml") else: ret_dict = { - 'filename': arg, - 'run_date': datetime.datetime.now().isoformat(), - 'application': f"wavinfo {version}", - 'scopes': {} + "filename": arg, + "run_date": datetime.datetime.now().isoformat(), + "application": f"wavinfo {version}", + "scopes": {}, } for scope, name, value in this_file.walk(): - if scope not in ret_dict['scopes'].keys(): - ret_dict['scopes'][scope] = {} + if scope not in ret_dict["scopes"].keys(): + ret_dict["scopes"][scope] = {} - ret_dict['scopes'][scope][name] = value + ret_dict["scopes"][scope][name] = value if options.i: interactive_dict.append(ret_dict) else: - 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: - print("MissingDataError: Missing metadata (%s) in file %s" % - (e, arg), file=sys.stderr) + print( + "MissingDataError: Missing metadata (%s) in file %s" % (e, arg), + file=sys.stderr, + ) continue except Exception as e: raise e diff --git a/src/wavinfo/rf64_parser.py b/src/wavinfo/rf64_parser.py index 5a46961..ccdb904 100644 --- a/src/wavinfo/rf64_parser.py +++ b/src/wavinfo/rf64_parser.py @@ -1,4 +1,5 @@ import struct + # from collections import namedtuple from typing import NamedTuple, Dict @@ -10,38 +11,38 @@ class RF64Context(NamedTuple): bigchunk_table: Dict[str, int] -def parse_rf64(stream, signature=b'RF64') -> RF64Context: +def parse_rf64(stream, signature=b"RF64") -> RF64Context: start = stream.tell() - assert stream.read(4) == b'WAVE' + assert stream.read(4) == b"WAVE" ds64_chunk = riff_parser.parse_chunk(stream) - assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \ + assert type(ds64_chunk) is riff_parser.ChunkDescriptor, ( f"Expected ds64 chunk here, found {type(ds64_chunk)}" + ) ds64_field_spec = "= ds64_fields_size riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec, ds64_data[0:ds64_fields_size] - ) + ) bigchunk_table = {} chunksize64format = "<4sL" # chunksize64size = struct.calcsize(chunksize64format) for _ in range(length_lookup_table): - bigname, bigsize = struct.unpack_from(chunksize64format, - ds64_data, - offset=ds64_fields_size) + bigname, bigsize = struct.unpack_from( + chunksize64format, ds64_data, offset=ds64_fields_size + ) bigchunk_table[bigname] = bigsize - bigchunk_table[b'data'] = data_size + bigchunk_table[b"data"] = data_size bigchunk_table[signature] = riff_size stream.seek(start, 0) - return RF64Context(sample_count=sample_count, - bigchunk_table=bigchunk_table) + return RF64Context(sample_count=sample_count, bigchunk_table=bigchunk_table) diff --git a/src/wavinfo/riff_parser.py b/src/wavinfo/riff_parser.py index 87ce6d9..8bb8289 100644 --- a/src/wavinfo/riff_parser.py +++ b/src/wavinfo/riff_parser.py @@ -12,7 +12,7 @@ class WavInfoEOFError(EOFError): class ListChunkDescriptor(NamedTuple): signature: bytes - children: List[Union['ChunkDescriptor', 'ListChunkDescriptor']] + children: List[Union["ChunkDescriptor", "ListChunkDescriptor"]] class ChunkDescriptor(NamedTuple): @@ -48,14 +48,15 @@ def parse_chunk(stream, rf64_context=None): if len(ident) != 4 or len(size_bytes) != 4: raise WavInfoEOFError(identifier=ident, chunk_start=header_start) - data_size = struct.unpack(' str: # honestly can't remember why I'm stripping nulls this way - first_null = next((index for index, byte in enumerate(b) - if byte == 0), None) + first_null = next( + (index for index, byte in enumerate(b) if byte == 0), None + ) trimmed = b if first_null is None else b[:first_null] decoded = trimmed.decode(encoding) return decoded @@ -91,18 +93,19 @@ class WavBextReader: # umid_str = None - return {'description': self.description, - 'originator': self.originator, - 'originator_ref': self.originator_ref, - 'originator_date': self.originator_date, - 'originator_time': self.originator_time, - 'time_reference': self.time_reference, - 'version': self.version, - 'umid': self.umid, - 'coding_history': self.coding_history, - 'loudness_value': self.loudness_value, - 'loudness_range': self.loudness_range, - 'max_true_peak': self.max_true_peak, - 'max_momentary_loudness': self.max_momentary_loudness, - 'max_shortterm_loudness': self.max_shortterm_loudness - } + return { + "description": self.description, + "originator": self.originator, + "originator_ref": self.originator_ref, + "originator_date": self.originator_date, + "originator_time": self.originator_time, + "time_reference": self.time_reference, + "version": self.version, + "umid": self.umid, + "coding_history": self.coding_history, + "loudness_value": self.loudness_value, + "loudness_range": self.loudness_range, + "max_true_peak": self.max_true_peak, + "max_momentary_loudness": self.max_momentary_loudness, + "max_shortterm_loudness": self.max_shortterm_loudness, + } diff --git a/src/wavinfo/wave_cues_reader.py b/src/wavinfo/wave_cues_reader.py index 9d6cfac..039832d 100644 --- a/src/wavinfo/wave_cues_reader.py +++ b/src/wavinfo/wave_cues_reader.py @@ -7,11 +7,12 @@ IBM Corporation and Microsoft Corporation https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf """ + from dataclasses import dataclass from .riff_parser import ChunkDescriptor from struct import unpack, calcsize -from typing import Optional, Tuple, NamedTuple, List, Dict, Any, Generator +from typing import Optional, Tuple, NamedTuple, List, Dict, Any, Generator #: Country Codes used in the RIFF standard to resolve locale. These codes #: appear in CSET and LTXT metadata. @@ -100,6 +101,7 @@ class CueEntry(NamedTuple): """ A ``cue`` element structure. """ + #: Cue "name" or id number name: int #: Cue position, as a frame count in the play order of the WAVE file. In @@ -118,29 +120,37 @@ class CueEntry(NamedTuple): return calcsize(cls.Format) @classmethod - def read(cls, data: bytes) -> 'CueEntry': - assert len(data) == cls.format_size(), \ - (f"cue data size incorrect, expected {calcsize(cls.Format)} " - "found {len(data)}") + def read(cls, data: bytes) -> "CueEntry": + assert len(data) == cls.format_size(), ( + f"cue data size incorrect, expected {calcsize(cls.Format)} " + "found {len(data)}" + ) parsed = unpack(cls.Format, data) - return cls(name=parsed[0], position=parsed[1], chunk_id=parsed[2], - chunk_start=parsed[3], block_start=parsed[4], - sample_offset=parsed[5]) + return cls( + name=parsed[0], + position=parsed[1], + chunk_id=parsed[2], + chunk_start=parsed[3], + block_start=parsed[4], + sample_offset=parsed[5], + ) class LabelEntry(NamedTuple): """ A ``labl`` structure. """ + name: int text: str @classmethod def read(cls, data: bytes, encoding: str): - return cls(name=unpack(" 'WavCuesReader': - + def read_all( + cls, + f, + cues: Optional[ChunkDescriptor], + labls: List[ChunkDescriptor], + ltxts: List[ChunkDescriptor], + notes: List[ChunkDescriptor], + fallback_encoding: str, + ) -> "WavCuesReader": cue_list = [] if cues is not None: cues_data = cues.read_data(f) @@ -205,33 +223,31 @@ class WavCuesReader: cues_count = unpack(" Generator[Tuple[int, int], None, None]: """ @@ -242,8 +258,7 @@ class WavCuesReader: for cue in self.cues: yield (cue.name, cue.sample_offset) - def label_and_note(self, cue_ident: int) -> Tuple[Optional[str], - Optional[str]]: + def label_and_note(self, cue_ident: int) -> Tuple[Optional[str], Optional[str]]: """ Get the label and note (extended comment) for a cue. @@ -251,10 +266,10 @@ class WavCuesReader: :returns: a tuple of the the cue's label (if present) and note (if present) """ - label = next((label.text for label in self.labels - if label.name == cue_ident), None) - note = next((n.text for n in self.notes - if n.name == cue_ident), None) + label = next( + (label.text for label in self.labels if label.name == cue_ident), None + ) + note = next((n.text for n in self.notes if n.name == cue_ident), None) return (label, note) def range(self, cue_ident: int) -> Optional[int]: @@ -264,23 +279,22 @@ class WavCuesReader: :param cue_ident: the cue's name, its unique identifying number :returns: the length of the marker's range, or `None` """ - return next((r.length for r in self.ranges - if r.name == cue_ident), None) + return next((r.length for r in self.ranges if r.name == cue_ident), None) def to_dict(self) -> Dict[str, Any]: retval = dict() for n, t in self.each_cue(): retval[n] = dict() - retval[n]['frame'] = t + retval[n]["frame"] = t label, note = self.label_and_note(n) r = self.range(n) if label is not None: - retval[n]['label'] = label + retval[n]["label"] = label if note is not None: - retval[n]['note'] = note + retval[n]["note"] = note if r is not None: - retval[n]['length'] = r + retval[n]["length"] = r return retval diff --git a/src/wavinfo/wave_dbmd_reader.py b/src/wavinfo/wave_dbmd_reader.py index e255420..7336604 100644 --- a/src/wavinfo/wave_dbmd_reader.py +++ b/src/wavinfo/wave_dbmd_reader.py @@ -19,6 +19,7 @@ class SegmentType(IntEnum): """ Metadata segment type. """ + EndMarker = 0x0 DolbyE = 0x1 # Reserved2 = 0x2 @@ -29,7 +30,7 @@ class SegmentType(IntEnum): DolbyDigitalPlus = 0x7 AudioInfo = 0x8 DolbyAtmos = 0x9 - DolbyAtmosSupplemental = 0xa + DolbyAtmosSupplemental = 0xA @classmethod def _missing_(cls, val): @@ -82,6 +83,7 @@ class DolbyDigitalPlusMetadata: """ Dolby surround endcoding mode. """ + RESERVED = 0b11 IN_USE = 0b10 NOT_IN_USE = 0b01 @@ -126,6 +128,7 @@ class DolbyDigitalPlusMetadata: Dolby Digital Plus `acmod` field § 4.3.2.3 """ + RESERVED = 0b000 CH_ORD_1_0 = 0b001 "Mono" @@ -163,6 +166,7 @@ class DolbyDigitalPlusMetadata: Dolby Digital Plus `surmixlev` field § 4.3.3.2 """ + DOWN_3DB = 0b00 DOWN_6DB = 0b01 MUTE = 0b10 @@ -174,24 +178,28 @@ class DolbyDigitalPlusMetadata: Per ATSC/A52 § 5.4.2.12, this is not in use and always 0xFF. """ + pass class MixLevel(int): """ § 4.3.6.2 """ + pass class DialnormLevel(int): """ § 4.3.4.4 """ + pass class RoomType(Enum): """ `roomtyp` 4.3.6.3 """ + NOT_INDICATED = 0b00 LARGE_ROOM_X_CURVE = 0b01 SMALL_ROOM_FLAT_CURVE = 0b10 @@ -203,6 +211,7 @@ class DolbyDigitalPlusMetadata: should downmix. § 4.3.8.1 """ + NOT_INDICATED = 0b00 PRO_LOGIC = 0b01 STEREO = 0b10 @@ -213,6 +222,7 @@ class DolbyDigitalPlusMetadata: Dolby Surround-EX mode. `dsurexmod` § 4.3.9.1 """ + NOT_INDICATED = 0b00 NOT_SEX = 0b01 SEX = 0b10 @@ -222,6 +232,7 @@ class DolbyDigitalPlusMetadata: """ `dheadphonmod` § 4.3.9.2 """ + NOT_INDICATED = 0b00 NOT_DOLBY_HEADPHONE = 0b01 DOLBY_HEADPHONE = 0b10 @@ -246,6 +257,7 @@ class DolbyDigitalPlusMetadata: `compr1` RF compression profile § 4.3.10 (fig 42) """ + NONE = 0 FILM_STANDARD = 1 FILM_LIGHT = 2 @@ -341,9 +353,11 @@ class DolbyDigitalPlusMetadata: return b def program_info(b): - return (b & 0x40) > 0, \ - DolbyDigitalPlusMetadata.BitStreamMode(b & 0x38 >> 3), \ - DolbyDigitalPlusMetadata.AudioCodingMode(b & 0x7) + return ( + (b & 0x40) > 0, + DolbyDigitalPlusMetadata.BitStreamMode(b & 0x38 >> 3), + DolbyDigitalPlusMetadata.AudioCodingMode(b & 0x7), + ) def ddplus_reserved1(_): pass @@ -351,39 +365,49 @@ class DolbyDigitalPlusMetadata: def surround_config(b): return ( DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4), - DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2), - DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3) - ) + DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xC >> 2), + DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3), + ) def dialnorm_info(b): - return (b & 0x80) > 0, b & 0x40 > 0, b & 0x20 > 0, \ - DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f) + return ( + (b & 0x80) > 0, + b & 0x40 > 0, + b & 0x20 > 0, + DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1F), + ) def langcod(b) -> int: return b def audio_prod_info(b): - return (b & 0x80) > 0, \ - DolbyDigitalPlusMetadata.MixLevel(b & 0x7c >> 2), \ - DolbyDigitalPlusMetadata.RoomType(b & 0x3) + return ( + (b & 0x80) > 0, + DolbyDigitalPlusMetadata.MixLevel(b & 0x7C >> 2), + DolbyDigitalPlusMetadata.RoomType(b & 0x3), + ) # loro_center_downmix_level, loro_surround_downmix_level def ext_bsi1_word1(b): - return DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \ - DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7) + return DolbyDigitalPlusMetadata.DownMixLevelToken( + b & 0x38 >> 3 + ), DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7) # downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level def ext_bsi1_word2(b): - return DolbyDigitalPlusMetadata\ - .PreferredDownMixMode(b & 0xC0 >> 6), \ - DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \ - DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7) + return ( + DolbyDigitalPlusMetadata.PreferredDownMixMode(b & 0xC0 >> 6), + DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), + DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7), + ) # surround_ex_mode, dolby_headphone_encoded, ad_converter_type def ext_bsi2_word1(b): - return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \ - DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \ - DolbyDigitalPlusMetadata.ADConverterType(b & 0x4 >> 2) + return ( + DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), + DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), + DolbyDigitalPlusMetadata.ADConverterType(b & 0x4 >> 2), + ) def ddplus_reserved2(_): pass @@ -398,7 +422,7 @@ class DolbyDigitalPlusMetadata: pass def ddplus_info1(b): - return DolbyDigitalPlusMetadata.StreamDependency(b & 0xc >> 2) + return DolbyDigitalPlusMetadata.StreamDependency(b & 0xC >> 2) def ddplus_reserved4(_): pass @@ -412,19 +436,24 @@ class DolbyDigitalPlusMetadata: pid = program_id(buffer[0]) lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1]) ddplus_reserved1(buffer[2:2]) - center_downmix_level, surround_downmix_level, \ - dolby_surround_encoded = surround_config(buffer[4]) - langcode_present, copyright_bitstream, original_bitstream, \ - dialnorm = dialnorm_info(buffer[5]) + center_downmix_level, surround_downmix_level, dolby_surround_encoded = ( + surround_config(buffer[4]) + ) + langcode_present, copyright_bitstream, original_bitstream, dialnorm = ( + dialnorm_info(buffer[5]) + ) langcode = langcod(buffer[6]) prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7]) - loro_center_downmix_level, \ - loro_surround_downmix_level = ext_bsi1_word1(buffer[8]) - downmix_mode, ltrt_center_downmix_level, \ - ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9]) - surround_ex_mode, dolby_headphone_encoded, \ - ad_converter_type = ext_bsi2_word1(buffer[10]) + loro_center_downmix_level, loro_surround_downmix_level = ext_bsi1_word1( + buffer[8] + ) + downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level = ( + ext_bsi1_word2(buffer[9]) + ) + surround_ex_mode, dolby_headphone_encoded, ad_converter_type = ext_bsi2_word1( + buffer[10] + ) ddplus_reserved2(buffer[11:14]) compression = compr1(buffer[14]) @@ -436,32 +465,34 @@ class DolbyDigitalPlusMetadata: reserved(buffer[27:69]) return DolbyDigitalPlusMetadata( - program_id=pid, lfe_on=lfe_on, - bitstream_mode=bitstream_mode, - audio_coding_mode=audio_coding_mode, - center_downmix_level=center_downmix_level, - surround_downmix_level=surround_downmix_level, - dolby_surround_encoded=dolby_surround_encoded, - langcode_present=langcode_present, - copyright_bitstream=copyright_bitstream, - original_bitstream=original_bitstream, - dialnorm=dialnorm, - langcode=langcode, - prod_info_exists=prod_info_exists, - mixlevel=mixlevel, - roomtype=roomtype, - loro_center_downmix_level=loro_center_downmix_level, - loro_surround_downmix_level=loro_surround_downmix_level, - downmix_mode=downmix_mode, - ltrt_center_downmix_level=ltrt_center_downmix_level, - ltrt_surround_downmix_level=ltrt_surround_downmix_level, - surround_ex_mode=surround_ex_mode, - dolby_headphone_encoded=dolby_headphone_encoded, - ad_converter_type=ad_converter_type, - compression_profile=compression, - dynamic_range=dynamic_range, - stream_dependency=stream_info, - datarate_kbps=data_rate) + program_id=pid, + lfe_on=lfe_on, + bitstream_mode=bitstream_mode, + audio_coding_mode=audio_coding_mode, + center_downmix_level=center_downmix_level, + surround_downmix_level=surround_downmix_level, + dolby_surround_encoded=dolby_surround_encoded, + langcode_present=langcode_present, + copyright_bitstream=copyright_bitstream, + original_bitstream=original_bitstream, + dialnorm=dialnorm, + langcode=langcode, + prod_info_exists=prod_info_exists, + mixlevel=mixlevel, + roomtype=roomtype, + loro_center_downmix_level=loro_center_downmix_level, + loro_surround_downmix_level=loro_surround_downmix_level, + downmix_mode=downmix_mode, + ltrt_center_downmix_level=ltrt_center_downmix_level, + ltrt_surround_downmix_level=ltrt_surround_downmix_level, + surround_ex_mode=surround_ex_mode, + dolby_headphone_encoded=dolby_headphone_encoded, + ad_converter_type=ad_converter_type, + compression_profile=compression, + dynamic_range=dynamic_range, + stream_dependency=stream_info, + datarate_kbps=data_rate, + ) @dataclass @@ -488,7 +519,6 @@ class DolbyAtmosMetadata: @classmethod def load(cls, data: bytes): - assert len(data) == cls.SEGMENT_LENGTH # (f"DolbyAtmosMetadata segment is incorrect length, " # f"expected {cls.SEGMENT_LENGTH} actual was {len(data)}") @@ -498,7 +528,7 @@ class DolbyAtmosMetadata: h.seek(32, 1) toolname = h.read(cls.TOOL_NAME_LENGTH) toolname = unpack("%is" % cls.TOOL_NAME_LENGTH, toolname)[0] - toolname = toolname.decode('utf-8').strip('\0') + toolname = toolname.decode("utf-8").strip("\0") vers = h.read(3) major, minor, fix = unpack("BBB", vers) @@ -508,10 +538,11 @@ class DolbyAtmosMetadata: a_val = unpack("B", h.read(1))[0] warp_mode = a_val & 0x7 - return DolbyAtmosMetadata(tool_name=toolname, - tool_version=(major, minor, fix), - warp_mode=DolbyAtmosMetadata - .WarpMode(warp_mode)) + return DolbyAtmosMetadata( + tool_name=toolname, + tool_version=(major, minor, fix), + warp_mode=DolbyAtmosMetadata.WarpMode(warp_mode), + ) @dataclass @@ -531,15 +562,14 @@ class DolbyAtmosSupplementalMetadata: NOT_INDICATED = 0x04 object_count: int - render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode'] + render_modes: List["DolbyAtmosSupplementalMetadata.BinauralRenderMode"] trim_modes: List[int] - MAGIC = 0xf8726fbd + MAGIC = 0xF8726FBD TRIM_CONFIG_COUNT = 9 @classmethod def load(cls, data: bytes): - trim_modes = [] render_modes = [] @@ -564,9 +594,9 @@ class DolbyAtmosSupplementalMetadata: binaural_mode &= 0x7 render_modes.append(binaural_mode) - return DolbyAtmosSupplementalMetadata(object_count=object_count, - render_modes=render_modes, - trim_modes=trim_modes) + return DolbyAtmosSupplementalMetadata( + object_count=object_count, render_modes=render_modes, trim_modes=trim_modes + ) class WavDolbyMetadataReader: @@ -589,9 +619,9 @@ class WavDolbyMetadataReader: retval = size for b in bs: retval += int(b) - retval &= 0xff + retval &= 0xFF - retval = ((~retval) + 1) & 0xff + retval = ((~retval) + 1) & 0xFF return retval @@ -614,8 +644,9 @@ class WavDolbyMetadataReader: else: seg_size = unpack(" List[DolbyDigitalPlusMetadata]: """ Every valid Dolby Digital Plus metadata segment in the file. """ - return [x[2] for x in self.segment_list - if x[0] == SegmentType.DolbyDigitalPlus and x[1]] + return [ + x[2] + for x in self.segment_list + if x[0] == SegmentType.DolbyDigitalPlus and x[1] + ] def dolby_atmos(self) -> List[DolbyAtmosMetadata]: """ Every valid Dolby Atmos metadata segment in the file. """ - return [x[2] for x in self.segment_list - if x[0] == SegmentType.DolbyAtmos and x[1]] + return [ + x[2] for x in self.segment_list if x[0] == SegmentType.DolbyAtmos and x[1] + ] def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]: """ Every valid Dolby Atmos Supplemental metadata segment in the file. """ - return [x[2] for x in self.segment_list - if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]] + return [ + x[2] + for x in self.segment_list + if x[0] == SegmentType.DolbyAtmosSupplemental and x[1] + ] def to_dict(self) -> dict: - ddp = map(lambda x: asdict(x), self.dolby_digital_plus()) atmos = map(lambda x: asdict(x), self.dolby_atmos()) # atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental()) - return dict(dolby_digital_plus=list(ddp), - dolby_atmos=list(atmos)) + return dict(dolby_digital_plus=list(ddp), dolby_atmos=list(atmos)) diff --git a/src/wavinfo/wave_info_reader.py b/src/wavinfo/wave_info_reader.py index 34b9b96..2b62ff5 100644 --- a/src/wavinfo/wave_info_reader.py +++ b/src/wavinfo/wave_info_reader.py @@ -4,7 +4,6 @@ from typing import Optional class WavInfoChunkReader: - def __init__(self, f, encoding): self.encoding = encoding @@ -12,56 +11,64 @@ class WavInfoChunkReader: parsed_chunks = parse_chunk(f) assert type(parsed_chunks) is ListChunkDescriptor - list_chunks = [chunk for chunk in parsed_chunks.children - if type(chunk) is ListChunkDescriptor] + list_chunks = [ + chunk + for chunk in parsed_chunks.children + if type(chunk) is ListChunkDescriptor + ] - self.info_chunk = next((chunk for chunk in list_chunks - if chunk.signature == b'INFO'), None) + self.info_chunk = next( + (chunk for chunk in list_chunks if chunk.signature == b"INFO"), None + ) #: 'ICOP' Copyright - self.copyright: Optional[str] = self._get_field(f, b'ICOP') + self.copyright: Optional[str] = self._get_field(f, b"ICOP") #: '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 #: 'IGNR' Genre - self.genre: Optional[str] = self._get_field(f, b'IGNR') + self.genre: Optional[str] = self._get_field(f, b"IGNR") #: '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 - self.artist: Optional[str] = self._get_field(f, b'IART') + self.artist: Optional[str] = self._get_field(f, b"IART") #: '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 - self.software: Optional[str] = self._get_field(f, b'ISFT') + self.software: Optional[str] = self._get_field(f, b"ISFT") #: '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 - self.engineer: Optional[str] = self._get_field(f, b'IENG') + self.engineer: Optional[str] = self._get_field(f, b"IENG") #: '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 - self.keywords: Optional[str] = self._get_field(f, b'IKEY') + self.keywords: Optional[str] = self._get_field(f, b"IKEY") #: '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 - self.source: Optional[str] = self._get_field(f, b'ISRC') + self.source: Optional[str] = self._get_field(f, b"ISRC") #: '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 - self.archival_location: Optional[str] = self._get_field(f, b'IARL') + self.archival_location: Optional[str] = self._get_field(f, b"IARL") #: '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]: - search = next(((chunk.start, chunk.length) - for chunk in self.info_chunk.children - if chunk.ident == field_ident), - None) + search = next( + ( + (chunk.start, chunk.length) + for chunk in self.info_chunk.children + if chunk.ident == field_ident + ), + None, + ) if search is not None: f.seek(search[0]) data = f.read(search[1]) - return data.decode(self.encoding).rstrip('\0') + return data.decode(self.encoding).rstrip("\0") else: return None @@ -69,26 +76,27 @@ class WavInfoChunkReader: """ A dictionary with all of the key/values read from the INFO scope. """ - return {'copyright': self.copyright, - 'product': self.product, - 'album': self.album, - 'genre': self.genre, - 'artist': self.artist, - 'comment': self.comment, - 'software': self.software, - 'created_date': self.created_date, - 'engineer': self.engineer, - 'keywords': self.keywords, - 'title': self.title, - 'source': self.source, - 'tape': self.tape, - 'commissioned': self.commissioned, - 'archival_location': self.archival_location, - 'subject': self.subject, - 'technician': self.technician - } + return { + "copyright": self.copyright, + "product": self.product, + "album": self.album, + "genre": self.genre, + "artist": self.artist, + "comment": self.comment, + "software": self.software, + "created_date": self.created_date, + "engineer": self.engineer, + "keywords": self.keywords, + "title": self.title, + "source": self.source, + "tape": self.tape, + "commissioned": self.commissioned, + "archival_location": self.archival_location, + "subject": self.subject, + "technician": self.technician, + } def __repr__(self): return_val = self.to_dict() - return_val.update({'encoding': self.encoding}) + return_val.update({"encoding": self.encoding}) return str(return_val) diff --git a/src/wavinfo/wave_ixml_reader.py b/src/wavinfo/wave_ixml_reader.py index 5f3b4df..2278fc0 100644 --- a/src/wavinfo/wave_ixml_reader.py +++ b/src/wavinfo/wave_ixml_reader.py @@ -1,5 +1,6 @@ from lxml import etree as ET import io + # from collections import namedtuple from typing import Optional from enum import IntEnum @@ -22,6 +23,7 @@ class SteinbergMetadata: """ Steinberg speaker format enumeration. """ + MONO = 0 STEREO = 1 LRC = 10 @@ -79,7 +81,8 @@ class SteinbergMetadata: `AudioSpeakerArrangement` property """ val = self.parsed.find( - "./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE") + "./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE" + ) if val is not None: return type(self).AudioSpeakerArrangement(int(val.text)) @@ -88,8 +91,7 @@ class SteinbergMetadata: """ AudioSampleFormatSize """ - val = self.parsed.find( - "./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE") + val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE") if val is not None: return int(val.text) @@ -98,8 +100,7 @@ class SteinbergMetadata: """ MediaCompany """ - val = self.parsed.find( - "./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE") + val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE") if val is not None: return val.text @@ -108,8 +109,7 @@ class SteinbergMetadata: """ MediaDropFrames """ - val = self.parsed.find( - "./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE") + val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE") if val is not None: return val.text == "1" @@ -118,8 +118,7 @@ class SteinbergMetadata: """ MediaDuration """ - val = self.parsed.find( - "./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE") + val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE") if val is not None: return float(val.text) @@ -192,13 +191,12 @@ class WavIXMLFormat: :yields: `IXMLTrack` for each track. """ for track in self.parsed.find("./TRACK_LIST").iter(): - if track.tag == 'TRACK': + if track.tag == "TRACK": yield IXMLTrack( - channel_index=track.xpath('string(CHANNEL_INDEX/text())'), - interleave_index=track.xpath( - 'string(INTERLEAVE_INDEX/text())'), - name=track.xpath('string(NAME/text())'), - function=track.xpath('string(FUNCTION/text())') + channel_index=track.xpath("string(CHANNEL_INDEX/text())"), + interleave_index=track.xpath("string(INTERLEAVE_INDEX/text())"), + name=track.xpath("string(NAME/text())"), + function=track.xpath("string(FUNCTION/text())"), ) @property @@ -258,6 +256,10 @@ class WavIXMLFormat: def to_dict(self): return dict( track_list=list(map(lambda x: x._asdict(), self.track_list)), - project=self.project, scene=self.scene, take=self.take, - tape=self.tape, family_uid=self.family_uid, - family_name=self.family_name) + project=self.project, + scene=self.scene, + take=self.take, + tape=self.tape, + family_uid=self.family_uid, + family_name=self.family_name, + ) diff --git a/src/wavinfo/wave_reader.py b/src/wavinfo/wave_reader.py index ca41c80..a05a0bc 100644 --- a/src/wavinfo/wave_reader.py +++ b/src/wavinfo/wave_reader.py @@ -38,7 +38,7 @@ class WavInfoReader: Parse a WAV audio file for metadata. """ - def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'): + def __init__(self, path, info_encoding="latin_1", bext_encoding="ascii"): """ Create a new reader object. @@ -86,9 +86,9 @@ class WavInfoReader: #: Sampler `smpl` metadata self.smpl: Optional[WavSmplReader] = None - if hasattr(path, 'read'): + if hasattr(path, "read"): self.get_wav_info(path) - self.url = 'about:blank' + self.url = "about:blank" self.path = repr(path) else: @@ -99,7 +99,7 @@ class WavInfoReader: self.path = absolute_path - with open(path, 'rb') as path: + with open(path, "rb") as path: self.get_wav_info(path) def get_wav_info(self, wavfile): @@ -119,36 +119,43 @@ class WavInfoReader: self.smpl = self._get_sampler_loops(wavfile) self.data = self._describe_data() - def _find_chunk_data(self, ident, from_stream, - default_none=False) -> Optional[bytes]: - top_chunks = (chunk for chunk in self.main_list - if type(chunk) is ChunkDescriptor and - chunk.ident == ident) + def _find_chunk_data( + self, ident, from_stream, default_none=False + ) -> Optional[bytes]: + top_chunks = ( + chunk + for chunk in self.main_list + if type(chunk) is ChunkDescriptor and chunk.ident == ident + ) - chunk_descriptor = next(top_chunks, None) \ - if default_none else next(top_chunks) + chunk_descriptor = next(top_chunks, None) if default_none else next(top_chunks) - return chunk_descriptor.read_data(from_stream) \ - if chunk_descriptor else None + return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]: - top_chunks = (chunk for chunk in self.main_list - if type(chunk) is ListChunkDescriptor and - chunk.signature == signature) + top_chunks = ( + chunk + for chunk in self.main_list + if type(chunk) is ListChunkDescriptor and chunk.signature == signature + ) return next(top_chunks, None) def _describe_data(self): - data_chunk = next(c for c in self.main_list - if type(c) is ChunkDescriptor and c.ident == b'data') + data_chunk = next( + c + for c in self.main_list + if type(c) is ChunkDescriptor and c.ident == b"data" + ) assert isinstance(self.fmt, WavAudioFormat) return WavDataDescriptor( byte_count=data_chunk.length, - frame_count=int(data_chunk.length / self.fmt.block_align)) + frame_count=int(data_chunk.length / self.fmt.block_align), + ) def _get_format(self, f): - fmt_data = self._find_chunk_data(b'fmt ', f) + fmt_data = self._find_chunk_data(b"fmt ", f) assert fmt_data is not None, "Fmt data not found, not a valid wav file" packstring = " 0: self.sampler_udata = smpl_data[ - header_size + loop_size * loop_count: - header_size + loop_size * loop_count + sampler_udata_length] + header_size + loop_size * loop_count : header_size + + loop_size * loop_count + + sampler_udata_length + ] def to_dict(self): return { - 'manufactuer': self.manufacturer, - 'product': self.product, - 'sample_period_ns': self.sample_period_ns, - 'midi_note': self.midi_note, - 'midi_pitch_detune_cents': self.midi_pitch_detune_cents, - 'smpte_format': self.smpte_format, - 'smpte_offset': "%02i:%02i:%02i:%02i" % self.smpte_offset, - 'loops': [x.to_dict() for x in self.sample_loops], - 'sampler_user_data': self.sampler_udata, + "manufactuer": self.manufacturer, + "product": self.product, + "sample_period_ns": self.sample_period_ns, + "midi_note": self.midi_note, + "midi_pitch_detune_cents": self.midi_pitch_detune_cents, + "smpte_format": self.smpte_format, + "smpte_offset": "%02i:%02i:%02i:%02i" % self.smpte_offset, + "loops": [x.to_dict() for x in self.sample_loops], + "sampler_user_data": self.sampler_udata, } diff --git a/src/wavinfo/wavfind.py b/src/wavinfo/wavfind.py index 0b2aedf..7b9b348 100644 --- a/src/wavinfo/wavfind.py +++ b/src/wavinfo/wavfind.py @@ -1,6 +1,4 @@ -""" - -""" +""" """ from optparse import OptionParser, OptionGroup import sys @@ -9,24 +7,19 @@ import sys def main(): parser = OptionParser() - parser.usage = ("wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] " - " +") + parser.usage = "wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] +" - primaries = OptionGroup(parser, title="Search Predicates", - description="Argument values can be globs, " - "and are logically-AND'ed.") + primaries = OptionGroup( + parser, + title="Search Predicates", + description="Argument values can be globs, and are logically-AND'ed.", + ) - primaries.add_option("--scene", - help='Search for this scene', - metavar='SCENE') + primaries.add_option("--scene", help="Search for this scene", metavar="SCENE") - primaries.add_option("--take", - help='Search for this take', - metavar='TAKE') + primaries.add_option("--take", help="Search for this take", metavar="TAKE") - primaries.add_option("--desc", - help='Search descriptions', - metavar='DESC') + primaries.add_option("--desc", help="Search descriptions", metavar="DESC") (options, args) = parser.parse_args(sys.argv) diff --git a/tests/__init__.py b/tests/__init__.py index 339ed45..ba1231f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -19,7 +19,7 @@ from contextlib import contextmanager # @property # def arch_path(self): -# return os.path.join(self.base_path, self.test_file_arch_name) +# return os.path.join(self.base_path, self.test_file_arch_name) # @contextmanager # def open(self, name) -> Generator[BytesIO]: @@ -40,5 +40,3 @@ from contextlib import contextmanager # p = os.path.join(root, name) # zip.write(p) # os.unlink(p) - - diff --git a/tests/test_adm.py b/tests/test_adm.py index 9e9cbb5..5a4c279 100644 --- a/tests/test_adm.py +++ b/tests/test_adm.py @@ -2,8 +2,8 @@ from unittest import TestCase import wavinfo -class TestADMWave(TestCase): +class TestADMWave(TestCase): def setUp(self) -> None: self.protools_adm_wav = "tests/test_files/protools/Test_ADM_ProTools.wav" return super().setUp() @@ -24,7 +24,7 @@ class TestADMWave(TestCase): assert adm is not None dict = adm.to_dict() self.assertIsNotNone(dict) - + def test_programme(self): info = wavinfo.WavInfoReader(self.protools_adm_wav) adm = info.adm @@ -32,8 +32,8 @@ class TestADMWave(TestCase): pdict = adm.programme() self.assertIn("programme_id", pdict.keys()) self.assertIn("programme_name", pdict.keys()) - self.assertEqual(pdict['programme_id'], 'APR_1001') - self.assertEqual(pdict['programme_name'], 'Atmos_Master') + self.assertEqual(pdict["programme_id"], "APR_1001") + self.assertEqual(pdict["programme_name"], "Atmos_Master") self.assertIn("contents", pdict.keys()) self.assertEqual(len(pdict["contents"]), 3) @@ -51,4 +51,3 @@ class TestADMWave(TestCase): t10 = adm.track_info(10) self.assertTrue("content_name" in t10.keys()) self.assertEqual("Dialog", t10["content_name"]) - diff --git a/tests/test_cue.py b/tests/test_cue.py index 4c5e3fa..27e27e1 100644 --- a/tests/test_cue.py +++ b/tests/test_cue.py @@ -3,9 +3,10 @@ from glob import glob import wavinfo + class TestCue(TestCase): def setUp(self) -> None: - self.test_files = glob("tests/test_files/cue_chunks/*.wav") + self.test_files = glob("tests/test_files/cue_chunks/*.wav") return super().setUp() def test_enumerate(self): @@ -14,7 +15,7 @@ class TestCue(TestCase): self.assertIsNotNone(w1.cues) assert w1.cues is not None vals = list(w1.cues.each_cue()) - self.assertEqual(vals, [(1,29616),(2,74592),(3,121200)]) + self.assertEqual(vals, [(1, 29616), (2, 74592), (3, 121200)]) def test_labels_notes(self): file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav" @@ -23,7 +24,7 @@ class TestCue(TestCase): assert w1.cues is not None for name, _ in w1.cues.each_cue(): - self.assertIn(name,[1,2,3]) + self.assertIn(name, [1, 2, 3]) label, note = w1.cues.label_and_note(name) if name == 1: self.assertEqual("Marker 1", label) @@ -35,18 +36,20 @@ class TestCue(TestCase): self.assertIsNotNone(w1.cues) assert w1.cues is not None - self.assertEqual(w1.cues.range(3), 10000) + self.assertEqual(w1.cues.range(3), 10000) def test_encoding_fallback(self): """ Added this after I noticed that iZotope RX seems to just encode "notes" - as utf-8 without bothering to dump this info into the ltxt or + as utf-8 without bothering to dump this info into the ltxt or specifying an encoding by some other means. """ file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav" - w = wavinfo.WavInfoReader(file, info_encoding='utf-8') - expected = ("Лорем ипсум долор сит амет, тимеам вивендум хас ет, " - "цу адолесценс дефинитионес еам.") + w = wavinfo.WavInfoReader(file, info_encoding="utf-8") + expected = ( + "Лорем ипсум долор сит амет, тимеам вивендум хас ет, " + "цу адолесценс дефинитионес еам." + ) assert w.cues is not None note = [n for n in w.cues.notes if n.name == 3] @@ -56,20 +59,16 @@ class TestCue(TestCase): def test_label(self): file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav" w = wavinfo.WavInfoReader(file) - + self.assertIsNotNone(w.cues) - assert w.cues is not None + assert w.cues is not None self.assertEqual(len(w.cues.labels), 3) for label in w.cues.labels: - self.assertIn(label.name, [1,2,3]) + self.assertIn(label.name, [1, 2, 3]) if label.name == 1: self.assertEqual(label.text, "Marker 1") elif label.name == 2: self.assertEqual(label.text, "Marker 2") elif label.name == 3: self.assertEqual(label.text, "Marker 3") - - - - diff --git a/tests/test_dolby.py b/tests/test_dolby.py index 93793cc..9ad0c4b 100644 --- a/tests/test_dolby.py +++ b/tests/test_dolby.py @@ -3,6 +3,7 @@ from unittest import TestCase import wavinfo from wavinfo.wave_dbmd_reader import SegmentType, DolbyDigitalPlusMetadata + class TestDolby(TestCase): def setUp(self): self.test_file = "tests/test_files/protools/Test_ADM_ProTools.wav" @@ -10,19 +11,17 @@ class TestDolby(TestCase): def test_version(self): t1 = wavinfo.WavInfoReader(self.test_file) d = t1.dolby - + assert d is not None - self.assertEqual((1,0,0,6), d.version) + self.assertEqual((1, 0, 0, 6), d.version) def test_segments(self): t1 = wavinfo.WavInfoReader(self.test_file) d = t1.dolby assert d is not None - ddp = [x for x in d.segment_list \ - if x[0] == SegmentType.DolbyDigitalPlus] - atmos = [x for x in d.segment_list \ - if x[0] == SegmentType.DolbyAtmos] + ddp = [x for x in d.segment_list if x[0] == SegmentType.DolbyDigitalPlus] + atmos = [x for x in d.segment_list if x[0] == SegmentType.DolbyAtmos] self.assertEqual(len(ddp), 1) self.assertEqual(len(atmos), 1) @@ -40,21 +39,23 @@ class TestDolby(TestCase): d = t1.dolby assert d is not None ddp = d.dolby_digital_plus() - self.assertEqual(len(ddp), 1, - ("Failed to find exactly one Dolby Digital Plus " - "metadata segment") - ) + self.assertEqual( + len(ddp), + 1, + ("Failed to find exactly one Dolby Digital Plus metadata segment"), + ) + + self.assertTrue( + ddp[0].audio_coding_mode, + DolbyDigitalPlusMetadata.AudioCodingMode.CH_ORD_3_2, + ) + self.assertTrue(ddp[0].lfe_on) - self.assertTrue( ddp[0].audio_coding_mode, - DolbyDigitalPlusMetadata.AudioCodingMode.CH_ORD_3_2 ) - self.assertTrue( ddp[0].lfe_on) - def test_atmos(self): t1 = wavinfo.WavInfoReader(self.test_file) d = t1.dolby assert d is not None atmos = d.dolby_atmos() - self.assertEqual(len(atmos), 1, - "Failed to find exactly one Atmos metadata segment") - - + self.assertEqual( + len(atmos), 1, "Failed to find exactly one Atmos metadata segment" + ) diff --git a/tests/test_main.py b/tests/test_main.py index e1474b0..8e4e7e0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,10 +7,10 @@ from wavinfo.__main__ import main import sys import glob + class MainTest(unittest.TestCase): - - def test_empty_argv(self): - with patch.object(sys, 'argv', []): + def test_empty_argv(self): + with patch.object(sys, "argv", []): try: main() except: @@ -18,17 +18,17 @@ class MainTest(unittest.TestCase): def test_a_file(self): for path in glob.glob("tests/test_files/**/*.wav"): - with patch.object(sys, 'argv', ["TEST", path]): + with patch.object(sys, "argv", ["TEST", path]): try: main() except: - self.fail("main() throwing an exception") + self.fail("main() throwing an exception") def test_ixml(self): - with patch.object(sys, 'argv', - ['TEST', '--ixml', - 'tests/test_files/sounddevices/A101_1.WAV']): + with patch.object( + sys, "argv", ["TEST", "--ixml", "tests/test_files/sounddevices/A101_1.WAV"] + ): try: main() except: - self.fail("main() throwing an exception") + self.fail("main() throwing an exception") diff --git a/tests/test_rf64.py b/tests/test_rf64.py index e8f08de..1652f68 100644 --- a/tests/test_rf64.py +++ b/tests/test_rf64.py @@ -9,17 +9,15 @@ from unittest import TestCase import wavinfo -class TestRf64(TestCase): +class TestRf64(TestCase): def setUp(self) -> None: return super().setUp() def test_open(self): - for path in glob("tests/test_files/rf64/*.wav.gz"): gz = gzip.open(path) wav_info = wavinfo.WavInfoReader(gz) self.assertIsNotNone(wav_info) # self.assertIsNotNone(wav_info.bext) - diff --git a/tests/test_smpl.py b/tests/test_smpl.py index 0529440..9863c22 100644 --- a/tests/test_smpl.py +++ b/tests/test_smpl.py @@ -3,6 +3,7 @@ from glob import glob import wavinfo + class TestSmpl(TestCase): def setUp(self) -> None: self.test_files = glob("tests/test_files/smpl/*.wav") diff --git a/tests/test_walk.py b/tests/test_walk.py index 361766a..eeb257a 100644 --- a/tests/test_walk.py +++ b/tests/test_walk.py @@ -3,35 +3,35 @@ import wavinfo import glob + class TestWalk(unittest.TestCase): def test_walk_metadata(self): - test_file = 'tests/test_files/protools/PT A101_4.A1.wav' + test_file = "tests/test_files/protools/PT A101_4.A1.wav" info = wavinfo.WavInfoReader(test_file) - tested_data , tested_format = False, False + tested_data, tested_format = False, False for scope, key, value in info.walk(): - if scope == 'fmt': - if key == 'channel_count': + if scope == "fmt": + if key == "channel_count": tested_format = True self.assertEqual(value, 2) - if scope == 'data': - if key == 'frame_count': + if scope == "data": + if key == "frame_count": tested_data = True self.assertEqual(value, 144140) self.assertTrue(tested_data and tested_format) def test_walk_all(self): - for file in glob.glob('tests/test_files/**/*.wav'): + for file in glob.glob("tests/test_files/**/*.wav"): info = wavinfo.WavInfoReader(file) - + try: for _, _, _ in info.walk(): - pass + pass except: self.fail(f"Failed to walk metadata in file {file}") - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_wave_parsing.py b/tests/test_wave_parsing.py index 39127cf..7371646 100644 --- a/tests/test_wave_parsing.py +++ b/tests/test_wave_parsing.py @@ -13,9 +13,10 @@ class TestWaveInfo(TestCase): def test_sanity(self): for wav_file in all_files(): info = wavinfo.WavInfoReader(wav_file) - self.assertEqual(info.__repr__(), - 'WavInfoReader({}, latin_1, ascii)' - .format(os.path.abspath(wav_file))) + self.assertEqual( + info.__repr__(), + "WavInfoReader({}, latin_1, ascii)".format(os.path.abspath(wav_file)), + ) self.assertIsNotNone(info) def test_fmt_against_ffprobe(self): @@ -26,31 +27,39 @@ class TestWaveInfo(TestCase): assert info.fmt is not None assert ffprobe_info is not None - self.assertEqual(info.fmt.channel_count, - ffprobe_info['streams'][0]['channels']) - self.assertEqual(info.fmt.sample_rate, - int(ffprobe_info['streams'][0]['sample_rate'])) - self.assertEqual(info.fmt.bits_per_sample, - int(ffprobe_info['streams'][0]['bits_per_sample'] - )) + self.assertEqual( + info.fmt.channel_count, ffprobe_info["streams"][0]["channels"] + ) + self.assertEqual( + info.fmt.sample_rate, int(ffprobe_info["streams"][0]["sample_rate"]) + ) + self.assertEqual( + info.fmt.bits_per_sample, + int(ffprobe_info["streams"][0]["bits_per_sample"]), + ) if info.fmt.audio_format == 1: - self.assertTrue(ffprobe_info['streams'][0]['codec_name']\ - .startswith('pcm')) - streams = ffprobe_info['streams'][0] - byte_rate = int(streams['sample_rate']) * \ - streams['channels'] * \ - int(streams['bits_per_sample']) / 8 + self.assertTrue( + ffprobe_info["streams"][0]["codec_name"].startswith("pcm") + ) + streams = ffprobe_info["streams"][0] + byte_rate = ( + int(streams["sample_rate"]) + * streams["channels"] + * int(streams["bits_per_sample"]) + / 8 + ) self.assertEqual(info.fmt.byte_rate, byte_rate) def test_data_against_ffprobe(self): for wav_file in all_files(): info = wavinfo.WavInfoReader(wav_file) - ffprobe_info = cast(Dict[str,Any], ffprobe(wav_file)) + ffprobe_info = cast(Dict[str, Any], ffprobe(wav_file)) assert ffprobe_info is not None assert info.data is not None - self.assertEqual(info.data.frame_count, - int(ffprobe_info['streams'][0]['duration_ts'])) + self.assertEqual( + info.data.frame_count, int(ffprobe_info["streams"][0]["duration_ts"]) + ) def test_bext_against_ffprobe(self): for wav_file in all_files(): @@ -59,65 +68,78 @@ class TestWaveInfo(TestCase): assert ffprobe_info is not None if info.bext: - if 'comment' in ffprobe_info['format']['tags']: - self.assertEqual(info.bext.description, - ffprobe_info['format']['tags']\ - ['comment']) + if "comment" in ffprobe_info["format"]["tags"]: + self.assertEqual( + info.bext.description, ffprobe_info["format"]["tags"]["comment"] + ) else: - self.assertEqual(info.bext.description, '') + self.assertEqual(info.bext.description, "") - if 'encoded_by' in ffprobe_info['format']['tags']: - self.assertEqual(info.bext.originator, - ffprobe_info['format']['tags']\ - ['encoded_by']) + if "encoded_by" in ffprobe_info["format"]["tags"]: + self.assertEqual( + info.bext.originator, + ffprobe_info["format"]["tags"]["encoded_by"], + ) else: - self.assertEqual(info.bext.originator, '') + self.assertEqual(info.bext.originator, "") - if 'originator_reference' in ffprobe_info['format']['tags']: - self.assertEqual(info.bext.originator_ref, - ffprobe_info['format']['tags']\ - ['originator_reference']) + if "originator_reference" in ffprobe_info["format"]["tags"]: + self.assertEqual( + info.bext.originator_ref, + ffprobe_info["format"]["tags"]["originator_reference"], + ) else: - self.assertEqual(info.bext.originator_ref, '') + self.assertEqual(info.bext.originator_ref, "") # these don't always reflect the bext info - # self.assertEqual(info.bext.originator_date, + # self.assertEqual(info.bext.originator_date, # ffprobe_info['format']['tags']['date']) - # self.assertEqual(info.bext.originator_time, + # self.assertEqual(info.bext.originator_time, # ffprobe_info['format']['tags']['creation_time']) - self.assertEqual(info.bext.time_reference, - int(ffprobe_info['format']['tags']\ - ['time_reference'])) + self.assertEqual( + info.bext.time_reference, + int(ffprobe_info["format"]["tags"]["time_reference"]), + ) - if 'coding_history' in ffprobe_info['format']['tags']: - self.assertEqual(info.bext.coding_history, - ffprobe_info['format']['tags']\ - ['coding_history']) + if "coding_history" in ffprobe_info["format"]["tags"]: + self.assertEqual( + info.bext.coding_history, + ffprobe_info["format"]["tags"]["coding_history"], + ) else: - self.assertEqual(info.bext.coding_history, '') + self.assertEqual(info.bext.coding_history, "") def test_ixml(self): - expected = {'A101_4.WAV': {'project': 'BMH', - 'scene': 'A101', 'take': '4', - 'tape': '18Y12M31', - 'family_uid': - 'USSDVGR1112089007124015008231000'}, - 'A101_3.WAV': {'project': 'BMH', - 'scene': 'A101', 'take': '3', - 'tape': '18Y12M31', - 'family_uid': - 'USSDVGR1112089007124014008228300'}, - 'A101_2.WAV': {'project': 'BMH', - 'scene': 'A101', 'take': '2', - 'tape': '18Y12M31', - 'family_uid': - 'USSDVGR1112089007124004008218600'}, - 'A101_1.WAV': {'project': 'BMH', - 'scene': 'A101', 'take': '1', - 'tape': '18Y12M31', - 'family_uid': - 'USSDVGR1112089007124001008206300'}, - } + expected = { + "A101_4.WAV": { + "project": "BMH", + "scene": "A101", + "take": "4", + "tape": "18Y12M31", + "family_uid": "USSDVGR1112089007124015008231000", + }, + "A101_3.WAV": { + "project": "BMH", + "scene": "A101", + "take": "3", + "tape": "18Y12M31", + "family_uid": "USSDVGR1112089007124014008228300", + }, + "A101_2.WAV": { + "project": "BMH", + "scene": "A101", + "take": "2", + "tape": "18Y12M31", + "family_uid": "USSDVGR1112089007124004008218600", + }, + "A101_1.WAV": { + "project": "BMH", + "scene": "A101", + "take": "1", + "tape": "18Y12M31", + "family_uid": "USSDVGR1112089007124001008206300", + }, + } for wav_file in all_files(): basename = os.path.basename(wav_file) @@ -125,29 +147,31 @@ class TestWaveInfo(TestCase): info = wavinfo.WavInfoReader(wav_file) e = expected[basename] self.assertIsNotNone(info.ixml) - assert info.ixml is not None - self.assertEqual(e['project'], info.ixml.project) - self.assertEqual(e['scene'], info.ixml.scene) - self.assertEqual(e['take'], info.ixml.take) - self.assertEqual(e['tape'], info.ixml.tape) - self.assertEqual(e['family_uid'], info.ixml.family_uid) + assert info.ixml is not None + self.assertEqual(e["project"], info.ixml.project) + self.assertEqual(e["scene"], info.ixml.scene) + self.assertEqual(e["take"], info.ixml.take) + self.assertEqual(e["tape"], info.ixml.tape) + self.assertEqual(e["family_uid"], info.ixml.family_uid) for track in info.ixml.track_list: self.assertIsNotNone(track.channel_index) - if basename == 'A101_4.WAV' and track.channel_index == '1': - self.assertEqual(track.name, 'MKH516 A') + if basename == "A101_4.WAV" and track.channel_index == "1": + self.assertEqual(track.name, "MKH516 A") def test_steinberg_ixml(self): - nuendo_files = 'tests/test_files/nuendo/*.wav' + nuendo_files = "tests/test_files/nuendo/*.wav" for file in glob(nuendo_files): info = wavinfo.WavInfoReader(file) - assert info.ixml is not None + assert info.ixml is not None self.assertIsNotNone(info.ixml.steinberg) assert info.ixml.steinberg is not None self.assertIsNotNone(info.ixml.steinberg.audio_speaker_arrangement) - self.assertEqual(info.ixml.steinberg.sample_format_size, 3) - self.assertEqual(info.ixml.steinberg.media_company, - "https://github.com/iluvcapra/wavinfo") + self.assertEqual(info.ixml.steinberg.sample_format_size, 3) + self.assertEqual( + info.ixml.steinberg.media_company, + "https://github.com/iluvcapra/wavinfo", + ) self.assertFalse(info.ixml.steinberg.media_drop_frames) self.assertEqual(info.ixml.steinberg.media_duration, 1200.0) @@ -159,24 +183,21 @@ class TestWaveInfo(TestCase): self.assertIsNone(info.ixml.steinberg) def test_info_metadata(self): - file_with_metadata = \ - 'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav' + file_with_metadata = "tests/test_files/sound_grinder_pro/new_camera bumb 1.wav" self.assertTrue(os.path.exists(file_with_metadata)) info = wavinfo.WavInfoReader(file_with_metadata).info assert info is not None - self.assertEqual(info.title, 'camera bumb 1') - self.assertEqual(info.artist, 'Jamie Hardt') - self.assertEqual(info.copyright, '© 2010 Jamie Hardt') - self.assertEqual(info.product, 'Test Sounds') # album + self.assertEqual(info.title, "camera bumb 1") + self.assertEqual(info.artist, "Jamie Hardt") + self.assertEqual(info.copyright, "© 2010 Jamie Hardt") + self.assertEqual(info.product, "Test Sounds") # album self.assertEqual(info.album, info.product) - self.assertEqual(info.comment, 'Comments') - self.assertEqual(info.software, 'Sound Grinder Pro') - self.assertEqual(info.created_date, '2010-12-28') - self.assertEqual(info.engineer, 'JPH') - self.assertEqual(info.keywords, - 'Sound Effect, movement, microphone, bump') - self.assertEqual(info.title, 'camera bumb 1') + self.assertEqual(info.comment, "Comments") + self.assertEqual(info.software, "Sound Grinder Pro") + self.assertEqual(info.created_date, "2010-12-28") + self.assertEqual(info.engineer, "JPH") + self.assertEqual(info.keywords, "Sound Effect, movement, microphone, bump") + self.assertEqual(info.title, "camera bumb 1") self.assertEqual(type(info.to_dict()), dict) self.assertEqual(type(info.__repr__()), str) - diff --git a/tests/utils.py b/tests/utils.py index 17ae284..fb2706f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,35 +4,33 @@ import subprocess from subprocess import PIPE import json -FFPROBE = 'ffprobe' +FFPROBE = "ffprobe" def ffprobe(path): - arguments = [FFPROBE, "-of", "json", - "-show_format", "-show_streams", path] - if int(sys.version[0]) < 3: + arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path] + if int(sys.version[0]) < 3: process = subprocess.Popen(arguments, stdout=PIPE) process.wait() if process.returncode == 0: output = process.communicate()[0] if output: - output_str = output.decode('utf-8') + output_str = output.decode("utf-8") return json.loads(output_str) else: return None - else: - process = subprocess.run(arguments, stdin=None, - stdout=PIPE, stderr=PIPE) + else: + process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE) if process.returncode == 0: - output_str = process.stdout.decode('utf-8') + output_str = process.stdout.decode("utf-8") return json.loads(output_str) else: return None def all_files(): - for dirpath, _, filenames in os.walk('tests/test_files'): + for dirpath, _, filenames in os.walk("tests/test_files"): for filename in filenames: _, ext = os.path.splitext(filename) - if ext in ['.wav', '.WAV']: + if ext in [".wav", ".WAV"]: yield os.path.join(dirpath, filename) From f9de312610168171419c58b105d63b32e8cadc6a Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 17 Sep 2026 10:30:32 -0700 Subject: [PATCH 3/6] Added test file, Ruff pass --- src/wavinfo/__init__.py | 2 +- src/wavinfo/__main__.py | 16 ++++++------ src/wavinfo/rf64_parser.py | 2 +- src/wavinfo/riff_parser.py | 5 ++-- src/wavinfo/wave_bext_reader.py | 2 +- src/wavinfo/wave_cues_reader.py | 6 ++--- src/wavinfo/wave_dbmd_reader.py | 9 +++---- src/wavinfo/wave_info_reader.py | 4 +-- src/wavinfo/wave_ixml_reader.py | 8 +++--- src/wavinfo/wave_reader.py | 16 +++++------- src/wavinfo/wave_smpl_reader.py | 3 +-- src/wavinfo/wavfind.py | 2 +- tests/__init__.py | 42 ------------------------------ tests/test_cue.py | 2 +- tests/test_dolby.py | 2 +- tests/test_files/adm/test_bwf.wav | Bin 0 -> 153890 bytes tests/test_main.py | 18 ++++++------- tests/test_rf64.py | 3 +-- tests/test_smpl.py | 2 +- tests/test_walk.py | 10 +++---- tests/test_wave_parsing.py | 7 +++-- tests/test_zoom_f8.py | 8 +----- tests/utils.py | 8 +++--- 23 files changed, 61 insertions(+), 116 deletions(-) create mode 100644 tests/test_files/adm/test_bwf.wav diff --git a/src/wavinfo/__init__.py b/src/wavinfo/__init__.py index f7fc1ad..82e9cb0 100644 --- a/src/wavinfo/__init__.py +++ b/src/wavinfo/__init__.py @@ -4,5 +4,5 @@ Probe WAVE Files for iXML, Broadcast-WAVE and other metadata. __all__ = ["WavInfoReader", "WavInfoEOFError"] -from .wave_reader import WavInfoReader from .riff_parser import WavInfoEOFError +from .wave_reader import WavInfoReader diff --git a/src/wavinfo/__main__.py b/src/wavinfo/__main__.py index 5a57a6e..9d1ddf7 100644 --- a/src/wavinfo/__main__.py +++ b/src/wavinfo/__main__.py @@ -1,16 +1,16 @@ -from . import WavInfoReader - import datetime -from optparse import OptionParser -import sys -import os -import json -from enum import Enum import importlib.metadata +import json +import os +import sys from base64 import b64encode from cmd import Cmd +from enum import Enum +from optparse import OptionParser from shlex import split -from typing import List, Dict, Union +from typing import Dict, List, Union + +from . import WavInfoReader class MyJSONEncoder(json.JSONEncoder): diff --git a/src/wavinfo/rf64_parser.py b/src/wavinfo/rf64_parser.py index ccdb904..92d3937 100644 --- a/src/wavinfo/rf64_parser.py +++ b/src/wavinfo/rf64_parser.py @@ -1,7 +1,7 @@ import struct # from collections import namedtuple -from typing import NamedTuple, Dict +from typing import Dict, NamedTuple from . import riff_parser diff --git a/src/wavinfo/riff_parser.py b/src/wavinfo/riff_parser.py index 8bb8289..ab317b3 100644 --- a/src/wavinfo/riff_parser.py +++ b/src/wavinfo/riff_parser.py @@ -1,7 +1,8 @@ # from optparse import Option import struct -from .rf64_parser import parse_rf64, RF64Context -from typing import NamedTuple, Union, List, Optional +from typing import List, NamedTuple, Optional, Union + +from .rf64_parser import RF64Context, parse_rf64 class WavInfoEOFError(EOFError): diff --git a/src/wavinfo/wave_bext_reader.py b/src/wavinfo/wave_bext_reader.py index e10f723..56e7112 100644 --- a/src/wavinfo/wave_bext_reader.py +++ b/src/wavinfo/wave_bext_reader.py @@ -1,6 +1,6 @@ import struct -# from .umid_parser import UMIDParser +# from .umid_parser import UMIDParser from typing import Optional diff --git a/src/wavinfo/wave_cues_reader.py b/src/wavinfo/wave_cues_reader.py index 039832d..fbe51b5 100644 --- a/src/wavinfo/wave_cues_reader.py +++ b/src/wavinfo/wave_cues_reader.py @@ -9,10 +9,10 @@ https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf """ from dataclasses import dataclass -from .riff_parser import ChunkDescriptor +from struct import calcsize, unpack +from typing import Any, Dict, Generator, List, NamedTuple, Optional, Tuple -from struct import unpack, calcsize -from typing import Optional, Tuple, NamedTuple, List, Dict, Any, Generator +from .riff_parser import ChunkDescriptor #: Country Codes used in the RIFF standard to resolve locale. These codes #: appear in CSET and LTXT metadata. diff --git a/src/wavinfo/wave_dbmd_reader.py b/src/wavinfo/wave_dbmd_reader.py index 7336604..e471f3a 100644 --- a/src/wavinfo/wave_dbmd_reader.py +++ b/src/wavinfo/wave_dbmd_reader.py @@ -7,12 +7,11 @@ Unless otherwise stated, all § references here are to .. _EBU Tech 3285 Supplement 6: https://tech.ebu.ch/docs/tech/tech3285s6.pdf """ -from enum import IntEnum, Enum -from struct import unpack -from dataclasses import dataclass, asdict -from typing import List, Tuple, Any, Union - +from dataclasses import asdict, dataclass +from enum import Enum, IntEnum from io import BytesIO +from struct import unpack +from typing import Any, List, Tuple, Union class SegmentType(IntEnum): diff --git a/src/wavinfo/wave_info_reader.py b/src/wavinfo/wave_info_reader.py index 2b62ff5..9a9679d 100644 --- a/src/wavinfo/wave_info_reader.py +++ b/src/wavinfo/wave_info_reader.py @@ -1,7 +1,7 @@ -from .riff_parser import parse_chunk, ListChunkDescriptor - from typing import Optional +from .riff_parser import ListChunkDescriptor, parse_chunk + class WavInfoChunkReader: def __init__(self, f, encoding): diff --git a/src/wavinfo/wave_ixml_reader.py b/src/wavinfo/wave_ixml_reader.py index 2278fc0..4d74f9f 100644 --- a/src/wavinfo/wave_ixml_reader.py +++ b/src/wavinfo/wave_ixml_reader.py @@ -1,10 +1,10 @@ -from lxml import etree as ET import io +from enum import IntEnum # from collections import namedtuple -from typing import Optional -from enum import IntEnum -from typing import NamedTuple +from typing import NamedTuple, Optional + +from lxml import etree as ET class IXMLTrack(NamedTuple): diff --git a/src/wavinfo/wave_reader.py b/src/wavinfo/wave_reader.py index a05a0bc..bccc6e5 100644 --- a/src/wavinfo/wave_reader.py +++ b/src/wavinfo/wave_reader.py @@ -1,18 +1,16 @@ # -*- coding: utf-8 -*- -import struct import os -from typing import Optional, Generator, Any, NamedTuple - import pathlib +import struct +from typing import Any, Generator, NamedTuple, Optional - -from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor -from .wave_ixml_reader import WavIXMLFormat -from .wave_bext_reader import WavBextReader -from .wave_info_reader import WavInfoChunkReader +from .riff_parser import ChunkDescriptor, ListChunkDescriptor, parse_chunk from .wave_adm_reader import WavADMReader -from .wave_dbmd_reader import WavDolbyMetadataReader +from .wave_bext_reader import WavBextReader from .wave_cues_reader import WavCuesReader +from .wave_dbmd_reader import WavDolbyMetadataReader +from .wave_info_reader import WavInfoChunkReader +from .wave_ixml_reader import WavIXMLFormat from .wave_smpl_reader import WavSmplReader #: Calculated statistics about the audio data. diff --git a/src/wavinfo/wave_smpl_reader.py b/src/wavinfo/wave_smpl_reader.py index 483cf2f..772141d 100644 --- a/src/wavinfo/wave_smpl_reader.py +++ b/src/wavinfo/wave_smpl_reader.py @@ -1,6 +1,5 @@ import struct - -from typing import Tuple, NamedTuple, List +from typing import List, NamedTuple, Tuple class WaveSmplLoop(NamedTuple): diff --git a/src/wavinfo/wavfind.py b/src/wavinfo/wavfind.py index 7b9b348..ca64042 100644 --- a/src/wavinfo/wavfind.py +++ b/src/wavinfo/wavfind.py @@ -1,7 +1,7 @@ """ """ -from optparse import OptionParser, OptionGroup import sys +from optparse import OptionGroup, OptionParser def main(): diff --git a/tests/__init__.py b/tests/__init__.py index ba1231f..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,42 +0,0 @@ -from . import test_wave_parsing - -from io import BytesIO -from typing import Generator -import zipfile as zf -import os.path -import os -from contextlib import contextmanager - -# class TestFileLoader: -# """ -# This guy manages the test_files archive. -# """ - -# def __init__(self, test_file_arch_name = 'archive.zip') -> None: -# self.base_path = os.path.join(os.path.dirname(__file__), "test_files") -# self.test_file_arch_name = test_file_arch_name -# self._gather_test_files_into_archive() - -# @property -# def arch_path(self): -# return os.path.join(self.base_path, self.test_file_arch_name) - -# @contextmanager -# def open(self, name) -> Generator[BytesIO]: -# z = zf.ZipFile(self.arch_path, 'r') -# member = z.open(name, 'r') -# try: -# yield member -# finally: -# zf.close() - -# def _gather_test_files_into_archive(self): -# with zf.ZipFile(self.arch_path, 'a') as zip: -# for root, _, files in os.walk(self.base_path): -# for name in files: -# if root == self.base_path and name == self.test_file_arch_name: -# continue -# else: -# p = os.path.join(root, name) -# zip.write(p) -# os.unlink(p) diff --git a/tests/test_cue.py b/tests/test_cue.py index 27e27e1..4d0a406 100644 --- a/tests/test_cue.py +++ b/tests/test_cue.py @@ -1,5 +1,5 @@ -from unittest import TestCase from glob import glob +from unittest import TestCase import wavinfo diff --git a/tests/test_dolby.py b/tests/test_dolby.py index 9ad0c4b..89eb13d 100644 --- a/tests/test_dolby.py +++ b/tests/test_dolby.py @@ -1,7 +1,7 @@ from unittest import TestCase import wavinfo -from wavinfo.wave_dbmd_reader import SegmentType, DolbyDigitalPlusMetadata +from wavinfo.wave_dbmd_reader import DolbyDigitalPlusMetadata, SegmentType class TestDolby(TestCase): diff --git a/tests/test_files/adm/test_bwf.wav b/tests/test_files/adm/test_bwf.wav new file mode 100644 index 0000000000000000000000000000000000000000..e67e02df5c5eceb2a904057929404e81d75a3981 GIT binary patch literal 153890 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 Date: Thu, 17 Sep 2026 10:43:17 -0700 Subject: [PATCH 4/6] Tweaked pyproject for ruff settings --- pyproject.toml | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3c756db..2759369 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,15 +58,22 @@ wavinfo = "wavinfo:__main__.main" [tool.pyright] typeCheckingMode = "basic" -[tool.pylint] -max-line-length = 88 -disable = [ - "C0103", # (invalid-name) - "C0114", # (missing-module-docstring) - "C0115", # (missing-class-docstring) - "C0116", # (missing-function-docstring) - "R0903", # (too-few-public-methods) - "R0913", # (too-many-arguments) - "W0105", # (pointless-string-statement) -] +[tool.ruff] +line-length = 88 +indent-width = 4 + +[tool.ruff.lint] +fixable = ['ALL'] + +# [tool.pylint] +# max-line-length = 88 +# disable = [ +# "C0103", # (invalid-name) +# "C0114", # (missing-module-docstring) +# "C0115", # (missing-class-docstring) +# "C0116", # (missing-function-docstring) +# "R0903", # (too-few-public-methods) +# "R0913", # (too-many-arguments) +# "W0105", # (pointless-string-statement) +# ] From 6f5cb795a31dbeb846f3a1663ab2a78e327df537 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 17 Sep 2026 11:15:09 -0700 Subject: [PATCH 5/6] Ruff --- pyproject.toml | 7 ++- src/wavinfo/wave_ixml_reader.py | 92 ++++++++++++++++----------------- src/wavinfo/wave_reader.py | 39 +++++++------- src/wavinfo/wave_smpl_reader.py | 8 +-- src/wavinfo/wavfind.py | 7 ++- tests/test_adm.py | 8 +-- tests/test_main.py | 15 ++---- tests/test_walk.py | 16 +++--- tests/test_wave_parsing.py | 2 +- tests/utils.py | 22 ++------ 10 files changed, 100 insertions(+), 116 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2759369..3de1026 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ [dependency-groups] dev = [ "pytest>=8.3.5", - "ruff>=0.14.0", + "ruff>=0.16.8", ] doc = [ "sphinx>=7.1.2", @@ -64,6 +64,11 @@ indent-width = 4 [tool.ruff.lint] fixable = ['ALL'] +ignore = [ + 'UP022', # Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE` + 'SIM115', # Use a context manager for opening files + 'UP031', #Use format specifiers instead of percent format +] # [tool.pylint] # max-line-length = 88 diff --git a/src/wavinfo/wave_ixml_reader.py b/src/wavinfo/wave_ixml_reader.py index 4d74f9f..1b66c59 100644 --- a/src/wavinfo/wave_ixml_reader.py +++ b/src/wavinfo/wave_ixml_reader.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import io from enum import IntEnum # from collections import namedtuple -from typing import NamedTuple, Optional +from typing import NamedTuple from lxml import etree as ET @@ -76,7 +78,7 @@ class SteinbergMetadata: self.parsed = xml.find(self.Steinberg_xpath) @property - def audio_speaker_arrangement(self) -> Optional[AudioSpeakerArrangement]: + def audio_speaker_arrangement(self) -> AudioSpeakerArrangement | None: """ `AudioSpeakerArrangement` property """ @@ -87,7 +89,7 @@ class SteinbergMetadata: return type(self).AudioSpeakerArrangement(int(val.text)) @property - def sample_format_size(self) -> Optional[int]: + def sample_format_size(self) -> int | None: """ AudioSampleFormatSize """ @@ -96,7 +98,7 @@ class SteinbergMetadata: return int(val.text) @property - def media_company(self) -> Optional[str]: + def media_company(self) -> str | None: """ MediaCompany """ @@ -105,7 +107,7 @@ class SteinbergMetadata: return val.text @property - def media_drop_frames(self) -> Optional[bool]: + def media_drop_frames(self) -> bool | None: """ MediaDropFrames """ @@ -114,7 +116,7 @@ class SteinbergMetadata: return val.text == "1" @property - def media_duration(self) -> Optional[float]: + def media_duration(self) -> float | None: """ MediaDuration """ @@ -122,33 +124,29 @@ class SteinbergMetadata: if val is not None: return float(val.text) - @property - def media_start_time(self) -> Optional[float]: - """ - MediaStartTime - """ - pass + # @property + # def media_start_time(self) -> float | None: + # """ + # MediaStartTime + # """ - @property - def media_track_title(self) -> Optional[str]: - """ - MediaTrackTitle - """ - pass + # @property + # def media_track_title(self) -> str | None: + # """ + # MediaTrackTitle + # """ - @property - def program_name(self) -> Optional[str]: - """ - ProgramName - """ - pass + # @property + # def program_name(self) -> str | None: + # """ + # ProgramName + # """ - @property - def program_version(self) -> Optional[str]: - """ - ProgramVersion - """ - pass + # @property + # def program_version(self) -> str | None: + # """ + # ProgramVersion + # """ class WavIXMLFormat: @@ -166,7 +164,7 @@ class WavIXMLFormat: parser = ET.XMLParser(recover=True) self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser) - def _get_text_value(self, xpath) -> Optional[str]: + def _get_text_value(self, xpath) -> str | None: e = self.parsed.find("./" + xpath) if e is not None: return e.text @@ -200,35 +198,35 @@ class WavIXMLFormat: ) @property - def project(self) -> Optional[str]: + def project(self) -> str | None: """ The project/film name entered for the recording. """ return self._get_text_value("PROJECT") @property - def scene(self) -> Optional[str]: + def scene(self) -> str | None: """ Scene/slate. """ return self._get_text_value("SCENE") @property - def take(self) -> Optional[str]: + def take(self) -> str | None: """ Take number. """ return self._get_text_value("TAKE") @property - def tape(self) -> Optional[str]: + def tape(self) -> str | None: """ Tape name. """ return self._get_text_value("TAPE") @property - def family_uid(self) -> Optional[str]: + def family_uid(self) -> str | None: """ The globally-unique ID for this file family. This may be in the format of a GUID, or an EBU Rec 9 source identifier, or some other dumb @@ -237,14 +235,14 @@ class WavIXMLFormat: return self._get_text_value("FILE_SET/FAMILY_UID") @property - def family_name(self) -> Optional[str]: + def family_name(self) -> str | None: """ The name of this file's file family. """ return self._get_text_value("FILE_SET/FAMILY_NAME") @property - def steinberg(self) -> Optional[SteinbergMetadata]: + def steinberg(self) -> SteinbergMetadata | None: """ Steinberg vendor iXML metadata if present. """ @@ -254,12 +252,12 @@ class WavIXMLFormat: return None def to_dict(self): - return dict( - track_list=list(map(lambda x: x._asdict(), self.track_list)), - project=self.project, - scene=self.scene, - take=self.take, - tape=self.tape, - family_uid=self.family_uid, - family_name=self.family_name, - ) + return { + "track_list": [x._asdict() for x in self.track_list], + "project": self.project, + "scene": self.scene, + "take": self.take, + "tape": self.tape, + "family_uid": self.family_uid, + "family_name": self.family_name, + } diff --git a/src/wavinfo/wave_reader.py b/src/wavinfo/wave_reader.py index bccc6e5..3691464 100644 --- a/src/wavinfo/wave_reader.py +++ b/src/wavinfo/wave_reader.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations + import os import pathlib import struct -from typing import Any, Generator, NamedTuple, Optional +from typing import Any, Generator, NamedTuple from .riff_parser import ChunkDescriptor, ListChunkDescriptor, parse_chunk from .wave_adm_reader import WavADMReader @@ -58,31 +59,31 @@ class WavInfoReader: self.bext_encoding = bext_encoding #: Wave audio data format. - self.fmt: Optional[WavAudioFormat] = None + self.fmt: WavAudioFormat | None = None #: Statistics of the `data` section. - self.data: Optional[WavDataDescriptor] = None + self.data: WavDataDescriptor | None = None #: Broadcast-Wave metadata. - self.bext: Optional[WavBextReader] = None + self.bext: WavBextReader | None = None #: iXML metadata. - self.ixml: Optional[WavIXMLFormat] = None + self.ixml: WavIXMLFormat | None = None #: ADM Audio Definiton Model metadata. - self.adm: Optional[WavADMReader] = None + self.adm: WavADMReader | None = None #: Dolby bitstream metadata. - self.dolby: Optional[WavDolbyMetadataReader] = None + self.dolby: WavDolbyMetadataReader | None = None #: RIFF INFO metadata. - self.info: Optional[WavInfoChunkReader] = None + self.info: WavInfoChunkReader | None = None #: RIFF cues markers, labels, and notes. - self.cues: Optional[WavCuesReader] = None + self.cues: WavCuesReader | None = None #: Sampler `smpl` metadata - self.smpl: Optional[WavSmplReader] = None + self.smpl: WavSmplReader | None = None if hasattr(path, "read"): self.get_wav_info(path) @@ -97,8 +98,8 @@ class WavInfoReader: self.path = absolute_path - with open(path, "rb") as path: - self.get_wav_info(path) + with open(path, "rb") as file: + self.get_wav_info(file) def get_wav_info(self, wavfile): chunks = parse_chunk(wavfile) @@ -117,9 +118,7 @@ class WavInfoReader: self.smpl = self._get_sampler_loops(wavfile) self.data = self._describe_data() - def _find_chunk_data( - self, ident, from_stream, default_none=False - ) -> Optional[bytes]: + def _find_chunk_data(self, ident, from_stream, default_none=False) -> bytes | None: top_chunks = ( chunk for chunk in self.main_list @@ -130,7 +129,7 @@ class WavInfoReader: return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None - def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]: + def _find_list_chunk(self, signature) -> ListChunkDescriptor | None: top_chunks = ( chunk for chunk in self.main_list @@ -261,10 +260,8 @@ class WavInfoReader: if self.__getattribute__(scope) else {} ) - for key in mdict.keys(): + for key in mdict: yield scope, key, mdict[key] def __repr__(self): - return "WavInfoReader({}, {}, {})".format( - self.path, self.info_encoding, self.bext_encoding - ) + return f"WavInfoReader({self.path}, {self.info_encoding}, {self.bext_encoding})" diff --git a/src/wavinfo/wave_smpl_reader.py b/src/wavinfo/wave_smpl_reader.py index 772141d..59f9d49 100644 --- a/src/wavinfo/wave_smpl_reader.py +++ b/src/wavinfo/wave_smpl_reader.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import struct -from typing import List, NamedTuple, Tuple +from typing import NamedTuple class WaveSmplLoop(NamedTuple): @@ -69,13 +71,13 @@ class WavSmplReader: #: The SMPTE offset to apply, as a tuple of four ints representing #: hh, mm, ss, ff - self.smpte_offset: Tuple[int, int, int, int] = unpacked_data[6:10] + self.smpte_offset: tuple[int, int, int, int] = unpacked_data[6:10] loop_count = unpacked_data[10] sampler_udata_length = unpacked_data[11] #: List of loops in the file. - self.sample_loops: List[WaveSmplLoop] = [] + self.sample_loops: list[WaveSmplLoop] = [] loop_buffer = smpl_data[header_size : header_size + loop_size * loop_count] diff --git a/src/wavinfo/wavfind.py b/src/wavinfo/wavfind.py index ca64042..4181f3e 100644 --- a/src/wavinfo/wavfind.py +++ b/src/wavinfo/wavfind.py @@ -1,5 +1,8 @@ -""" """ +""" +wavfind.py +Under construction! Do Not Use! +""" import sys from optparse import OptionGroup, OptionParser @@ -21,7 +24,7 @@ def main(): primaries.add_option("--desc", help="Search descriptions", metavar="DESC") - (options, args) = parser.parse_args(sys.argv) + (_options, _args) = parser.parse_args(sys.argv) if __name__ == "__main__": diff --git a/tests/test_adm.py b/tests/test_adm.py index 5a4c279..7180c7a 100644 --- a/tests/test_adm.py +++ b/tests/test_adm.py @@ -42,12 +42,14 @@ class TestADMWave(TestCase): adm = info.adm assert adm is not None t1 = adm.track_info(0) - self.assertTrue("channel_format_name" in t1.keys()) + assert t1 is not None + self.assertTrue("channel_format_name" in t1) self.assertEqual("RoomCentricLeft", t1["channel_format_name"]) - self.assertTrue("pack_format_name" in t1.keys()) + self.assertTrue("pack_format_name" in t1) self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"]) t10 = adm.track_info(10) - self.assertTrue("content_name" in t10.keys()) + assert t10 is not None + self.assertTrue("content_name" in t10) self.assertEqual("Dialog", t10["content_name"]) diff --git a/tests/test_main.py b/tests/test_main.py index 0fedd32..683c3ba 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,24 +9,15 @@ from wavinfo.__main__ import main class MainTest(unittest.TestCase): def test_empty_argv(self): with patch.object(sys, "argv", []): - try: - main() - except Exception as e: - self.fail(f"main() throwing an exception: {e}") + main() def test_a_file(self): for path in glob.glob("tests/test_files/**/*.wav"): with patch.object(sys, "argv", ["TEST", path]): - try: - main() - except Exception as e: - self.fail(f"main() throwing an exception: {e}") + main() def test_ixml(self): with patch.object( sys, "argv", ["TEST", "--ixml", "tests/test_files/sounddevices/A101_1.WAV"] ): - try: - main() - except Exception as e: - self.fail(f"main() throwing an exception: {e}") + main() diff --git a/tests/test_walk.py b/tests/test_walk.py index 58d0438..41d9661 100644 --- a/tests/test_walk.py +++ b/tests/test_walk.py @@ -11,12 +11,10 @@ class TestWalk(unittest.TestCase): tested_data, tested_format = False, False for scope, key, value in info.walk(): - if scope == "fmt": - if key == "channel_count": + if scope == "fmt" and key == "channel_count": tested_format = True self.assertEqual(value, 2) - if scope == "data": - if key == "frame_count": + if scope == "data" and key == "frame_count": tested_data = True self.assertEqual(value, 144140) @@ -26,11 +24,11 @@ class TestWalk(unittest.TestCase): for file in glob.glob("tests/test_files/**/*.wav"): info = wavinfo.WavInfoReader(file) - try: - for _, _, _ in info.walk(): - pass - except Exception as e: - self.fail(f"Failed to walk metadata in file {file}: {e}") + # try: + for _, _, _ in info.walk(): + pass + # except Exception as e: + # self.fail(f"Failed to walk metadata in file {file}: {e}") if __name__ == "__main__": diff --git a/tests/test_wave_parsing.py b/tests/test_wave_parsing.py index fbeec15..1804a84 100644 --- a/tests/test_wave_parsing.py +++ b/tests/test_wave_parsing.py @@ -14,7 +14,7 @@ class TestWaveInfo(TestCase): info = wavinfo.WavInfoReader(wav_file) self.assertEqual( info.__repr__(), - "WavInfoReader({}, latin_1, ascii)".format(os.path.abspath(wav_file)), + f"WavInfoReader({os.path.abspath(wav_file)}, latin_1, ascii)", ) self.assertIsNotNone(info) diff --git a/tests/utils.py b/tests/utils.py index 70f2b2d..0e0f847 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,6 @@ import json import os.path import subprocess -import sys from subprocess import PIPE FFPROBE = "ffprobe" @@ -9,23 +8,12 @@ FFPROBE = "ffprobe" def ffprobe(path): arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path] - if int(sys.version[0]) < 3: - process = subprocess.Popen(arguments, stdout=PIPE) - process.wait() - if process.returncode == 0: - output = process.communicate()[0] - if output: - output_str = output.decode("utf-8") - return json.loads(output_str) - else: - return None + process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE, check=True) + if process.returncode == 0: + output_str = process.stdout.decode("utf-8") + return json.loads(output_str) else: - process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE) - if process.returncode == 0: - output_str = process.stdout.decode("utf-8") - return json.loads(output_str) - else: - return None + return None def all_files(): From 50dbc3559560323ac698932d06702cec078c7ae0 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 17 Sep 2026 11:28:36 -0700 Subject: [PATCH 6/6] Ruff --- docs/source/conf.py | 1 - pyproject.toml | 3 +++ src/wavinfo/__init__.py | 2 +- src/wavinfo/__main__.py | 16 ++++++++++------ src/wavinfo/wave_dbmd_reader.py | 6 ------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index c0fdb6b..72e8486 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # diff --git a/pyproject.toml b/pyproject.toml index 3de1026..c7d9909 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,9 +65,12 @@ indent-width = 4 [tool.ruff.lint] fixable = ['ALL'] ignore = [ + 'FA100', #Add `from __future__ import annotations` to simplify `typing.Optional` 'UP022', # Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE` 'SIM115', # Use a context manager for opening files 'UP031', #Use format specifiers instead of percent format + 'C408', # Unnecessary `dict()` call (rewrite as a literal) + 'C417' # Unnecessary `map()` usage (rewrite using a generator expression) ] # [tool.pylint] diff --git a/src/wavinfo/__init__.py b/src/wavinfo/__init__.py index 82e9cb0..fd0ffcc 100644 --- a/src/wavinfo/__init__.py +++ b/src/wavinfo/__init__.py @@ -2,7 +2,7 @@ Probe WAVE Files for iXML, Broadcast-WAVE and other metadata. """ -__all__ = ["WavInfoReader", "WavInfoEOFError"] +__all__ = ["WavInfoEOFError", "WavInfoReader"] from .riff_parser import WavInfoEOFError from .wave_reader import WavInfoReader diff --git a/src/wavinfo/__main__.py b/src/wavinfo/__main__.py index 9d1ddf7..43001a4 100644 --- a/src/wavinfo/__main__.py +++ b/src/wavinfo/__main__.py @@ -31,7 +31,11 @@ class MetaBrowser(Cmd): prompt = "(wavinfo) " metadata: Union[List, Dict] - path: List[str] = [] + path: List[str] + + def preloop(self) -> None: + self.path = [] + return super().preloop() @property def cwd(self): @@ -91,7 +95,7 @@ class MetaBrowser(Cmd): else: print(f"Index {argv[0]} does not exist") elif isinstance(self.cwd, dict): - if argv[0] in self.cwd.keys(): + if argv[0] in self.cwd: self.path = self.path + [argv[0]] else: print(f'Key "{argv[0]}" does not exist') @@ -177,12 +181,14 @@ def main(): else: ret_dict = { "filename": arg, - "run_date": datetime.datetime.now().isoformat(), + "run_date": datetime.datetime.now( + tz=datetime.timezone.utc + ).isoformat(), "application": f"wavinfo {version}", "scopes": {}, } for scope, name, value in this_file.walk(): - if scope not in ret_dict["scopes"].keys(): + if scope not in ret_dict["scopes"]: ret_dict["scopes"][scope] = {} ret_dict["scopes"][scope][name] = value @@ -198,8 +204,6 @@ def main(): file=sys.stderr, ) continue - except Exception as e: - raise e if len(interactive_dict) > 0: cli = MetaBrowser() diff --git a/src/wavinfo/wave_dbmd_reader.py b/src/wavinfo/wave_dbmd_reader.py index e471f3a..a25563f 100644 --- a/src/wavinfo/wave_dbmd_reader.py +++ b/src/wavinfo/wave_dbmd_reader.py @@ -178,22 +178,16 @@ class DolbyDigitalPlusMetadata: Per ATSC/A52 § 5.4.2.12, this is not in use and always 0xFF. """ - pass - class MixLevel(int): """ § 4.3.6.2 """ - pass - class DialnormLevel(int): """ § 4.3.4.4 """ - pass - class RoomType(Enum): """ `roomtyp` 4.3.6.3