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
.vscode/
poetry.lock

View File

@@ -36,6 +36,11 @@ 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
-------------
* `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",
" * `cues`: Cue marker metadata, including labels and notes \n",
" * `dolby`: Dolby recorder and playback metadata\n",
" * `smpl`: Sampler midi note and loop metadata\n",
"\n",
"Each of these is an attribute of a `WavInfoReader` object.\n",
"\n",
@@ -304,7 +305,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
"version": "3.12.5"
}
},
"nbformat": 4,

View File

@@ -1,13 +1,16 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
# https://python-poetry.org/docs/pyproject/
[project]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
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"
dynamic = ["version", "description"]
requires-python = "~=3.8"
classifiers = [
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License',
@@ -20,9 +23,10 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13"
]
dependencies = [
"lxml ~= 5.3.0"
]
homepage = "https://github.com/iluvcapra/wavinfo"
repository = "https://github.com/iluvcapra/wavinfo.git"
documentation = "https://wavinfo.readthedocs.io/"
urls.Tracker = 'https://github.com/iluvcapra/wavinfo/issues'
keywords = [
'waveform',
'metadata',
@@ -35,29 +39,17 @@ keywords = [
'broadcast'
]
[tool.flit.module]
name = "wavinfo"
[tool.poetry.extras]
doc = ['sphinx', 'sphinx_rtd_theme']
[project.optional-dependencies]
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]
[tool.poetry.scripts]
wavinfo = 'wavinfo.__main__:main'
[project.scripts]
wavinfo = "wavinfo.__main__:main"
[tool.flit.external-data]
directory = "data"
[tool.poetry.dependencies]
python = "^3.8"
lxml = "~= 5.3.0"
sphinx_rtd_theme = {version= '>= 1.1.1', optional=true}
sphinx = {version= '>= 5.3.0', optional=true}
[tool.pyright]
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 .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 __version__
import datetime
from optparse import OptionParser
import sys
import os
import json
from enum import Enum
import importlib.metadata
from base64 import b64encode
class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Enum):
return o._name_
elif isinstance(o, bytes):
return b64encode(o).decode('ascii')
else:
return super().default(o)
@@ -21,10 +25,22 @@ class MissingDataError(RuntimeError):
def main():
version = importlib.metadata.version('wavinfo')
manpath = os.path.dirname(__file__) + "/man"
parser = OptionParser()
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',
help='Output ADM XML',
default=False,
@@ -36,6 +52,26 @@ def main():
action='store_true')
(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:]:
try:
this_file = WavInfoReader(path=arg)
@@ -53,7 +89,7 @@ def main():
ret_dict = {
'filename': arg,
'run_date': datetime.datetime.now().isoformat(),
'application': "wavinfo " + __version__,
'application': f"wavinfo {version}",
'scopes': {}
}
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
detected metadata.
.IP "\-\-adm"
Output any Audio Definition Model (ADM) metadata in
Output Audio Definition Model (ADM) XML metadata in
.BR FILE .
.IP "\-\-ixml"
Output any iXML metdata in

View File

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