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]
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
+45 -47
View File
@@ -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,
}
+18 -21
View File
@@ -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})"
+5 -3
View File
@@ -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]
+5 -2
View File
@@ -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__":
+5 -3
View File
@@ -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"])
+3 -12
View File
@@ -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()
+7 -9
View File
@@ -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__":
+1 -1
View File
@@ -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)
+5 -17
View File
@@ -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():