mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
58 Commits
maint-poet
...
master
| 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 |
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
|
||||||
|
|||||||
@@ -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 ---------------------------------------------------
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ iXML
|
|||||||
Sampler Metadata
|
Sampler Metadata
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
* `RecordingBlogs.com — Sample chunk (of a Wave file)<https://www.recordingblogs.com/wiki/sample-chunk-of-a-wave-file>`_
|
* `RecordingBlogs.com — Sample chunk (of a Wave file) <https://www.recordingblogs.com/wiki/sample-chunk-of-a-wave-file>`_
|
||||||
|
|
||||||
RIFF Metadata
|
RIFF Metadata
|
||||||
-------------
|
-------------
|
||||||
|
|||||||
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:
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
# https://python-poetry.org/docs/pyproject/
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["uv_build>=0.8.18,<0.9.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "uv_build"
|
||||||
|
|
||||||
[tool.poetry]
|
[project]
|
||||||
name = "wavinfo"
|
name = "wavinfo"
|
||||||
version = "3.0.1"
|
version = "4.0.0"
|
||||||
description = "Probe WAVE files for all metadata"
|
description = "Probe WAVE files for all metadata"
|
||||||
authors = ["Jamie Hardt <jamiehardt@me.com>"]
|
authors = [{ name = "Jamie Hardt", email = "jamiehardt@me.com"}]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
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"
|
||||||
]
|
]
|
||||||
homepage = "https://github.com/iluvcapra/wavinfo"
|
homepage = "https://github.com/iluvcapra/wavinfo"
|
||||||
repository = "https://github.com/iluvcapra/wavinfo.git"
|
repository = "https://github.com/iluvcapra/wavinfo.git"
|
||||||
@@ -39,17 +38,22 @@ keywords = [
|
|||||||
'broadcast'
|
'broadcast'
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.extras]
|
dependencies = [
|
||||||
doc = ['sphinx', 'sphinx_rtd_theme']
|
"lxml>=6.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[dependency-groups]
|
||||||
wavinfo = 'wavinfo.__main__:main'
|
dev = [
|
||||||
|
"pytest>=8.3.5",
|
||||||
|
"ruff>=0.14.0",
|
||||||
|
]
|
||||||
|
doc = [
|
||||||
|
"sphinx>=7.1.2",
|
||||||
|
"sphinx-rtd-theme>=3.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[project.scripts]
|
||||||
python = "^3.8"
|
wavinfo = "wavinfo:__main__.main"
|
||||||
lxml = "~= 5.3.0"
|
|
||||||
sphinx_rtd_theme = {version= '>= 1.1.1', optional=true}
|
|
||||||
sphinx = {version= '>= 5.3.0', optional=true}
|
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "basic"
|
||||||
@@ -65,3 +69,4 @@ disable = [
|
|||||||
"R0913", # (too-many-arguments)
|
"R0913", # (too-many-arguments)
|
||||||
"W0105", # (pointless-string-statement)
|
"W0105", # (pointless-string-statement)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +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
|
||||||
|
|
||||||
@@ -8,6 +8,9 @@ import json
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
from cmd import Cmd
|
||||||
|
from shlex import split
|
||||||
|
from typing import List, Dict, Union
|
||||||
|
|
||||||
|
|
||||||
class MyJSONEncoder(json.JSONEncoder):
|
class MyJSONEncoder(json.JSONEncoder):
|
||||||
@@ -15,7 +18,7 @@ class MyJSONEncoder(json.JSONEncoder):
|
|||||||
if isinstance(o, Enum):
|
if isinstance(o, Enum):
|
||||||
return o._name_
|
return o._name_
|
||||||
elif isinstance(o, bytes):
|
elif isinstance(o, bytes):
|
||||||
return b64encode(o).decode('ascii')
|
return 'base64:' + b64encode(o).decode('ascii')
|
||||||
else:
|
else:
|
||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
@@ -24,6 +27,85 @@ class MissingDataError(RuntimeError):
|
|||||||
pass
|
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():
|
def main():
|
||||||
version = importlib.metadata.version('wavinfo')
|
version = importlib.metadata.version('wavinfo')
|
||||||
manpath = os.path.dirname(__file__) + "/man"
|
manpath = os.path.dirname(__file__) + "/man"
|
||||||
@@ -51,8 +133,15 @@ def main():
|
|||||||
default=False,
|
default=False,
|
||||||
action='store_true')
|
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)
|
(options, args) = parser.parse_args(sys.argv)
|
||||||
|
|
||||||
|
interactive_dict = []
|
||||||
|
|
||||||
# if options.install_manpages:
|
# if options.install_manpages:
|
||||||
# print("Installing manpages...")
|
# print("Installing manpages...")
|
||||||
# print(f"Docfiles at {__file__}")
|
# print(f"Docfiles at {__file__}")
|
||||||
@@ -98,7 +187,12 @@ def main():
|
|||||||
|
|
||||||
ret_dict['scopes'][scope][name] = value
|
ret_dict['scopes'][scope][name] = value
|
||||||
|
|
||||||
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
|
if options.i:
|
||||||
|
interactive_dict.append(ret_dict)
|
||||||
|
else:
|
||||||
|
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout,
|
||||||
|
indent=2)
|
||||||
|
|
||||||
except MissingDataError as e:
|
except MissingDataError as e:
|
||||||
print("MissingDataError: Missing metadata (%s) in file %s" %
|
print("MissingDataError: Missing metadata (%s) in file %s" %
|
||||||
(e, arg), file=sys.stderr)
|
(e, arg), file=sys.stderr)
|
||||||
@@ -106,6 +200,11 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
if len(interactive_dict) > 0:
|
||||||
|
cli = MetaBrowser()
|
||||||
|
cli.metadata = interactive_dict
|
||||||
|
cli.cmdloop()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
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 ...
|
||||||
@@ -24,6 +25,10 @@ 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,
|
||||||
@@ -8,7 +8,7 @@ class WaveSmplLoop(NamedTuple):
|
|||||||
loop_type: int
|
loop_type: int
|
||||||
start: int
|
start: int
|
||||||
end: int
|
end: int
|
||||||
fraction: int
|
detune_cents: int
|
||||||
repetition_count: int
|
repetition_count: int
|
||||||
|
|
||||||
def loop_type_desc(self):
|
def loop_type_desc(self):
|
||||||
@@ -30,7 +30,7 @@ class WaveSmplLoop(NamedTuple):
|
|||||||
'loop_type_description': self.loop_type_desc(),
|
'loop_type_description': self.loop_type_desc(),
|
||||||
'start_samples': self.start,
|
'start_samples': self.start,
|
||||||
'end_samples': self.end,
|
'end_samples': self.end,
|
||||||
'fraction': self.fraction,
|
'detune_cents': self.detune_cents,
|
||||||
'repetition_count': self.repetition_count,
|
'repetition_count': self.repetition_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ class WavSmplReader:
|
|||||||
Read sampler metadata from smpl chunk.
|
Read sampler metadata from smpl chunk.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
header_field_fmt = "<IIIIIIbbbbII"
|
header_field_fmt = "<IIIIiIbbbbII"
|
||||||
loop_field_fmt = "<IIIIII"
|
loop_field_fmt = "<IIIIiI"
|
||||||
header_size = struct.calcsize(header_field_fmt)
|
header_size = struct.calcsize(header_field_fmt)
|
||||||
loop_size = struct.calcsize(loop_field_fmt)
|
loop_size = struct.calcsize(loop_field_fmt)
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class WavSmplReader:
|
|||||||
self.midi_note: int = unpacked_data[3]
|
self.midi_note: int = unpacked_data[3]
|
||||||
|
|
||||||
#: The number of semitones above the MIDI note the loops tune for.
|
#: The number of semitones above the MIDI note the loops tune for.
|
||||||
self.midi_pitch_fraction_semis: int = unpacked_data[4]
|
self.midi_pitch_detune_cents: int = unpacked_data[4]
|
||||||
|
|
||||||
#: SMPTE timecode format, one of (0, 24, 25, 29, 30)
|
#: SMPTE timecode format, one of (0, 24, 25, 29, 30)
|
||||||
self.smpte_format: int = unpacked_data[5]
|
self.smpte_format: int = unpacked_data[5]
|
||||||
@@ -89,11 +89,14 @@ class WavSmplReader:
|
|||||||
loop_type=unpacked_loop[1],
|
loop_type=unpacked_loop[1],
|
||||||
start=unpacked_loop[2],
|
start=unpacked_loop[2],
|
||||||
end=unpacked_loop[3],
|
end=unpacked_loop[3],
|
||||||
fraction=unpacked_loop[4],
|
detune_cents=unpacked_loop[4],
|
||||||
repetition_count=unpacked_loop[5]))
|
repetition_count=unpacked_loop[5]))
|
||||||
|
|
||||||
#: Sampler-specific user data.
|
#: Sampler-specific user data.
|
||||||
self.sampler_udata: bytes = smpl_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:
|
||||||
header_size + loop_size * loop_count + sampler_udata_length]
|
header_size + loop_size * loop_count + sampler_udata_length]
|
||||||
|
|
||||||
@@ -103,7 +106,7 @@ class WavSmplReader:
|
|||||||
'product': self.product,
|
'product': self.product,
|
||||||
'sample_period_ns': self.sample_period_ns,
|
'sample_period_ns': self.sample_period_ns,
|
||||||
'midi_note': self.midi_note,
|
'midi_note': self.midi_note,
|
||||||
'midi_pitch_fraction_semis': self.midi_pitch_fraction_semis,
|
'midi_pitch_detune_cents': self.midi_pitch_detune_cents,
|
||||||
'smpte_format': self.smpte_format,
|
'smpte_format': self.smpte_format,
|
||||||
'smpte_offset': "%02i:%02i:%02i:%02i" % self.smpte_offset,
|
'smpte_offset': "%02i:%02i:%02i:%02i" % self.smpte_offset,
|
||||||
'loops': [x.to_dict() for x in self.sample_loops],
|
'loops': [x.to_dict() for x in self.sample_loops],
|
||||||
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.
Reference in New Issue
Block a user