79 Commits

Author SHA1 Message Date
95ba34187a Docs 2025-10-09 23:22:26 -07:00
4d2dfcd370 Docs 2025-10-09 23:21:43 -07:00
717b6a4117 Docs 2025-10-09 23:19:05 -07:00
7393afac95 Docs 2025-10-09 23:17:46 -07:00
e9450cd65a Docs 2025-10-09 23:16:11 -07:00
51b1a2e8b4 Docs 2025-10-09 23:15:19 -07:00
53303232b4 Docs 2025-10-09 23:11:00 -07:00
3519a0251f Docs 2025-10-09 23:09:51 -07:00
79ec1649c4 Docs 2025-10-09 23:09:04 -07:00
7c3f4c9b5e Docs 2025-10-09 23:08:32 -07:00
2e8224cb3c Docs fix 2025-10-09 23:07:39 -07:00
8ef4186b4c Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2025-10-09 23:02:32 -07:00
cd5346c1cb Docs fixups 2025-10-09 23:02:15 -07:00
Jamie Hardt
925bf4f8a6 Update pythonpublish.yml 2025-10-09 22:55:13 -07:00
Jamie Hardt
e98ba0bf07 Update pythonpublish.yml 2025-10-09 22:48:40 -07:00
afca634dc3 modernized build action 2025-10-09 22:44:58 -07:00
Jamie Hardt
df9ae0f4d6 Update pyproject.toml 2025-10-09 22:34:45 -07:00
Jamie Hardt
2f23bcb982 Merge pull request #40 from iluvcapra/maint-uv
Build modernization
2025-10-09 22:33:55 -07:00
5e641b0963 Removing .flake8 file 2025-10-09 22:32:10 -07:00
1b57ad0fac Typo 2025-10-09 22:26:04 -07:00
c7a34e0064 Added 3.14 to test matrix and dropped 3.8 2025-10-09 22:24:45 -07:00
6b788484da Typo 2025-10-09 22:22:12 -07:00
76905f1a40 Updated name of lint workflow 2025-10-09 22:21:11 -07:00
9ac06040a2 Making changes to the workflows 2025-10-09 22:16:12 -07:00
1b78f5b821 Reorganized pyproject, nudged version 2025-10-09 22:08:36 -07:00
03d718b4ad Fixed dumb typo 2025-10-09 22:06:48 -07:00
61f79760e6 Initial work on uv build system
Moved module into src/ and modernized pyproject.toml
2025-10-09 21:49:33 -07:00
Jamie Hardt
afe5ea9ed3 Update pythonpublish.yml
Updated publish action to latest version
2025-09-08 12:49:05 -07:00
Jamie Hardt
c1205d52e8 Merge pull request #38 from iluvcapra/maint-no-mastodon
Workflow Spruce-up
2024-11-26 12:00:18 -08:00
Jamie Hardt
b82b6b6d43 Fixed publish to Bluesky worksflow 2024-11-26 11:56:51 -08:00
Jamie Hardt
8ef664266f Updated flake8 step to use python 3.13 2024-11-26 10:30:24 -08:00
Jamie Hardt
dfb7e34fc7 Update pythonpublish.yml
Updated `checkout` and `setup-python` versions
2024-11-25 18:41:36 -08:00
Jamie Hardt
2ebdefaab5 Update pythonpublish.yml 2024-11-25 18:37:15 -08:00
Jamie Hardt
c609e22270 Update pythonpublish.yml 2024-11-25 18:33:53 -08:00
Jamie Hardt
ef9c39f1b6 Update pythonpublish.yml
Adding posting to Bluesky
2024-11-25 18:32:19 -08:00
Jamie Hardt
cc9d884ea8 Update pythonpublish.yml
Removed Mastodon notification step
2024-11-25 18:07:17 -08:00
Jamie Hardt
94563f69a9 Merge pull request #37 from iluvcapra/feature-interactive
Feature: interactive shell
2024-11-25 11:17:20 -08:00
Jamie Hardt
2830cb87a4 flake8 2024-11-25 11:15:16 -08:00
Jamie Hardt
1c8581ff35 Merge branch 'master' of https://github.com/iluvcapra/wavinfo into feature-interactive 2024-11-25 11:11:04 -08:00
Jamie Hardt
1d499d9741 Merge pull request #36 from iluvcapra/feature-smpl
Feature: smpl Metadata
2024-11-25 11:09:43 -08:00
Jamie Hardt
299f79aeb3 README update and stubbed out docs. 2024-11-25 11:05:32 -08:00
Jamie Hardt
a46590df29 Merge branch 'master' of https://github.com/iluvcapra/wavinfo into feature-smpl 2024-11-25 10:52:46 -08:00
Jamie Hardt
c6f66b2d6e Changes to fix docs 2024-11-25 10:48:58 -08:00
Jamie Hardt
b8617a35e2 Fixing doc dependencies I think 2024-11-25 10:41:05 -08:00
Jamie Hardt
8cabf948ff Merge branch 'master' of https://github.com/iluvcapra/wavinfo into feature-interactive 2024-11-25 10:38:43 -08:00
Jamie Hardt
8a755b4466 Merge pull request #35 from iluvcapra/maint-poetry
Change build system to Poetry
2024-11-25 10:37:05 -08:00
Jamie Hardt
c13b07e4a3 typing fix for python 3.8/3.9 2024-11-25 10:33:07 -08:00
Jamie Hardt
ac37c14b3d flake8 2024-11-25 10:30:02 -08:00
Jamie Hardt
36e4a02ab8 Documenation of base64 output 2024-11-25 10:27:25 -08:00
Jamie Hardt
ffc0c48af7 A small change to report umids as binary data 2024-11-25 10:18:24 -08:00
Jamie Hardt
206962b218 More documentation changes. 2024-11-25 10:16:26 -08:00
Jamie Hardt
d560e5a9f0 Added documentation. 2024-11-25 10:04:50 -08:00
Jamie Hardt
98ca1ec462 Implementing an interactive shell
...for browsing metadata
2024-11-24 16:23:39 -08:00
Jamie Hardt
f0353abd4e Added a test case for sampler udata
And a little marker for base64
2024-11-24 15:10:31 -08:00
Jamie Hardt
6304666d11 Autopep8 2024-11-24 15:05:19 -08:00
Jamie Hardt
d2b0c68dd2 Made sampler udata field nullable 2024-11-24 15:04:00 -08:00
Jamie Hardt
a0a9c38cb4 Assuming detune is signed 2024-11-24 14:37:18 -08:00
Jamie Hardt
f68eea4cd9 Rectified some terminology 2024-11-24 14:35:56 -08:00
Jamie Hardt
016e504f65 Merge branch 'feature-smpl' into maint-poetry 2024-11-24 14:31:50 -08:00
Jamie Hardt
bf536f66ec Tests for smpl 2024-11-24 14:28:32 -08:00
Jamie Hardt
2ab9e940ab Added "smpl" to the list of supported scopes 2024-11-24 13:36:27 -08:00
Jamie Hardt
7104f3c18a Merge branch 'feature-smpl' into maint-poetry 2024-11-24 13:31:52 -08:00
Jamie Hardt
f04c563fe2 Removed extraneous import 2024-11-24 13:26:26 -08:00
Jamie Hardt
06fa3cc422 autopep8 2024-11-24 13:25:29 -08:00
Jamie Hardt
83a44de492 Integrated smpl metadata reading
Now reads from command line and WavInfoReader interface.
2024-11-24 13:24:00 -08:00
Jamie Hardt
d8f57c8607 autopep8 2024-11-24 12:49:27 -08:00
Jamie Hardt
7c3ae745b7 Lints 2024-11-24 12:48:06 -08:00
Jamie Hardt
dc18b4eb99 autopep8 2024-11-24 12:47:09 -08:00
Jamie Hardt
259994d514 Implementation of WaveSmplReader 2024-11-24 12:44:09 -08:00
Jamie Hardt
9c51a6d146 Added test file with smpl metadata from #34 2024-11-24 12:01:30 -08:00
Jamie Hardt
28e0532994 Made the man opening code cleaner 2024-11-23 21:22:06 -08:00
Jamie Hardt
29ca62b970 Autopep8 2024-11-23 21:02:40 -08:00
Jamie Hardt
77ce1e3bc0 Removing "--install-manpages" for now 2024-11-23 21:00:05 -08:00
Jamie Hardt
82129cee07 Clarified a man item 2024-11-23 20:59:15 -08:00
Jamie Hardt
c249ce058d Reorganized man files to fall inside module 2024-11-23 20:56:20 -08:00
Jamie Hardt
a66049b425 Added poetry.lock to gitignore 2024-11-23 20:23:52 -08:00
Jamie Hardt
e60723afcf Added version detection back to output 2024-11-23 20:20:19 -08:00
Jamie Hardt
8b402f310c Changes for poetry 2024-11-23 19:15:16 -08:00
Jamie Hardt
c3c8ba2908 Updated pyproject.toml to poetry 2024-11-23 18:47:20 -08:00
37 changed files with 568 additions and 228 deletions

