This commit is contained in:
2026-09-17 11:15:09 -07:00
parent 7d5bb29a42
commit 6f5cb795a3
10 changed files with 100 additions and 116 deletions
+6 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pytest>=8.3.5", "pytest>=8.3.5",
"ruff>=0.14.0", "ruff>=0.16.8",
] ]
doc = [ doc = [
"sphinx>=7.1.2", "sphinx>=7.1.2",
@@ -64,6 +64,11 @@ indent-width = 4
[tool.ruff.lint] [tool.ruff.lint]
fixable = ['ALL'] 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] # [tool.pylint]
# max-line-length = 88 # max-line-length = 88
+45 -47
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
import io import io
from enum import IntEnum from enum import IntEnum
# from collections import namedtuple # from collections import namedtuple
from typing import NamedTuple, Optional from typing import NamedTuple
from lxml import etree as ET from lxml import etree as ET
@@ -76,7 +78,7 @@ class SteinbergMetadata:
self.parsed = xml.find(self.Steinberg_xpath) self.parsed = xml.find(self.Steinberg_xpath)
@property @property
def audio_speaker_arrangement(self) -> Optional[AudioSpeakerArrangement]: def audio_speaker_arrangement(self) -> AudioSpeakerArrangement | None:
""" """
`AudioSpeakerArrangement` property `AudioSpeakerArrangement` property
""" """
@@ -87,7 +89,7 @@ class SteinbergMetadata:
return type(self).AudioSpeakerArrangement(int(val.text)) return type(self).AudioSpeakerArrangement(int(val.text))
@property @property
def sample_format_size(self) -> Optional[int]: def sample_format_size(self) -> int | None:
""" """
AudioSampleFormatSize AudioSampleFormatSize
""" """
@@ -96,7 +98,7 @@ class SteinbergMetadata:
return int(val.text) return int(val.text)
@property @property
def media_company(self) -> Optional[str]: def media_company(self) -> str | None:
""" """
MediaCompany MediaCompany
""" """
@@ -105,7 +107,7 @@ class SteinbergMetadata:
return val.text return val.text
@property @property
def media_drop_frames(self) -> Optional[bool]: def media_drop_frames(self) -> bool | None:
""" """
MediaDropFrames MediaDropFrames
""" """
@@ -114,7 +116,7 @@ class SteinbergMetadata:
return val.text == "1" return val.text == "1"
@property @property
def media_duration(self) -> Optional[float]: def media_duration(self) -> float | None:
""" """
MediaDuration MediaDuration
""" """
@@ -122,33 +124,29 @@ class SteinbergMetadata:
if val is not None: if val is not None:
return float(val.text) return float(val.text)
@property # @property
def media_start_time(self) -> Optional[float]: # def media_start_time(self) -> float | None:
""" # """
MediaStartTime # MediaStartTime
""" # """
pass
@property # @property
def media_track_title(self) -> Optional[str]: # def media_track_title(self) -> str | None:
""" # """
MediaTrackTitle # MediaTrackTitle
""" # """
pass
@property # @property
def program_name(self) -> Optional[str]: # def program_name(self) -> str | None:
""" # """
ProgramName # ProgramName
""" # """
pass
@property # @property
def program_version(self) -> Optional[str]: # def program_version(self) -> str | None:
""" # """
ProgramVersion # ProgramVersion
""" # """
pass
class WavIXMLFormat: class WavIXMLFormat:
@@ -166,7 +164,7 @@ class WavIXMLFormat:
parser = ET.XMLParser(recover=True) parser = ET.XMLParser(recover=True)
self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser) self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser)
def _get_text_value(self, xpath) -> Optional[str]: def _get_text_value(self, xpath) -> str | None:
e = self.parsed.find("./" + xpath) e = self.parsed.find("./" + xpath)
if e is not None: if e is not None:
return e.text return e.text
@@ -200,35 +198,35 @@ class WavIXMLFormat:
) )
@property @property
def project(self) -> Optional[str]: def project(self) -> str | None:
""" """
The project/film name entered for the recording. The project/film name entered for the recording.
""" """
return self._get_text_value("PROJECT") return self._get_text_value("PROJECT")
@property @property
def scene(self) -> Optional[str]: def scene(self) -> str | None:
""" """
Scene/slate. Scene/slate.
""" """
return self._get_text_value("SCENE") return self._get_text_value("SCENE")
@property @property
def take(self) -> Optional[str]: def take(self) -> str | None:
""" """
Take number. Take number.
""" """
return self._get_text_value("TAKE") return self._get_text_value("TAKE")
@property @property
def tape(self) -> Optional[str]: def tape(self) -> str | None:
""" """
Tape name. Tape name.
""" """
return self._get_text_value("TAPE") return self._get_text_value("TAPE")
@property @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 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 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") return self._get_text_value("FILE_SET/FAMILY_UID")
@property @property
def family_name(self) -> Optional[str]: def family_name(self) -> str | None:
""" """
The name of this file's file family. The name of this file's file family.
""" """
return self._get_text_value("FILE_SET/FAMILY_NAME") return self._get_text_value("FILE_SET/FAMILY_NAME")
@property @property
def steinberg(self) -> Optional[SteinbergMetadata]: def steinberg(self) -> SteinbergMetadata | None:
""" """
Steinberg vendor iXML metadata if present. Steinberg vendor iXML metadata if present.
""" """
@@ -254,12 +252,12 @@ class WavIXMLFormat:
return None return None
def to_dict(self): def to_dict(self):
return dict( return {
track_list=list(map(lambda x: x._asdict(), self.track_list)), "track_list": [x._asdict() for x in self.track_list],
project=self.project, "project": self.project,
scene=self.scene, "scene": self.scene,
take=self.take, "take": self.take,
tape=self.tape, "tape": self.tape,
family_uid=self.family_uid, "family_uid": self.family_uid,
family_name=self.family_name, "family_name": self.family_name,
) }
+18 -21
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- from __future__ import annotations
import os import os
import pathlib import pathlib
import struct import struct
from typing import Any, Generator, NamedTuple, Optional from typing import Any, Generator, NamedTuple
from .riff_parser import ChunkDescriptor, ListChunkDescriptor, parse_chunk from .riff_parser import ChunkDescriptor, ListChunkDescriptor, parse_chunk
from .wave_adm_reader import WavADMReader from .wave_adm_reader import WavADMReader
@@ -58,31 +59,31 @@ class WavInfoReader:
self.bext_encoding = bext_encoding self.bext_encoding = bext_encoding
#: Wave audio data format. #: Wave audio data format.
self.fmt: Optional[WavAudioFormat] = None self.fmt: WavAudioFormat | None = None
#: Statistics of the `data` section. #: Statistics of the `data` section.
self.data: Optional[WavDataDescriptor] = None self.data: WavDataDescriptor | None = None
#: Broadcast-Wave metadata. #: Broadcast-Wave metadata.
self.bext: Optional[WavBextReader] = None self.bext: WavBextReader | None = None
#: iXML metadata. #: iXML metadata.
self.ixml: Optional[WavIXMLFormat] = None self.ixml: WavIXMLFormat | None = None
#: ADM Audio Definiton Model metadata. #: ADM Audio Definiton Model metadata.
self.adm: Optional[WavADMReader] = None self.adm: WavADMReader | None = None
#: Dolby bitstream metadata. #: Dolby bitstream metadata.
self.dolby: Optional[WavDolbyMetadataReader] = None self.dolby: WavDolbyMetadataReader | None = None
#: RIFF INFO metadata. #: RIFF INFO metadata.
self.info: Optional[WavInfoChunkReader] = None self.info: WavInfoChunkReader | None = None
#: RIFF cues markers, labels, and notes. #: RIFF cues markers, labels, and notes.
self.cues: Optional[WavCuesReader] = None self.cues: WavCuesReader | None = None
#: Sampler `smpl` metadata #: Sampler `smpl` metadata
self.smpl: Optional[WavSmplReader] = None self.smpl: WavSmplReader | None = None
if hasattr(path, "read"): if hasattr(path, "read"):
self.get_wav_info(path) self.get_wav_info(path)
@@ -97,8 +98,8 @@ class WavInfoReader:
self.path = absolute_path self.path = absolute_path
with open(path, "rb") as path: with open(path, "rb") as file:
self.get_wav_info(path) self.get_wav_info(file)
def get_wav_info(self, wavfile): def get_wav_info(self, wavfile):
chunks = parse_chunk(wavfile) chunks = parse_chunk(wavfile)
@@ -117,9 +118,7 @@ class WavInfoReader:
self.smpl = self._get_sampler_loops(wavfile) self.smpl = self._get_sampler_loops(wavfile)
self.data = self._describe_data() self.data = self._describe_data()
def _find_chunk_data( def _find_chunk_data(self, ident, from_stream, default_none=False) -> bytes | None:
self, ident, from_stream, default_none=False
) -> Optional[bytes]:
top_chunks = ( top_chunks = (
chunk chunk
for chunk in self.main_list for chunk in self.main_list
@@ -130,7 +129,7 @@ class WavInfoReader:
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]: def _find_list_chunk(self, signature) -> ListChunkDescriptor | None:
top_chunks = ( top_chunks = (
chunk chunk
for chunk in self.main_list for chunk in self.main_list
@@ -261,10 +260,8 @@ class WavInfoReader:
if self.__getattribute__(scope) if self.__getattribute__(scope)
else {} else {}
) )
for key in mdict.keys(): for key in mdict:
yield scope, key, mdict[key] yield scope, key, mdict[key]
def __repr__(self): def __repr__(self):
return "WavInfoReader({}, {}, {})".format( return f"WavInfoReader({self.path}, {self.info_encoding}, {self.bext_encoding})"
self.path, self.info_encoding, self.bext_encoding
)
+5 -3
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import struct import struct
from typing import List, NamedTuple, Tuple from typing import NamedTuple
class WaveSmplLoop(NamedTuple): class WaveSmplLoop(NamedTuple):
@@ -69,13 +71,13 @@ class WavSmplReader:
#: The SMPTE offset to apply, as a tuple of four ints representing #: The SMPTE offset to apply, as a tuple of four ints representing
#: hh, mm, ss, ff #: 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] loop_count = unpacked_data[10]
sampler_udata_length = unpacked_data[11] sampler_udata_length = unpacked_data[11]
#: List of loops in the file. #: 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] loop_buffer = smpl_data[header_size : header_size + loop_size * loop_count]
+5 -2
View File
@@ -1,5 +1,8 @@
""" """ """
wavfind.py
Under construction! Do Not Use!
"""
import sys import sys
from optparse import OptionGroup, OptionParser from optparse import OptionGroup, OptionParser
@@ -21,7 +24,7 @@ def main():
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) (_options, _args) = parser.parse_args(sys.argv)
if __name__ == "__main__": if __name__ == "__main__":
+5 -3
View File
@@ -42,12 +42,14 @@ class TestADMWave(TestCase):
adm = info.adm adm = info.adm
assert adm is not None assert adm is not None
t1 = adm.track_info(0) 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.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"]) self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"])
t10 = adm.track_info(10) 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"]) self.assertEqual("Dialog", t10["content_name"])
+3 -12
View File
@@ -9,24 +9,15 @@ from wavinfo.__main__ import main
class MainTest(unittest.TestCase): class MainTest(unittest.TestCase):
def test_empty_argv(self): def test_empty_argv(self):
with patch.object(sys, "argv", []): with patch.object(sys, "argv", []):
try: main()
main()
except Exception as e:
self.fail(f"main() throwing an exception: {e}")
def test_a_file(self): def test_a_file(self):
for path in glob.glob("tests/test_files/**/*.wav"): 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()
main()
except Exception as e:
self.fail(f"main() throwing an exception: {e}")
def test_ixml(self): def test_ixml(self):
with patch.object( with patch.object(
sys, "argv", ["TEST", "--ixml", "tests/test_files/sounddevices/A101_1.WAV"] sys, "argv", ["TEST", "--ixml", "tests/test_files/sounddevices/A101_1.WAV"]
): ):
try: main()
main()
except Exception as e:
self.fail(f"main() throwing an exception: {e}")
+7 -9
View File
@@ -11,12 +11,10 @@ class TestWalk(unittest.TestCase):
tested_data, tested_format = False, False tested_data, tested_format = False, False
for scope, key, value in info.walk(): for scope, key, value in info.walk():
if scope == "fmt": if scope == "fmt" and key == "channel_count":
if key == "channel_count":
tested_format = True tested_format = True
self.assertEqual(value, 2) self.assertEqual(value, 2)
if scope == "data": if scope == "data" and key == "frame_count":
if key == "frame_count":
tested_data = True tested_data = True
self.assertEqual(value, 144140) self.assertEqual(value, 144140)
@@ -26,11 +24,11 @@ class TestWalk(unittest.TestCase):
for file in glob.glob("tests/test_files/**/*.wav"): for file in glob.glob("tests/test_files/**/*.wav"):
info = wavinfo.WavInfoReader(file) info = wavinfo.WavInfoReader(file)
try: # try:
for _, _, _ in info.walk(): for _, _, _ in info.walk():
pass pass
except Exception as e: # except Exception as e:
self.fail(f"Failed to walk metadata in file {file}: {e}") # self.fail(f"Failed to walk metadata in file {file}: {e}")
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -14,7 +14,7 @@ class TestWaveInfo(TestCase):
info = wavinfo.WavInfoReader(wav_file) info = wavinfo.WavInfoReader(wav_file)
self.assertEqual( self.assertEqual(
info.__repr__(), 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) self.assertIsNotNone(info)
+5 -17
View File
@@ -1,7 +1,6 @@
import json import json
import os.path import os.path
import subprocess import subprocess
import sys
from subprocess import PIPE from subprocess import PIPE
FFPROBE = "ffprobe" FFPROBE = "ffprobe"
@@ -9,23 +8,12 @@ FFPROBE = "ffprobe"
def ffprobe(path): def ffprobe(path):
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path] arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
if int(sys.version[0]) < 3: process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE, check=True)
process = subprocess.Popen(arguments, stdout=PIPE) if process.returncode == 0:
process.wait() output_str = process.stdout.decode("utf-8")
if process.returncode == 0: return json.loads(output_str)
output = process.communicate()[0]
if output:
output_str = output.decode("utf-8")
return json.loads(output_str)
else:
return None
else: else:
process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE) return None
if process.returncode == 0:
output_str = process.stdout.decode("utf-8")
return json.loads(output_str)
else:
return None
def all_files(): def all_files():