diff --git a/.gitignore b/.gitignore
index a9c6ddc..186fd5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -110,3 +110,5 @@ venv_docs/
.DS_Store
.vscode/
+
+poetry.lock
diff --git a/docs/source/references.rst b/docs/source/references.rst
index aa388c1..4006cb2 100644
--- a/docs/source/references.rst
+++ b/docs/source/references.rst
@@ -36,6 +36,11 @@ iXML
* `Gallery Software iXML Specification `_
+Sampler Metadata
+----------------
+
+* `RecordingBlogs.com — Sample chunk (of a Wave file)`_
+
RIFF Metadata
-------------
* `1991. Multimedia Programming Interface and Data Specifications 1.0 `_
diff --git a/examples/demo.ipynb b/examples/demo.ipynb
index f05ac2b..7eaf2b6 100644
--- a/examples/demo.ipynb
+++ b/examples/demo.ipynb
@@ -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,
diff --git a/pyproject.toml b/pyproject.toml
index 500d65e..19e7eda 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 "]
+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"
diff --git a/tests/test_files/smpl/alarm_citizen_loop1.wav b/tests/test_files/smpl/alarm_citizen_loop1.wav
new file mode 100644
index 0000000..9b61e91
Binary files /dev/null and b/tests/test_files/smpl/alarm_citizen_loop1.wav differ
diff --git a/tests/test_files/smpl/alarm_citizen_loop1_fr.wav b/tests/test_files/smpl/alarm_citizen_loop1_fr.wav
new file mode 100644
index 0000000..8de35b6
Binary files /dev/null and b/tests/test_files/smpl/alarm_citizen_loop1_fr.wav differ
diff --git a/tests/test_files/smpl/alarm_citizen_loop1_res3.wav b/tests/test_files/smpl/alarm_citizen_loop1_res3.wav
new file mode 100644
index 0000000..4b55dc9
Binary files /dev/null and b/tests/test_files/smpl/alarm_citizen_loop1_res3.wav differ
diff --git a/tests/test_files/smpl/alarm_citizen_loop1_rev.wav b/tests/test_files/smpl/alarm_citizen_loop1_rev.wav
new file mode 100644
index 0000000..b54cf29
Binary files /dev/null and b/tests/test_files/smpl/alarm_citizen_loop1_rev.wav differ
diff --git a/tests/test_files/smpl/alarm_citizen_loop1_vendFF.wav b/tests/test_files/smpl/alarm_citizen_loop1_vendFF.wav
new file mode 100644
index 0000000..a9c7cdf
Binary files /dev/null and b/tests/test_files/smpl/alarm_citizen_loop1_vendFF.wav differ
diff --git a/tests/test_smpl.py b/tests/test_smpl.py
new file mode 100644
index 0000000..0529440
--- /dev/null
+++ b/tests/test_smpl.py
@@ -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)
diff --git a/wavinfo/__init__.py b/wavinfo/__init__.py
index f8d7de0..d5bbfaf 100644
--- a/wavinfo/__init__.py
+++ b/wavinfo/__init__.py
@@ -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'
diff --git a/wavinfo/__main__.py b/wavinfo/__main__.py
index 0fd7428..5d444b7 100644
--- a/wavinfo/__main__.py
+++ b/wavinfo/__main__.py
@@ -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) +'
+ # 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,9 +89,9 @@ 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():
if scope not in ret_dict['scopes'].keys():
ret_dict['scopes'][scope] = {}
diff --git a/data/share/man/man1/wavinfo.1 b/wavinfo/man/man1/wavinfo.1
similarity index 97%
rename from data/share/man/man1/wavinfo.1
rename to wavinfo/man/man1/wavinfo.1
index 58fd7d6..0fe5382 100644
--- a/data/share/man/man1/wavinfo.1
+++ b/wavinfo/man/man1/wavinfo.1
@@ -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
diff --git a/data/share/man/man7/wavinfo.7 b/wavinfo/man/man7/wavinfo.7
similarity index 100%
rename from data/share/man/man7/wavinfo.7
rename to wavinfo/man/man7/wavinfo.7
diff --git a/wavinfo/wave_reader.py b/wavinfo/wave_reader.py
index 6c7c506..ca41c80 100644
--- a/wavinfo/wave_reader.py
+++ b/wavinfo/wave_reader.py
@@ -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,
diff --git a/wavinfo/wave_smpl_reader.py b/wavinfo/wave_smpl_reader.py
new file mode 100644
index 0000000..746689d
--- /dev/null
+++ b/wavinfo/wave_smpl_reader.py
@@ -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 = "