View File

@@ -1,3 +0,0 @@
[flake8]
per-file-ignores =
wavinfo/__init__.py: F401

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps: steps:
- uses: actions/checkout@v2.5.0 - uses: actions/checkout@v2.5.0
@@ -27,8 +27,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install pytest python -m pip install --group dev
python -m pip install -e . python -m pip install .
- name: Setup FFmpeg - name: Setup FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v2 uses: FedericoCarboni/setup-ffmpeg@v2
- name: Test with pytest - name: Test with pytest

View File

@@ -1,7 +1,7 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Flake8 name: Lint with Ruff
on: on:
push: push:
@@ -11,12 +11,11 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.11"] python-version: ["3.13", "3.14"]
steps: steps:
- uses: actions/checkout@v2.5.0 - uses: actions/checkout@v2.5.0
@@ -27,14 +26,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install flake8 python -m pip install --group dev
python -m pip install -e . python -m pip install .
- name: Lint with flake8 - name: Lint with ruff
run: | run: |
# stop the build if there are Python syntax errors or undefined names ruff check src
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Lint with flake8
run: |
flake8 wavinfo

View File

@@ -8,29 +8,33 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4.2.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v1 uses: actions/setup-python@v5.3.0
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install setuptools build wheel twine lxml - name: Setup uv and Handle Its Cache
- name: Build and publish # You may pin to the exact commit or the version.
env: # uses: hynek/setup-cached-uv@757bedc3f972eb7227a1aa657651f15a8527c817
TWINE_USERNAME: __token__ uses: hynek/setup-cached-uv@v2.3.0
TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }} - name: Build
run: | run: |
python -m build . uv build --wheel
twine upload dist/* - name: Publish to Pypi
- name: Report to Mastodon uses: pypa/gh-action-pypi-publish@v1.13.0
uses: cbrgm/mastodon-github-action@v1.0.1
with: with:
message: | password: ${{ secrets.PYPI_APIKEY }}
I just released a new version of wavinfo, my library for reading WAVE file metadata! # - name: Send Bluesky Post
#sounddesign #filmmaking #audio #python # uses: myConsciousness/bluesky-post@v5
${{ github.server_url }}/${{ github.repository }} # with:
env: # text: |
MASTODON_URL: ${{ secrets.MASTODON_URL }} # I've released a new version of wavinfo, my module for
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} # reading WAVE metadata.
# link-preview-url: ${{ github.server_url }}/${{ github.repository }}
# identifier: ${{ secrets.BLUESKY_APP_USER }}
# password: ${{ secrets.BLUESKY_APP_PASSWORD }}
# service: bsky.social
# retry-count: 1

2
.gitignore vendored
View File

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

View File

@@ -9,11 +9,11 @@ version: 2
build: build:
os: ubuntu-20.04 os: ubuntu-20.04
tools: tools:
python: "3.10" python: "3.13"
# You can also specify other tool versions: jobs:
# nodejs: "16" install:
# rust: "1.55" - pip install --upgrade pip
# golang: "1.17" - pip install --group 'doc'
# Build documentation in the docs/ directory with Sphinx # Build documentation in the docs/ directory with Sphinx
sphinx: sphinx:
@@ -28,5 +28,3 @@ python:
install: install:
- method: pip - method: pip
path: . path: .
extra_requirements:
- doc

View File

@@ -2,7 +2,7 @@
![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/wavinfo) [![Documentation Status](https://readthedocs.org/projects/wavinfo/badge/?version=latest)](https://wavinfo.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/wavinfo.svg) ![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/wavinfo) [![Documentation Status](https://readthedocs.org/projects/wavinfo/badge/?version=latest)](https://wavinfo.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/wavinfo.svg)
[![Tests](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml/badge.svg)](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml) [![Tests](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml/badge.svg)](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
[![Flake8](https://github.com/iluvcapra/wavinfo/actions/workflows/python-flake8.yml/badge.svg)](https://github.com/iluvcapra/wavinfo/actions/workflows/python-flake8.yml) [![Ruff](https://github.com/iluvcapra/wavinfo/actions/workflows/python-ruff.yml/badge.svg)](https://github.com/iluvcapra/wavinfo/actions/workflows/python-ruff.yml)
[![codecov](https://codecov.io/gh/iluvcapra/wavinfo/branch/master/graph/badge.svg?token=9DZQfZENYv)](https://codecov.io/gh/iluvcapra/wavinfo) [![codecov](https://codecov.io/gh/iluvcapra/wavinfo/branch/master/graph/badge.svg?token=9DZQfZENYv)](https://codecov.io/gh/iluvcapra/wavinfo)
# wavinfo # wavinfo
@@ -31,6 +31,7 @@ it is not supported, please submit an issue!
and Dolby Atmos `dbmd` metadata for re-renders and mixdowns. and Dolby Atmos `dbmd` metadata for re-renders and mixdowns.
* Wave embedded [cue markers][cues], cue marker labels, notes and timed ranges as used * Wave embedded [cue markers][cues], cue marker labels, notes and timed ranges as used
by Zoom, iZotope RX, etc. by Zoom, iZotope RX, etc.
* Wave embedded [sampler][smpl] and sample loop metadata.
* The [wav format][format] is also parsed, so you can access the basic sample rate * The [wav format][format] is also parsed, so you can access the basic sample rate
and channel count information. and channel count information.
@@ -38,6 +39,7 @@ it is not supported, please submit an issue!
[format]:https://wavinfo.readthedocs.io/en/latest/classes.html#wavinfo.wave_reader.WavAudioFormat [format]:https://wavinfo.readthedocs.io/en/latest/classes.html#wavinfo.wave_reader.WavAudioFormat
[cues]:https://wavinfo.readthedocs.io/en/latest/scopes/cue.html [cues]:https://wavinfo.readthedocs.io/en/latest/scopes/cue.html
[bext]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html [bext]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html
[smpl]:https://wavinfo.readthedocs.io/en/latest/scopes/smpl.html
[smpte_330m2011]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html#wavinfo.wave_bext_reader.WavBextReader.umid [smpte_330m2011]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html#wavinfo.wave_bext_reader.WavBextReader.umid
[adm]:https://wavinfo.readthedocs.io/en/latest/scopes/adm.html [adm]:https://wavinfo.readthedocs.io/en/latest/scopes/adm.html
[ebu3285s6]:https://wavinfo.readthedocs.io/en/latest/scopes/dolby.html [ebu3285s6]:https://wavinfo.readthedocs.io/en/latest/scopes/dolby.html

View File

@@ -6,89 +6,135 @@ from the command line and output metadata to stdout.
.. code-block:: shell .. code-block:: shell
$ wavinfo [--ixml | --adm] INFILE + $ wavinfo [[-i] | [--ixml | --adm]] INFILE +
By default, `wavinfo` will output a JSON dictionary for each file argument.
Options Options
------- -------
Two option flags will change the behavior of the command: By default, `wavinfo` will output a JSON dictionary for each file argument.
``-i``
`wavinfo` will run in `interactive mode`_.
Two option flags will change the behavior of the command in non-interactive
mode:
``--ixml`` ``--ixml``
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata
of each input wave file, or will emit an error message to stderr if iXML payload of each input wave file, or will emit an error message to stderr if
metadata is not present. iXML metadata is not present.
``--adm`` ``--adm``
The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata
payload of each input wave file, or will emit an error message to stderr if payload of each input wave file, or will emit an error message to stderr if
ADM XML metadata is not present. ADM XML metadata is not present.
These options are mutually-exclusive, with `\-\-adm` taking precedence. These options are mutually-exclusive, with `\-\-adm` taking precedence. The
``--ixml`` and ``--adm`` flags futher take precedence over ``-i``.
Interactive Mode
-----------------
In interactive mode, `wavinfo` will present a command prompt which allows you
to query the files provided on the command line and explore the metadata tree
interactively. Each file on the command line is scanned and presented as a
tree of metadata records.
Commands include:
``ls``
List the available metadata keys at the current level.
``cd``
Traverse to a metadata key in the current level (or enter `..` to go up
to the prevvious level).
``bye``
Exit to the shell.
Type `help` or `?` at the prompt to get a full list of commands.
Example Output Example Output
-------------- --------------
.. attention::
Metadata fields containing binary data, such as the Broadcast-WAV UMID, will
be included in the JSON output as a base-64 encoded string, preceded by the
marker "base64:".
.. code-block:: javascript .. code-block:: javascript
{ {
"filename": "tests/test_files/sounddevices/A101_1.WAV", "filename": "../tests/test_files/nuendo/wavinfo Test Project - Audio - 1OA.wav",
"run_date": "2022-11-26T17:56:38.342935", "run_date": "2024-11-25T10:26:11.280053",
"application": "wavinfo 2.1.0", "application": "wavinfo 3.0.0",
"scopes": { "scopes": {
"fmt": { "fmt": {
"audio_format": 1, "audio_format": 65534,
"channel_count": 2, "channel_count": 4,
"sample_rate": 48000, "sample_rate": 48000,
"byte_rate": 288000, "byte_rate": 576000,
"block_align": 6, "block_align": 12,
"bits_per_sample": 24 "bits_per_sample": 24
},
"data": {
"byte_count": 576000,
"frame_count": 48000
},
"ixml": {
"track_list": [
{
"channel_index": "1",
"interleave_index": "1",
"name": "",
"function": "ACN0-FOA"
}, },
"data": { {
"byte_count": 1441434, "channel_index": "2",
"frame_count": 240239 "interleave_index": "2",
"name": "",
"function": "ACN1-FOA"
}, },
"ixml": { {
"track_list": [ "channel_index": "3",
{ "interleave_index": "3",
"channel_index": "1", "name": "",
"interleave_index": "1", "function": "ACN2-FOA"
"name": "MKH516 A",
"function": ""
},
{
"channel_index": "2",
"interleave_index": "2",
"name": "Boom",
"function": ""
}
],
"project": "BMH",
"scene": "A101",
"take": "1",
"tape": "18Y12M31",
"family_uid": "USSDVGR1112089007124001008206300",
"family_name": null
}, },
"bext": { {
"description": "sSPEED=023.976-ND\r\nsTAKE=1\r\nsUBITS=$12311801\r\nsSWVER=2.67\r\nsPROJECT=BMH\r\nsSCENE=A101\r\nsFILENAME=A101_1.WAV\r\nsTAPE=18Y12M31\r\nsTRK1=MKH516 A\r\nsTRK2=Boom\r\nsNOTE=\r\n", "channel_index": "4",
"originator": "Sound Dev: 702T S#GR1112089007", "interleave_index": "4",
"originator_ref": "USSDVGR1112089007124001008206301", "name": "",
"originator_date": "2018-12-31", "function": "ACN3-FOA"
"originator_time": "12:40:00",
"time_reference": 2190940753,
"version": 1,
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
"loudness_value": null,
"loudness_range": null,
"max_true_peak": null,
"max_momentary_loudness": null,
"max_shortterm_loudness": null
} }
],
"project": "wavinfo Test Project",
"scene": null,
"take": null,
"tape": null,
"family_uid": "E5DDE719B9484A758162FF7B652383A3",
"family_name": null
},
"bext": {
"description": "wavinfo Test Project Nuendo output",
"originator": "Nuendo",
"originator_ref": "USJPHNNNNNNNNN202829RRRRRRRRR",
"originator_date": "2022-12-02",
"originator_time": "10:21:06",
"time_reference": 172800000,
"version": 2,
"umid": "base64:k/zr4qE4RiaXyd/fO7GuCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"coding_history": "A=PCM,F=48000,W=24,T=Nuendo\r\n",
"loudness_value": 327.67,
"loudness_range": 327.67,
"max_true_peak": 327.67,
"max_momentary_loudness": 327.67,
"max_shortterm_loudness": 327.67
} }
} }
}

View File

@@ -12,24 +12,24 @@
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
# #
# import importlib
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('../../src'))
sys.path.insert(0, os.path.abspath("../../..")) sys.path.insert(0, os.path.abspath("../../../src"))
print(sys.path) print(sys.path)
import wavinfo
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = u'wavinfo' project = u'wavinfo'
copyright = u'2018-2023, Jamie Hardt' copyright = u'2018-2025, Jamie Hardt'
author = u'Jamie Hardt' author = u'Jamie Hardt'
# The short X.Y version # The short X.Y version
version = wavinfo.__short_version__ version = "4.0"
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = wavinfo.__version__ release = "4.0.0"
# release = importlib.metadata.version("wavinfo")
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

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

@@ -0,0 +1,14 @@
Sampler Metadata
=================
Class Reference
---------------
.. automodule:: wavinfo.wave_smpl_reader
.. autoclass:: wavinfo.wave_smpl_reader.WavSmplReader
:members:
.. autoclass:: wavinfo.wave_smpl_reader.WaveSmplLoop
:members:

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,28 +1,31 @@
[build-system] [build-system]
requires = ["flit_core >=3.2,<4"] requires = ["uv_build>=0.8.18,<0.9.0"]
build-backend = "flit_core.buildapi" build-backend = "uv_build"
[project] [project]
name = "wavinfo" name = "wavinfo"
authors = [{name = "Jamie Hardt", email = "jamiehardt@me.com"}] version = "4.0.0"
description = "Probe WAVE files for all metadata"
authors = [{ name = "Jamie Hardt", email = "jamiehardt@me.com"}]
license = "MIT"
readme = "README.md" readme = "README.md"
dynamic = ["version", "description"] requires-python = ">=3.8"
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',
'Topic :: Multimedia', 'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13" "Programming Language :: Python :: 3.13",
] "Programming Language :: Python :: 3.14"
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 = [ keywords = [
'waveform', 'waveform',
'metadata', 'metadata',
@@ -35,29 +38,22 @@ keywords = [
'broadcast' 'broadcast'
] ]
[tool.flit.module] dependencies = [
name = "wavinfo" "lxml>=6.0.2",
[project.optional-dependencies]
doc = [
'sphinx >= 5.3.0',
'sphinx_rtd_theme >= 1.1.1',
] ]
[project.urls] [dependency-groups]
Home = "https://github.com/iluvcapra/wavinfo" dev = [
Documentation = "https://wavinfo.readthedocs.io/" "pytest>=8.3.5",
Source = "https://github.com/iluvcapra/wavinfo.git" "ruff>=0.14.0",
Issues = 'https://github.com/iluvcapra/wavinfo/issues' ]
doc = [
[project.entry_points.console_scripts] "sphinx>=7.1.2",
wavinfo = 'wavinfo.__main__:main' "sphinx-rtd-theme>=3.0.2",
]
[project.scripts] [project.scripts]
wavinfo = "wavinfo.__main__:main" wavinfo = "wavinfo:__main__.main"
[tool.flit.external-data]
directory = "data"
[tool.pyright] [tool.pyright]
typeCheckingMode = "basic" typeCheckingMode = "basic"
@@ -73,3 +69,4 @@ disable = [
"R0913", # (too-many-arguments) "R0913", # (too-many-arguments)
"W0105", # (pointless-string-statement) "W0105", # (pointless-string-statement)
] ]

View File

@@ -2,8 +2,8 @@
Probe WAVE Files for iXML, Broadcast-WAVE and other metadata. Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.
""" """
__all__ = ['WavInfoReader', 'WavInfoEOFError']
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'

210
src/wavinfo/__main__.py Normal file
View File

@@ -0,0 +1,210 @@
from . import WavInfoReader
import datetime
from optparse import OptionParser
import sys
import os
import json
from enum import Enum
import importlib.metadata
from base64 import b64encode
from cmd import Cmd
from shlex import split
from typing import List, Dict, Union
class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Enum):
return o._name_
elif isinstance(o, bytes):
return 'base64:' + b64encode(o).decode('ascii')
else:
return super().default(o)
class MissingDataError(RuntimeError):
pass
class MetaBrowser(Cmd):
prompt = "(wavinfo) "
metadata: Union[List, Dict]
path: List[str] = []
@property
def cwd(self):
root: List | Dict = self.metadata
for key in self.path:
if isinstance(root, list):
root = root[int(key)]
else:
root = root[key]
return root
@staticmethod
def print_value(collection, key):
val = collection[key]
if isinstance(val, int):
print(f" - {key}: {val}")
elif isinstance(val, str):
print(f" - {key}: \"{val}\"")
elif isinstance(val, dict):
print(f" - {key}: Dict ({len(val)} keys)")
elif isinstance(val, list):
print(f" - {key}: List ({len(val)} keys)")
elif isinstance(val, bytes):
print(f" - {key}: ({len(val)} bytes)")
elif val is None:
print(f" - {key}: (NO VALUE)")
else:
print(f" - {key}: Unknown")
def do_ls(self, _):
'List items at the current node: LS'
root = self.cwd
if isinstance(root, list):
print("List:")
for i in range(len(root)):
self.print_value(root, i)
elif isinstance(root, dict):
print("Dictionary:")
for key in root:
self.print_value(root, key)
else:
print("Cannot print node, is not a list or dictionary.")
def do_cd(self, args):
'Switch to a different node: CD node-name | ".."'
argv = split(args)
if argv[0] == "..":
self.path = self.path[0:-1]
else:
if isinstance(self.cwd, list):
if int(argv[0]) < len(self.cwd):
self.path = self.path + [argv[0]]
else:
print(f"Index {argv[0]} does not exist")
elif isinstance(self.cwd, dict):
if argv[0] in self.cwd.keys():
self.path = self.path + [argv[0]]
else:
print(f"Key \"{argv[0]}\" does not exist")
if len(self.path) > 0:
self.prompt = "(" + "/".join(self.path) + ") "
else:
self.prompt = "(wavinfo) "
def do_bye(self, _):
'Exit the interactive browser: BYE'
return True
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,
action='store_true')
parser.add_option('--ixml', dest='ixml',
help='Output iXML',
default=False,
action='store_true')
parser.add_option('-i',
help='Read metadata with an interactive prompt',
default=False,
action='store_true')
(options, args) = parser.parse_args(sys.argv)
interactive_dict = []
# 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)
if options.adm:
if this_file.adm:
sys.stdout.write(this_file.adm.xml_str())
else:
raise MissingDataError("adm")
elif options.ixml:
if this_file.ixml:
sys.stdout.write(this_file.ixml.xml_str())
else:
raise MissingDataError("ixml")
else:
ret_dict = {
'filename': arg,
'run_date': datetime.datetime.now().isoformat(),
'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] = {}
ret_dict['scopes'][scope][name] = value
if options.i:
interactive_dict.append(ret_dict)
else:
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout,
indent=2)
except MissingDataError as e:
print("MissingDataError: Missing metadata (%s) in file %s" %
(e, arg), file=sys.stderr)
continue
except Exception as e:
raise e
if len(interactive_dict) > 0:
cli = MetaBrowser()
cli.metadata = interactive_dict
cli.cmdloop()
if __name__ == "__main__":
main()

View File

@@ -3,6 +3,7 @@
wavinfo \- probe wave files for metadata wavinfo \- probe wave files for metadata
.SH SYNOPSIS .SH SYNOPSIS
.SY wavinfo .SY wavinfo
.I "[\-i]"
.I "[\-\-adm]" .I "[\-\-adm]"
.I "[\-\-ixml]" .I "[\-\-ixml]"
.I FILE ... .I FILE ...
@@ -17,13 +18,17 @@ 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
.BR FILE . .BR FILE .
.IP "\-h, \-\-help" .IP "\-h, \-\-help"
Print brief help. Print brief help.
.IP "\-i"
Enter
.I "interactive mode"
and browse metadata in FILE with an interactive command prompt.
.SH DETAILED DESCRIPTION .SH DETAILED DESCRIPTION
.B wavinfo .B wavinfo
collects metadata according to different collects metadata according to different

View File

@@ -89,7 +89,7 @@ class WavBextReader:
# umid_str = umid_parsed.basic_umid_to_str() # umid_str = umid_parsed.basic_umid_to_str()
# else: # else:
umid_str = None # umid_str = None
return {'description': self.description, return {'description': self.description,
'originator': self.originator, 'originator': self.originator,
@@ -98,7 +98,7 @@ class WavBextReader:
'originator_time': self.originator_time, 'originator_time': self.originator_time,
'time_reference': self.time_reference, 'time_reference': self.time_reference,
'version': self.version, 'version': self.version,
'umid': umid_str, 'umid': self.umid,
'coding_history': self.coding_history, 'coding_history': self.coding_history,
'loudness_value': self.loudness_value, 'loudness_value': self.loudness_value,
'loudness_range': self.loudness_range, 'loudness_range': self.loudness_range,

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,

View File

@@ -0,0 +1,114 @@
import struct
from typing import Tuple, NamedTuple, List
class WaveSmplLoop(NamedTuple):
ident: int
loop_type: int
start: int
end: int
detune_cents: 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,
'detune_cents': self.detune_cents,
'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_detune_cents: 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],
detune_cents=unpacked_loop[4],
repetition_count=unpacked_loop[5]))
#: Sampler-specific user data.
self.sampler_udata: bytes | None = None
if sampler_udata_length > 0:
self.sampler_udata = 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_detune_cents': self.midi_pitch_detune_cents,
'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,
}

Binary file not shown.

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

@@ -1,75 +0,0 @@
import datetime
from . import WavInfoReader
from . import __version__
from optparse import OptionParser
import sys
import json
from enum import Enum
class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Enum):
return o._name_
else:
return super().default(o)
class MissingDataError(RuntimeError):
pass
def main():
parser = OptionParser()
parser.usage = 'wavinfo (--adm | --ixml) <FILE> +'
parser.add_option('--adm', dest='adm',
help='Output ADM XML',
default=False,
action='store_true')
parser.add_option('--ixml', dest='ixml',
help='Output iXML',
default=False,
action='store_true')
(options, args) = parser.parse_args(sys.argv)
for arg in args[1:]:
try:
this_file = WavInfoReader(path=arg)
if options.adm:
if this_file.adm:
sys.stdout.write(this_file.adm.xml_str())
else:
raise MissingDataError("adm")
elif options.ixml:
if this_file.ixml:
sys.stdout.write(this_file.ixml.xml_str())
else:
raise MissingDataError("ixml")
else:
ret_dict = {
'filename': arg,
'run_date': datetime.datetime.now().isoformat(),
'application': "wavinfo " + __version__,
'scopes': {}
}
for scope, name, value in this_file.walk():
if scope not in ret_dict['scopes'].keys():
ret_dict['scopes'][scope] = {}
ret_dict['scopes'][scope][name] = value
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
except MissingDataError as e:
print("MissingDataError: Missing metadata (%s) in file %s" %
(e, arg), file=sys.stderr)
continue
except Exception as e:
raise e
if __name__ == "__main__":
main()