Merge pull request #35 from iluvcapra/maint-poetry

Change build system to Poetry
This commit is contained in:
Jamie Hardt
2024-11-25 10:37:05 -08:00
committed by GitHub
16 changed files with 216 additions and 45 deletions

2
.gitignore vendored
View File

@@ -110,3 +110,5 @@ venv_docs/
.DS_Store .DS_Store
.vscode/ .vscode/
poetry.lock

View File

@@ -36,6 +36,11 @@ iXML
* `Gallery Software iXML Specification <http://www.gallery.co.uk/ixml/>`_ * `Gallery Software iXML Specification <http://www.gallery.co.uk/ixml/>`_
Sampler Metadata
----------------
* `RecordingBlogs.com — Sample chunk (of a Wave file)<https://www.recordingblogs.com/wiki/sample-chunk-of-a-wave-file>`_
RIFF Metadata RIFF Metadata
------------- -------------
* `1991. Multimedia Programming Interface and Data Specifications 1.0 <https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf>`_ * `1991. Multimedia Programming Interface and Data Specifications 1.0 <https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf>`_

View File

@@ -46,6 +46,7 @@
" * `adm`: EBU Audio Defintion Model metadata, as used by Dolby Atmos.\n", " * `adm`: EBU Audio Defintion Model metadata, as used by Dolby Atmos.\n",
" * `cues`: Cue marker metadata, including labels and notes \n", " * `cues`: Cue marker metadata, including labels and notes \n",
" * `dolby`: Dolby recorder and playback metadata\n", " * `dolby`: Dolby recorder and playback metadata\n",
" * `smpl`: Sampler midi note and loop metadata\n",
"\n", "\n",
"Each of these is an attribute of a `WavInfoReader` object.\n", "Each of these is an attribute of a `WavInfoReader` object.\n",
"\n", "\n",
@@ -304,7 +305,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.11.5" "version": "3.12.5"
} }
}, },
"nbformat": 4, "nbformat": 4,

View File

