mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95ba34187a | |||
| 4d2dfcd370 | |||
| 717b6a4117 | |||
| 7393afac95 | |||
| e9450cd65a | |||
| 51b1a2e8b4 | |||
| 53303232b4 | |||
| 3519a0251f | |||
| 79ec1649c4 | |||
| 7c3f4c9b5e | |||
| 2e8224cb3c | |||
| 8ef4186b4c | |||
| cd5346c1cb | |||
|
|
925bf4f8a6 | ||
|
|
e98ba0bf07 | ||
| afca634dc3 | |||
|
|
df9ae0f4d6 | ||
|
|
2f23bcb982 | ||
| 5e641b0963 | |||
| 1b57ad0fac | |||
| c7a34e0064 | |||
| 6b788484da | |||
| 76905f1a40 | |||
| 9ac06040a2 | |||
| 1b78f5b821 | |||
| 03d718b4ad | |||
| 61f79760e6 | |||
|
|
afe5ea9ed3 | ||
|
|
c1205d52e8 | ||
|
|
b82b6b6d43 | ||
|
|
8ef664266f | ||
|
|
dfb7e34fc7 | ||
|
|
2ebdefaab5 | ||
|
|
c609e22270 | ||
|
|
ef9c39f1b6 | ||
|
|
cc9d884ea8 | ||
|
|
94563f69a9 | ||
|
|
2830cb87a4 | ||
|
|
1c8581ff35 | ||
|
|
1d499d9741 | ||
|
|
299f79aeb3 | ||
|
|
a46590df29 | ||
|
|
c6f66b2d6e | ||
|
|
b8617a35e2 | ||
|
|
8cabf948ff | ||
|
|
8a755b4466 | ||
|
|
c13b07e4a3 | ||
|
|
ac37c14b3d | ||
|
|
36e4a02ab8 | ||
|
|
ffc0c48af7 | ||
|
|
206962b218 | ||
|
|
d560e5a9f0 | ||
|
|
98ca1ec462 | ||
|
|
f0353abd4e | ||
|
|
6304666d11 | ||
|
|
d2b0c68dd2 | ||
|
|
a0a9c38cb4 | ||
|
|
f68eea4cd9 | ||
|
|
016e504f65 | ||
|
|
bf536f66ec | ||
|
|
2ab9e940ab | ||
|
|
7104f3c18a | ||
|
|
f04c563fe2 | ||
|
|
06fa3cc422 | ||
|
|
83a44de492 | ||
|
|
d8f57c8607 | ||
|
|
7c3ae745b7 | ||
|
|
dc18b4eb99 | ||
|
|
259994d514 | ||
|
|
9c51a6d146 | ||
|
|
28e0532994 | ||
|
|
29ca62b970 | ||
|
|
77ce1e3bc0 | ||
|
|
82129cee07 | ||
|
|
c249ce058d | ||
|
|
a66049b425 | ||
|
|
e60723afcf | ||
|
|
8b402f310c | ||
|
|
c3c8ba2908 |
6
.github/workflows/python-package.yml
vendored
6
.github/workflows/python-package.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
40
.github/workflows/pythonpublish.yml
vendored
40
.github/workflows/pythonpublish.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -110,3 +110,5 @@ venv_docs/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
poetry.lock
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
 [](https://wavinfo.readthedocs.io/en/latest/?badge=latest) 
|
 [](https://wavinfo.readthedocs.io/en/latest/?badge=latest) 
|
||||||
|
|
||||||
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
|
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
|
||||||
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-flake8.yml)
|
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-ruff.yml)
|
||||||
[](https://codecov.io/gh/iluvcapra/wavinfo)
|
[](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
|
||||||
|
|||||||
@@ -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": {
|
"data": {
|
||||||
"byte_count": 1441434,
|
"byte_count": 576000,
|
||||||
"frame_count": 240239
|
"frame_count": 48000
|
||||||
},
|
},
|
||||||
"ixml": {
|
"ixml": {
|
||||||
"track_list": [
|
"track_list": [
|
||||||
{
|
{
|
||||||
"channel_index": "1",
|
"channel_index": "1",
|
||||||
"interleave_index": "1",
|
"interleave_index": "1",
|
||||||
"name": "MKH516 A",
|
"name": "",
|
||||||
"function": ""
|
"function": "ACN0-FOA"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"channel_index": "2",
|
"channel_index": "2",
|
||||||
"interleave_index": "2",
|
"interleave_index": "2",
|
||||||
"name": "Boom",
|
"name": "",
|
||||||
"function": ""
|
"function": "ACN1-FOA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel_index": "3",
|
||||||
|
"interleave_index": "3",
|
||||||
|
"name": "",
|
||||||
|
"function": "ACN2-FOA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel_index": "4",
|
||||||
|
"interleave_index": "4",
|
||||||
|
"name": "",
|
||||||
|
"function": "ACN3-FOA"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"project": "BMH",
|
"project": "wavinfo Test Project",
|
||||||
"scene": "A101",
|
"scene": null,
|
||||||
"take": "1",
|
"take": null,
|
||||||
"tape": "18Y12M31",
|
"tape": null,
|
||||||
"family_uid": "USSDVGR1112089007124001008206300",
|
"family_uid": "E5DDE719B9484A758162FF7B652383A3",
|
||||||
"family_name": null
|
"family_name": null
|
||||||
},
|
},
|
||||||
"bext": {
|
"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",
|
"description": "wavinfo Test Project Nuendo output",
|
||||||
"originator": "Sound Dev: 702T S#GR1112089007",
|
"originator": "Nuendo",
|
||||||
"originator_ref": "USSDVGR1112089007124001008206301",
|
"originator_ref": "USJPHNNNNNNNNN202829RRRRRRRRR",
|
||||||
"originator_date": "2018-12-31",
|
"originator_date": "2022-12-02",
|
||||||
"originator_time": "12:40:00",
|
"originator_time": "10:21:06",
|
||||||
"time_reference": 2190940753,
|
"time_reference": 172800000,
|
||||||
"version": 1,
|
"version": 2,
|
||||||
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
|
"umid": "base64:k/zr4qE4RiaXyd/fO7GuCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
|
"coding_history": "A=PCM,F=48000,W=24,T=Nuendo\r\n",
|
||||||
"loudness_value": null,
|
"loudness_value": 327.67,
|
||||||
"loudness_range": null,
|
"loudness_range": 327.67,
|
||||||
"max_true_peak": null,
|
"max_true_peak": 327.67,
|
||||||
"max_momentary_loudness": null,
|
"max_momentary_loudness": 327.67,
|
||||||
"max_shortterm_loudness": null
|
"max_shortterm_loudness": 327.67
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ---------------------------------------------------
|
||||||
|
|||||||
@@ -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>`_
|
||||||
|
|||||||
14
docs/source/scopes/smpl.rst
Normal file
14
docs/source/scopes/smpl.rst
Normal 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:
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
210
src/wavinfo/__main__.py
Normal 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()
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
@@ -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,
|
||||||
114
src/wavinfo/wave_smpl_reader.py
Normal file
114
src/wavinfo/wave_smpl_reader.py
Normal 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,
|
||||||
|
}
|
||||||
BIN
tests/test_files/smpl/alarm_citizen_loop1.wav
Normal file
BIN
tests/test_files/smpl/alarm_citizen_loop1.wav
Normal file
Binary file not shown.
BIN
tests/test_files/smpl/alarm_citizen_loop1_fr.wav
Normal file
BIN
tests/test_files/smpl/alarm_citizen_loop1_fr.wav
Normal file
Binary file not shown.
BIN
tests/test_files/smpl/alarm_citizen_loop1_res3.wav
Normal file
BIN
tests/test_files/smpl/alarm_citizen_loop1_res3.wav
Normal file
Binary file not shown.
BIN
tests/test_files/smpl/alarm_citizen_loop1_rev.wav
Normal file
BIN
tests/test_files/smpl/alarm_citizen_loop1_rev.wav
Normal file
Binary file not shown.
BIN
tests/test_files/smpl/alarm_citizen_loop1_udata.wav
Normal file
BIN
tests/test_files/smpl/alarm_citizen_loop1_udata.wav
Normal file
Binary file not shown.
BIN
tests/test_files/smpl/alarm_citizen_loop1_vendFF.wav
Normal file
BIN
tests/test_files/smpl/alarm_citizen_loop1_vendFF.wav
Normal file
Binary file not shown.
15
tests/test_smpl.py
Normal file
15
tests/test_smpl.py
Normal 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)
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user