@@ -1,13 +1,16 @@
[build-system] # https://python-poetry.org/docs/pyproject/
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
[project] [build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "wavinfo" name = "wavinfo"
authors = [{name = "Jamie Hardt", email = "jamiehardt@me.com"}] version = "3.0.1"
description = "Probe WAVE files for all metadata"
authors = ["Jamie Hardt <jamiehardt@me.com>"]
license = "MIT"
readme = "README.md" readme = "README.md"
dynamic = ["version", "description"]
requires-python = "~=3.8"
classifiers = [ classifiers = [
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
@@ -20,9 +23,10 @@ classifiers = [
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13" "Programming Language :: Python :: 3.13"
] ]
dependencies = [ homepage = "https://github.com/iluvcapra/wavinfo"
"lxml ~= 5.3.0" repository = "https://github.com/iluvcapra/wavinfo.git"
] documentation = "https://wavinfo.readthedocs.io/"
urls.Tracker = 'https://github.com/iluvcapra/wavinfo/issues'
keywords = [ keywords = [
'waveform', 'waveform',
'metadata', 'metadata',
@@ -35,29 +39,17 @@ keywords = [
'broadcast' 'broadcast'
] ]
[tool.flit.module] [tool.poetry.extras]
name = "wavinfo" doc = ['sphinx', 'sphinx_rtd_theme']
[project.optional-dependencies] [tool.poetry.scripts]
doc = [
'sphinx >= 5.3.0',
'sphinx_rtd_theme >= 1.1.1',
]
[project.urls]
Home = "https://github.com/iluvcapra/wavinfo"
Documentation = "https://wavinfo.readthedocs.io/"
Source = "https://github.com/iluvcapra/wavinfo.git"
Issues = 'https://github.com/iluvcapra/wavinfo/issues'
[project.entry_points.console_scripts]
wavinfo = 'wavinfo.__main__:main' wavinfo = 'wavinfo.__main__:main'
[project.scripts] [tool.poetry.dependencies]
wavinfo = "wavinfo.__main__:main" python = "^3.8"
lxml = "~= 5.3.0"
[tool.flit.external-data] sphinx_rtd_theme = {version= '>= 1.1.1', optional=true}
directory = "data" sphinx = {version= '>= 5.3.0', optional=true}
[tool.pyright] [tool.pyright]
typeCheckingMode = "basic" typeCheckingMode = "basic"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
tests/test_smpl.py Normal file
View File

@@ -0,0 +1,15 @@
from unittest import TestCase
from glob import glob
import wavinfo
class TestSmpl(TestCase):
def setUp(self) -> None:
self.test_files = glob("tests/test_files/smpl/*.wav")
return super().setUp()
def test_each(self):
for file in self.test_files:
w = wavinfo.WavInfoReader(file)
d = w.walk()
self.assertIsNotNone(d)

View File

@@ -4,6 +4,3 @@ Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.
from .wave_reader import WavInfoReader from .wave_reader import WavInfoReader
from .riff_parser import WavInfoEOFError from .riff_parser import WavInfoEOFError
__version__ = '3.0.0'
__short_version__ = '3.0.0'

View File

@@ -1,17 +1,21 @@
import datetime
from . import WavInfoReader from . import WavInfoReader
from . import __version__
import datetime
from optparse import OptionParser from optparse import OptionParser
import sys import sys
import os
import json import json
from enum import Enum from enum import Enum
import importlib.metadata
from base64 import b64encode
class MyJSONEncoder(json.JSONEncoder): class MyJSONEncoder(json.JSONEncoder):
def default(self, o): def default(self, o):
if isinstance(o, Enum): if isinstance(o, Enum):
return o._name_ return o._name_
elif isinstance(o, bytes):
return b64encode(o).decode('ascii')
else: else:
return super().default(o) return super().default(o)
@@ -21,10 +25,22 @@ class MissingDataError(RuntimeError):
def main(): def main():
version = importlib.metadata.version('wavinfo')
manpath = os.path.dirname(__file__) + "/man"
parser = OptionParser() parser = OptionParser()
parser.usage = 'wavinfo (--adm | --ixml) <FILE> +' parser.usage = 'wavinfo (--adm | --ixml) <FILE> +'
# 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('--adm', dest='adm', parser.add_option('--adm', dest='adm',
help='Output ADM XML', help='Output ADM XML',
default=False, default=False,
@@ -36,6 +52,26 @@ def main():
action='store_true') action='store_true')
(options, args) = parser.parse_args(sys.argv) (options, args) = parser.parse_args(sys.argv)
# if options.install_manpages:
# print("Installing manpages...")
# print(f"Docfiles at {__file__}")
# return
if options.man:
import shlex
print("Which man page?")
print("1) wavinfo usage")
print("7) General info on Wave file metadata")
m = input("?> ")
args = ["man", "-M", manpath, "1", "wavinfo"]
if m.startswith("7"):
args[3] = "7"
os.system(shlex.join(args))
return
for arg in args[1:]: for arg in args[1:]:
try: try:
this_file = WavInfoReader(path=arg) this_file = WavInfoReader(path=arg)
@@ -53,7 +89,7 @@ def main():
ret_dict = { ret_dict = {
'filename': arg, 'filename': arg,
'run_date': datetime.datetime.now().isoformat(), 'run_date': datetime.datetime.now().isoformat(),
'application': "wavinfo " + __version__, 'application': f"wavinfo {version}",
'scopes': {} 'scopes': {}
} }
for scope, name, value in this_file.walk(): for scope, name, value in this_file.walk():

View File

@@ -17,7 +17,7 @@ With no options,
will emit a JSON (Javascript Object Notation) object containing all will emit a JSON (Javascript Object Notation) object containing all
detected metadata. detected metadata.
.IP "\-\-adm" .IP "\-\-adm"
Output any Audio Definition Model (ADM) metadata in Output Audio Definition Model (ADM) XML metadata in
.BR FILE . .BR FILE .
.IP "\-\-ixml" .IP "\-\-ixml"
Output any iXML metdata in Output any iXML metdata in

View File

@@ -5,6 +5,7 @@ from typing import Optional, Generator, Any, NamedTuple
import pathlib import pathlib
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
from .wave_ixml_reader import WavIXMLFormat from .wave_ixml_reader import WavIXMLFormat
from .wave_bext_reader import WavBextReader from .wave_bext_reader import WavBextReader
@@ -12,9 +13,11 @@ from .wave_info_reader import WavInfoChunkReader
from .wave_adm_reader import WavADMReader from .wave_adm_reader import WavADMReader
from .wave_dbmd_reader import WavDolbyMetadataReader from .wave_dbmd_reader import WavDolbyMetadataReader
from .wave_cues_reader import WavCuesReader from .wave_cues_reader import WavCuesReader
from .wave_smpl_reader import WavSmplReader
#: Calculated statistics about the audio data. #: Calculated statistics about the audio data.
class WavDataDescriptor(NamedTuple): class WavDataDescriptor(NamedTuple):
byte_count: int byte_count: int
frame_count: int frame_count: int
@@ -80,6 +83,9 @@ class WavInfoReader:
#: RIFF cues markers, labels, and notes. #: RIFF cues markers, labels, and notes.
self.cues: Optional[WavCuesReader] = None self.cues: Optional[WavCuesReader] = None
#: Sampler `smpl` metadata
self.smpl: Optional[WavSmplReader] = None
if hasattr(path, 'read'): if hasattr(path, 'read'):
self.get_wav_info(path) self.get_wav_info(path)
self.url = 'about:blank' self.url = 'about:blank'
@@ -110,6 +116,7 @@ class WavInfoReader:
self.info = self._get_info(wavfile, encoding=self.info_encoding) self.info = self._get_info(wavfile, encoding=self.info_encoding)
self.dolby = self._get_dbmd(wavfile) self.dolby = self._get_dbmd(wavfile)
self.cues = self._get_cue(wavfile) self.cues = self._get_cue(wavfile)
self.smpl = self._get_sampler_loops(wavfile)
self.data = self._describe_data() self.data = self._describe_data()
def _find_chunk_data(self, ident, from_stream, def _find_chunk_data(self, ident, from_stream,
@@ -203,6 +210,10 @@ class WavInfoReader:
return WavCuesReader.read_all(f, cue, labls, ltxts, notes, return WavCuesReader.read_all(f, cue, labls, ltxts, notes,
fallback_encoding=self.info_encoding) fallback_encoding=self.info_encoding)
def _get_sampler_loops(self, f):
sampler_data = self._find_chunk_data(b'smpl', f, default_none=True)
return WavSmplReader(sampler_data) if sampler_data else None
# FIXME: this should probably be named "iter()" # FIXME: this should probably be named "iter()"
def walk(self) -> Generator[str, str, Any]: def walk(self) -> Generator[str, str, Any]:
""" """
@@ -210,11 +221,12 @@ class WavInfoReader:
:yields: tuples of the *scope*, *key*, and *value* of :yields: tuples of the *scope*, *key*, and *value* of
each metadatum. The *scope* value will be one of each metadatum. The *scope* value will be one of
"fmt", "data", "ixml", "bext", "info", "dolby", "cues" or "adm". "fmt", "data", "ixml", "bext", "info", "dolby", "cues", "adm" or
"smpl".
""" """
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues', scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues',
'dolby') 'dolby', 'smpl')
for scope in scopes: for scope in scopes:
if scope in ['fmt', 'data']: if scope in ['fmt', 'data']:
@@ -223,10 +235,10 @@ class WavInfoReader:
yield scope, field, attr.__getattribute__(field) yield scope, field, attr.__getattribute__(field)
else: else:
dict = self.__getattribute__(scope).to_dict( mdict = self.__getattribute__(scope).to_dict(
) if self.__getattribute__(scope) else {} ) if self.__getattribute__(scope) else {}
for key in dict.keys(): for key in mdict.keys():
yield scope, key, dict[key] yield scope, key, mdict[key]
def __repr__(self): def __repr__(self):
return 'WavInfoReader({}, {}, {})'.format(self.path, return 'WavInfoReader({}, {}, {})'.format(self.path,

111
wavinfo/wave_smpl_reader.py Normal file
View File

@@ -0,0 +1,111 @@
import struct
from typing import Tuple, NamedTuple, List
class WaveSmplLoop(NamedTuple):
ident: int
loop_type: int
start: int
end: int
fraction: int
repetition_count: int
def loop_type_desc(self):
if self.loop_type == 0:
return 'FORWARD'
elif self.loop_type == 1:
return 'FORWARD_BACKWARD'
elif self.loop_type == 2:
return 'BACKWARD'
elif 3 <= self.loop_type <= 31:
return 'RESERVED'
else:
return 'VENDOR'
def to_dict(self):
return {
'ident': self.ident,
'loop_type': self.loop_type,
'loop_type_description': self.loop_type_desc(),
'start_samples': self.start,
'end_samples': self.end,
'fraction': self.fraction,
'repetition_count': self.repetition_count,
}
class WavSmplReader:
def __init__(self, smpl_data: bytes):
"""
Read sampler metadata from smpl chunk.
"""
header_field_fmt = "<IIIIIIbbbbII"
loop_field_fmt = "<IIIIII"
header_size = struct.calcsize(header_field_fmt)
loop_size = struct.calcsize(loop_field_fmt)
unpacked_data = struct.unpack(header_field_fmt,
smpl_data[0:header_size])
#: The MIDI Manufacturer's Association code for the sampler
#: manufactuer, or 0 if not specific.
self.manufacturer: int = unpacked_data[0]
#: The manufacturer-assigned code for their specific sampler model, or
#: 0 if not specific.
self.product: int = unpacked_data[1]
#: The number of nanoseconds in one audio frame.
self.sample_period_ns: int = unpacked_data[2]
#: The MIDI note number for the loops in this sample
self.midi_note: int = unpacked_data[3]
#: The number of semitones above the MIDI note the loops tune for.
self.midi_pitch_fraction_semis: int = unpacked_data[4]
#: SMPTE timecode format, one of (0, 24, 25, 29, 30)
self.smpte_format: int = unpacked_data[5]
#: 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]
loop_count = unpacked_data[10]
sampler_udata_length = unpacked_data[11]
#: List of loops in the file.
self.sample_loops: List[WaveSmplLoop] = []
loop_buffer = smpl_data[header_size:
header_size + loop_size * loop_count]
for unpacked_loop in struct.iter_unpack(loop_field_fmt, loop_buffer):
self.sample_loops.append(WaveSmplLoop(
ident=unpacked_loop[0],
loop_type=unpacked_loop[1],
start=unpacked_loop[2],
end=unpacked_loop[3],
fraction=unpacked_loop[4],
repetition_count=unpacked_loop[5]))
#: Sampler-specific user data.
self.sampler_udata: bytes = smpl_data[
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_fraction_semis': self.midi_pitch_fraction_semis,
'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,
}