mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9b4f3d7ede | ||
|
|
38eddccf85 | ||
|
|
d3e8349d81 | ||
|
|
57603ff618 | ||
|
|
e7d5f612ea | ||
|
|
b322c8171b | ||
|
|
7e5c888e32 | ||
|
|
275ac10636 | ||
|
|
38601c64db | ||
|
|
067cca82b6 | ||
|
|
37ae8de5b0 | ||
|
|
a20e9dd9ac | ||
|
|
94a84b49dd | ||
|
|
4210905e17 | ||
|
|
a8ede17201 | ||
|
|
8579dc0693 | ||
|
|
89e9959a43 | ||
|
|
bfd2217e23 | ||
|
|
f32055964d | ||
|
|
8e97c2f7b0 | ||
|
|
6d0fee02fc | ||
|
|
10a28f8fb3 | ||
|
|
514cfe0e75 | ||
|
|
ab42cba5b0 | ||
|
|
73a9f93beb | ||
|
|
3071bad007 | ||
|
|
dbb282ad07 | ||
|
|
51ca03816a | ||
|
|
6107342e98 | ||
|
|
adf90612cd | ||
|
|
a196e4786e | ||
|
|
e2ca087e08 | ||
|
|
c5841a5fd0 | ||
|
|
2e5cd4331f | ||
|
|
5e07d01688 | ||
|
|
a01d791262 | ||
|
|
ca7a177ea6 | ||
|
|
4206cd4473 | ||
|
|
5b1e4ab631 | ||
|
|
f978927648 | ||
|
|
6575a0c442 | ||
|
|
f5be5b36d7 | ||
|
|
8a58df2b87 | ||
|
|
3817357fac | ||
|
|
4f51584fe9 | ||
|
|
86a4edc983 | ||
|
|
ce2e1fe8bc | ||
|
|
6a10cd8427 | ||
|
|
d75e55e870 | ||
|
|
4f3ea72c98 | ||
|
|
32b0878229 | ||
|
|
9fee03a67b | ||
|
|
a2ea978de0 | ||
|
|
bfeb7ed651 | ||
|
|
f978c5cf8b | ||
|
|
41b84b8399 | ||
|
|
77275a7351 | ||
|
|
c25ac56555 | ||
|
|
99118367e9 | ||
|
|
c002120c61 | ||
|
|
d7540b0a79 | ||
|
|
d04af2d194 | ||
|
|
bbbe947f3b | ||
|
|
71a6d752ca | ||
|
|
42c0f9ce0d | ||
|
|
75ec68f500 | ||
|
|
f3f9f6b784 | ||
|
|
7bc5378304 | ||
|
|
45c6e90db6 | ||
|
|
8da8e0f4f4 | ||
|
|
9e41d39b26 | ||
|
|
cd5aacfe10 |
3
.flake8
Normal file
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[flake8]
|
||||||
|
per-file-ignores =
|
||||||
|
wavinfo/__init__.py: F401
|
||||||
26
.github/ISSUE_TEMPLATE/add-support-for-new-metadata-type.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/add-support-for-new-metadata-type.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: Add Support For New Metadata Type
|
||||||
|
about: For adding support for a new type of metadata
|
||||||
|
title: "[METADATA]"
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the type of metadata you want to read:**
|
||||||
|
???
|
||||||
|
|
||||||
|
**List some applications that read and write this metadata:**
|
||||||
|
???
|
||||||
|
|
||||||
|
**List the authorities or organizations that use and standardize this metadata:**
|
||||||
|
???
|
||||||
|
|
||||||
|
**URL for example WAVE file with this metadata:**
|
||||||
|
???
|
||||||
|
|
||||||
|
**(Optional) Four-character code identifier for this metadata (if known):**
|
||||||
|
???
|
||||||
|
|
||||||
|
**(Optional) URLs for documentation of this metadata:**
|
||||||
|
???
|
||||||
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.5.0
|
- uses: actions/checkout@v2.5.0
|
||||||
|
|||||||
40
.github/workflows/python-flake8.yml
vendored
Normal file
40
.github/workflows/python-flake8.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
name: Flake8
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.5.0
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4.3.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install flake8
|
||||||
|
python -m pip install -e .
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
|
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
|
||||||
12
.github/workflows/python-package.yml
vendored
12
.github/workflows/python-package.yml
vendored
@@ -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: Python Lint and Test
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -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"]
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.5.0
|
- uses: actions/checkout@v2.5.0
|
||||||
@@ -27,16 +27,10 @@ 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 pytest
|
python -m pip install pytest
|
||||||
python -m pip install -e .
|
python -m pip install -e .
|
||||||
- name: Setup FFmpeg
|
- name: Setup FFmpeg
|
||||||
uses: FedericoCarboni/setup-ffmpeg@v2
|
uses: FedericoCarboni/setup-ffmpeg@v2
|
||||||
- name: Lint with flake8
|
|
||||||
run: |
|
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
|
||||||
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: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
pytest
|
pytest
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -110,3 +110,5 @@ venv_docs/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
poetry.lock
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -1,34 +1,45 @@
|
|||||||
[](https://wavinfo.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/wavinfo/) 
|
 [](https://pypi.org/project/wavinfo/) 
|
||||||
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
|
 [](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-flake8.yml)
|
||||||
[](https://codecov.io/gh/iluvcapra/wavinfo)
|
[](https://codecov.io/gh/iluvcapra/wavinfo)
|
||||||
|
|
||||||
# wavinfo
|
# wavinfo
|
||||||
|
|
||||||
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64]
|
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64]
|
||||||
and extract extended metadata, with an emphasis on film, video and
|
and extract extended metadata. `wavinfo` has an emphasis on film, video and
|
||||||
professional music production.
|
professional music production but can read many other kinds.
|
||||||
|
|
||||||
|
If you are trying to read a particular kind of metadata from a WAV file and
|
||||||
|
it is not supported, please submit an issue!
|
||||||
|
|
||||||
|
|
||||||
## Metadata Support
|
## Metadata Support
|
||||||
|
|
||||||
`wavinfo` reads:
|
`wavinfo` reads:
|
||||||
|
|
||||||
* [Broadcast-WAVE][bext] metadata, including embedded program
|
* All defined [Broadcast-WAVE][bext] fields, including embedded program
|
||||||
loudness, coding history and [SMPTE UMID][smpte_330m2011].
|
loudness, coding history and [SMPTE UMID][smpte_330m2011].
|
||||||
* [Audio Definition Model (ADM)][adm] track metadata and schema, including
|
|
||||||
channel, pack formats,
|
|
||||||
object, content and programme.
|
|
||||||
* [Dolby Digital Plus][ebu3285s6] and Dolby Atmos `dbmd` metadata.
|
|
||||||
* [iXML][ixml] production recorder metadata, including project, scene, and
|
* [iXML][ixml] production recorder metadata, including project, scene, and
|
||||||
take tags, recorder notes and file family information.
|
take tags, recorder notes and file family information.
|
||||||
* iXML `STEINBERG` sound library attributes.
|
* iXML `STEINBERG` sound library attributes.
|
||||||
* Wave embedded cue markers, cue marker labels, notes and timed ranges as used
|
* All known [RIFF INFO][info-tags] metadata fields.
|
||||||
|
* [Audio Definition Model (ADM)][adm] track metadata and schema, including
|
||||||
|
channel, pack formats,
|
||||||
|
object, content and programme, including [Dolby Digital Plus][ebu3285s6]
|
||||||
|
and Dolby Atmos `dbmd` metadata for re-renders and mixdowns.
|
||||||
|
* Wave embedded [cue markers][cues], cue marker labels, notes and timed ranges as used
|
||||||
by Zoom, iZotope RX, etc.
|
by Zoom, iZotope RX, etc.
|
||||||
* Most of the common [RIFF INFO][info-tags] metadata fields.
|
* Wave embedded [sampler][smpl] and sample loop metadata.
|
||||||
* The __wav 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.
|
||||||
|
|
||||||
|
|
||||||
|
[format]:https://wavinfo.readthedocs.io/en/latest/classes.html#wavinfo.wave_reader.WavAudioFormat
|
||||||
|
[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
|
||||||
@@ -58,6 +69,12 @@ The package also installs a shell command:
|
|||||||
$ wavinfo test_files/A101_1.WAV
|
$ wavinfo test_files/A101_1.WAV
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Contributions!
|
||||||
|
|
||||||
|
Any new or different kind of metadata you find, or any
|
||||||
|
new or different use of exising metadata you encounter, please submit
|
||||||
|
an Issue or Pull Request!
|
||||||
|
|
||||||
## Other Resources
|
## Other Resources
|
||||||
|
|
||||||
* For other file formats and ID3 decoding,
|
* For other file formats and ID3 decoding,
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
.TH waveinfo 7 "2023-11-07" "Jamie Hardt" "Miscellaneous Information Manuals"
|
|
||||||
.SH NAME
|
|
||||||
wavinfo \- information about wave sound file metadata
|
|
||||||
.\" .SH DESCRIPTION
|
|
||||||
.SH CHUNK MENAGERIE
|
|
||||||
A list of chunks that you may find in a wave file from our experience.
|
|
||||||
.SS Essential WAV Chunks
|
|
||||||
.IP fmt
|
|
||||||
Defines the format of the audio in the
|
|
||||||
.I data
|
|
||||||
chunk: the audio codec, the sample rate, bit depth, channel count, block
|
|
||||||
alignment and other data. May take an "extended" form, with additional data
|
|
||||||
(such as channel speaker assignments) if there are more than two channels in
|
|
||||||
the file or if it is a compressed format.
|
|
||||||
.IP data
|
|
||||||
The audio data itself. PCM audio data is always stored as interleaved samples.
|
|
||||||
.IP JUNK
|
|
||||||
A region of the file not currently in use. Clients sometimes add these before
|
|
||||||
the
|
|
||||||
.I data
|
|
||||||
chunk in order to align the beginning of the audio data with a memory page
|
|
||||||
boundary (this can make memory-mapped reads from a wave file a little more
|
|
||||||
efficient). A
|
|
||||||
.I JUNK
|
|
||||||
chunk is often placed at the beginning of a WAVE file to reserve space for
|
|
||||||
a
|
|
||||||
.I ds64
|
|
||||||
chunk that will be written to the file at the end of recording, in the event
|
|
||||||
that after the file is finalized, it exceeds the RIFF size limit. Thus a WAVE
|
|
||||||
file can be upgraded in-place to an RF64 without re-writing the audio data.
|
|
||||||
.IP fact
|
|
||||||
Fact chunks record the number of samples in the decoded audio stream. It's only
|
|
||||||
present in WAVE files that contain compressed audio.
|
|
||||||
.IP "LIST or list"
|
|
||||||
(Both have been seen) Not a chunk type itself but signals to a RIFF parser that
|
|
||||||
this chunk contains chunks itself. A LIST chunk's payload will begin with a
|
|
||||||
four-character code identifying the form of the list, and is then followed
|
|
||||||
by chunks of the standard key-length-data form, which may themselves be
|
|
||||||
LISTs that themselves contain child chunks. WAVE files don't tend to have a
|
|
||||||
very deep heirarchy of chunks, compared to AVI files.
|
|
||||||
.SS RIFF Metadata
|
|
||||||
The RIFF container format has a metadata system common to all RIFF files, WAVE
|
|
||||||
being the most common at present, AVI being another very common format
|
|
||||||
historically.
|
|
||||||
.IP INFO
|
|
||||||
A
|
|
||||||
.I LIST
|
|
||||||
form containing a flat list of chunks, each containing text metadata. The role
|
|
||||||
of the string, like "Artist", "Composer", "Comment", "Engineer" etc. are given
|
|
||||||
by the four-character code: "Artist" is
|
|
||||||
.IR IART ,
|
|
||||||
Composer is
|
|
||||||
.IR ICMP ,
|
|
||||||
engineer is
|
|
||||||
.IR IENG ,
|
|
||||||
Comment is
|
|
||||||
.IR ICMT ,
|
|
||||||
etc.
|
|
||||||
.IP cue
|
|
||||||
A binary list of cues, which are timed points within the audio data.
|
|
||||||
.IP adtl
|
|
||||||
A
|
|
||||||
.I LIST
|
|
||||||
form containing text labels
|
|
||||||
.RI ( labl )
|
|
||||||
for the cues in the
|
|
||||||
.I cue
|
|
||||||
chunk, "notes"
|
|
||||||
.RI ( note ,
|
|
||||||
which are structurally identical to
|
|
||||||
.I labl
|
|
||||||
but hosts tend to use notes for longer text), and "length text"
|
|
||||||
.I ltxt
|
|
||||||
metadata records, which can give a cue a length, making it a range, and a text
|
|
||||||
field that defines its own encoding.
|
|
||||||
.IP CSET
|
|
||||||
Defines the character set for all text fields in
|
|
||||||
.IR INFO ,
|
|
||||||
.I adtl
|
|
||||||
and other RIFF-defined text fields. By default, all of the text in RIFF
|
|
||||||
metadata fields is Windows Latin 1/ISO 8859-1, though as time passes many
|
|
||||||
clients have simply taken to sticking UTF-8 into these fields. The
|
|
||||||
.I CSET
|
|
||||||
cannot represent UTF-8 as a valid option for text encoding, it only speaks
|
|
||||||
Windows codepages, and we've never seen one in a WAVE file in any event and
|
|
||||||
it's vanishingly likely an audio app would recognize one if it saw it.
|
|
||||||
.SS Broadcast-WAVE Metadata
|
|
||||||
Broadcast-WAVE is a set of extensions to WAVE files to facilitate media
|
|
||||||
production maintained by the EBU.
|
|
||||||
.IP bext
|
|
||||||
A multi-field structure containing mostly fixed-width text data capturing
|
|
||||||
essential production information: a 256 character free description field,
|
|
||||||
originator name and a unique reference, recording date and time, a frame-based
|
|
||||||
timestamp for sample-accurate recording time, and a coding history record. The
|
|
||||||
extended form of the structure can hold a SMPTE UMID (a kind of UUID, which
|
|
||||||
may also contain timestamp and geolocation data) and pre-computed program
|
|
||||||
loudness measurements.
|
|
||||||
.IP peak
|
|
||||||
A binary data structure containing the peak envelope for the audio data, for
|
|
||||||
use by clients to generate a waveform overview.
|
|
||||||
.SS Audio Definition Model Metadata
|
|
||||||
Audio Definition Model (ADM) metadata is a metadata standard for audio
|
|
||||||
broadcast and distribution maintained by the ITU.
|
|
||||||
.IP chna
|
|
||||||
A binary list that associates individual channels in the file to entities
|
|
||||||
in the ADM XML document stored in the
|
|
||||||
.I axml
|
|
||||||
chunk. A
|
|
||||||
.I chna
|
|
||||||
chunk will always appear with an
|
|
||||||
.I axml
|
|
||||||
chunk and vice versa.
|
|
||||||
.IP axml
|
|
||||||
Contains an XML document with Audio Definition Model metadata. ADM metadata
|
|
||||||
describes the program the WAVE file belongs to, role, channel assignment,
|
|
||||||
and encoding properties of individual channels in the WAVE file, and if the
|
|
||||||
WAVE file contains object-based audio, it will also give all of the positioning
|
|
||||||
and panning automation envelopes.
|
|
||||||
.IP bxml
|
|
||||||
This is defined by the ITU as a gzip-compressed version of the
|
|
||||||
.I axml
|
|
||||||
chunk.
|
|
||||||
.IP sxml
|
|
||||||
This is a hybrid binary/gzip-compressed-XML chunk that associates ADM
|
|
||||||
documents with timed ranges of a WAVE file.
|
|
||||||
.SS Dolby Metadata
|
|
||||||
.IP dbmd
|
|
||||||
Records hints for Dolby playback applications for downmixing, level
|
|
||||||
normalization and other things.
|
|
||||||
.SS Proprietary Chunks
|
|
||||||
.IP ovwf
|
|
||||||
.B (Pro Tools)
|
|
||||||
Pre-computed waveform overview data.
|
|
||||||
.IP regn
|
|
||||||
.B (Pro Tools)
|
|
||||||
Region and cue point metadata.
|
|
||||||
.SS Chunks of Unknown Purpose
|
|
||||||
.IP elm1
|
|
||||||
.IP minf
|
|
||||||
.IP umid
|
|
||||||
.SH HISTORY
|
|
||||||
The oldest document that defines the form of a Wave file is the
|
|
||||||
.I Multimedia Programming Interface and Data Specifications 1.0
|
|
||||||
of August 1991.
|
|
||||||
.\" .SH REFERENCES
|
|
||||||
.\" .SS ESSENTIAL FILE FORMAT
|
|
||||||
.\" .TP
|
|
||||||
.\" .UR https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
|
||||||
.\" Multimedia Programming Interface and Data Specifications 1.0
|
|
||||||
.\" .UE
|
|
||||||
.\" The original definition of the
|
|
||||||
.\" .I RIFF
|
|
||||||
.\" container, the
|
|
||||||
.\" .I WAVE
|
|
||||||
.\" form, the original metadata facilites, and things like language, country and
|
|
||||||
.\" dialect enumerations.
|
|
||||||
.\" .TP
|
|
||||||
.\" .UR https://datatracker.ietf.org/doc/html/rfc2361
|
|
||||||
.\" RFC 2361
|
|
||||||
.\" .UE
|
|
||||||
.\" A large RFC compilation of all of the known (in 1998) audio encoding formats
|
|
||||||
.\" in use. 104 different codecs are documented with a name, the corresponding
|
|
||||||
.\" magic number, and a vendor contact name, phone number and address (no
|
|
||||||
.\" emails, strangely). Almost all of these are of historical interest only.
|
|
||||||
.\" .SS RF64/Extended WAVE Format
|
|
||||||
.\"
|
|
||||||
.\" .TP
|
|
||||||
.\" .UR https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
|
|
||||||
.\" ITU Recommendation BS.2088-1-2019
|
|
||||||
.\" .UE
|
|
||||||
.\" BS.2088 gives a detailed description of the internals of an RF64 file,
|
|
||||||
.\" .I ds64
|
|
||||||
.\" structure and all formal requirements. It also defines the use of
|
|
||||||
.\" .IR <axml> ,
|
|
||||||
.\" .IR <bxml> ,
|
|
||||||
.\" .IR <sxml> ,
|
|
||||||
.\" and
|
|
||||||
.\" .I <chna>
|
|
||||||
.\" metadata chunks for the carriage of Audio Definition Model metadata.
|
|
||||||
.\" .TP
|
|
||||||
.\" .UR https://tech.ebu.ch/docs/tech/tech3306.pdf
|
|
||||||
.\" EBU Tech 3306 "RF64: An Extended File Format for Audio Data"
|
|
||||||
.\" .UE
|
|
||||||
.\" Version 1 of Tech 3306 laid out the
|
|
||||||
.\" .I RF64
|
|
||||||
.\" extended WAVE
|
|
||||||
.\" file format almost identically to
|
|
||||||
.\" .IR BS.2088 ,
|
|
||||||
.\" Version 2 of the standard wholly adopted
|
|
||||||
.\" .IR BS.2088 .
|
|
||||||
@@ -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,25 @@
|
|||||||
# 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('../..'))
|
||||||
sys.path.insert(0, os.path.abspath("../../.."))
|
sys.path.insert(0, os.path.abspath("../../.."))
|
||||||
print(sys.path)
|
print(sys.path)
|
||||||
|
|
||||||
import wavinfo
|
import importlib
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = u'wavinfo'
|
project = u'wavinfo'
|
||||||
copyright = u'2018-2023, Jamie Hardt'
|
copyright = u'2018-2024, 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 = "3.1"
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = wavinfo.__version__
|
release = importlib.metadata.version("wavinfo")
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
References
|
References
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
A complete list of technical references and commentary is available as a man page
|
||||||
|
and is installed as wavinfo(7) when you install `wavinfo` via pip.
|
||||||
|
|
||||||
Wave File Format
|
Wave File Format
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
@@ -33,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>`_
|
||||||
|
|||||||
@@ -29,3 +29,12 @@ Class Reference
|
|||||||
|
|
||||||
.. autoclass:: wavinfo.wave_cues_reader.WavCuesReader
|
.. autoclass:: wavinfo.wave_cues_reader.WavCuesReader
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_cues_reader.CueEntry
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_cues_reader.LabelEntry
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_cues_reader.NoteEntry
|
||||||
|
:members:
|
||||||
|
|||||||
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,13 +1,16 @@
|
|||||||
[build-system]
|
# https://python-poetry.org/docs/pyproject/
|
||||||
requires = ["flit_core >=3.2,<4"]
|
|
||||||
build-backend = "flit_core.buildapi"
|
|
||||||
|
|
||||||
[project]
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
name = "wavinfo"
|
name = "wavinfo"
|
||||||
authors = [{name = "Jamie Hardt", email = "jamiehardt@me.com"}]
|
version = "3.1.0"
|
||||||
|
description = "Probe WAVE files for all metadata"
|
||||||
|
authors = ["Jamie Hardt <jamiehardt@me.com>"]
|
||||||
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
dynamic = ["version", "description"]
|
|
||||||
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',
|
||||||
@@ -17,11 +20,13 @@ classifiers = [
|
|||||||
"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"
|
||||||
dependencies = [
|
|
||||||
"lxml ~= 4.9.2"
|
|
||||||
]
|
]
|
||||||
|
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',
|
||||||
@@ -34,29 +39,17 @@ keywords = [
|
|||||||
'broadcast'
|
'broadcast'
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.flit.module]
|
[tool.poetry.extras]
|
||||||
name = "wavinfo"
|
doc = ['sphinx', 'sphinx_rtd_theme']
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[tool.poetry.scripts]
|
||||||
doc = [
|
|
||||||
'sphinx >= 5.3.0',
|
|
||||||
'sphinx_rtd_theme >= 1.1.1',
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
Home = "https://github.com/iluvcapra/wavinfo"
|
|
||||||
Documentation = "https://wavinfo.readthedocs.io/"
|
|
||||||
Source = "https://github.com/iluvcapra/wavinfo.git"
|
|
||||||
Issues = 'https://github.com/iluvcapra/wavinfo/issues'
|
|
||||||
|
|
||||||
[project.entry_points.console_scripts]
|
|
||||||
wavinfo = 'wavinfo.__main__:main'
|
wavinfo = 'wavinfo.__main__:main'
|
||||||
|
|
||||||
[project.scripts]
|
[tool.poetry.dependencies]
|
||||||
wavinfo = "wavinfo.__main__:main"
|
python = "^3.8"
|
||||||
|
lxml = "~= 5.3.0"
|
||||||
[tool.flit.external-data]
|
sphinx_rtd_theme = {version= '>= 1.1.1', optional=true}
|
||||||
directory = "data"
|
sphinx = {version= '>= 5.3.0', optional=true}
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "basic"
|
||||||
|
|||||||
@@ -15,17 +15,20 @@ class TestADMWave(TestCase):
|
|||||||
adm = info.adm
|
adm = info.adm
|
||||||
self.assertIsNotNone(adm)
|
self.assertIsNotNone(adm)
|
||||||
|
|
||||||
|
assert adm is not None
|
||||||
self.assertEqual(len(adm.channel_uids), 14)
|
self.assertEqual(len(adm.channel_uids), 14)
|
||||||
|
|
||||||
def test_to_dict(self):
|
def test_to_dict(self):
|
||||||
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
adm = info.adm
|
adm = info.adm
|
||||||
|
assert adm is not None
|
||||||
dict = adm.to_dict()
|
dict = adm.to_dict()
|
||||||
self.assertIsNotNone(dict)
|
self.assertIsNotNone(dict)
|
||||||
|
|
||||||
def test_programme(self):
|
def test_programme(self):
|
||||||
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
adm = info.adm
|
adm = info.adm
|
||||||
|
assert adm is not None
|
||||||
pdict = adm.programme()
|
pdict = adm.programme()
|
||||||
self.assertIn("programme_id", pdict.keys())
|
self.assertIn("programme_id", pdict.keys())
|
||||||
self.assertIn("programme_name", pdict.keys())
|
self.assertIn("programme_name", pdict.keys())
|
||||||
@@ -37,7 +40,7 @@ class TestADMWave(TestCase):
|
|||||||
def test_track_info(self):
|
def test_track_info(self):
|
||||||
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
adm = info.adm
|
adm = info.adm
|
||||||
|
assert adm is not None
|
||||||
t1 = adm.track_info(0)
|
t1 = adm.track_info(0)
|
||||||
self.assertTrue("channel_format_name" in t1.keys())
|
self.assertTrue("channel_format_name" in t1.keys())
|
||||||
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
|
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class TestCue(TestCase):
|
|||||||
file1 = "tests/test_files/cue_chunks/STE-000.wav"
|
file1 = "tests/test_files/cue_chunks/STE-000.wav"
|
||||||
w1 = wavinfo.WavInfoReader(file1)
|
w1 = wavinfo.WavInfoReader(file1)
|
||||||
self.assertIsNotNone(w1.cues)
|
self.assertIsNotNone(w1.cues)
|
||||||
|
assert w1.cues is not None
|
||||||
vals = list(w1.cues.each_cue())
|
vals = list(w1.cues.each_cue())
|
||||||
self.assertEqual(vals, [(1,29616),(2,74592),(3,121200)])
|
self.assertEqual(vals, [(1,29616),(2,74592),(3,121200)])
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
from wavinfo.wave_dbmd_reader import SegmentType, DolbyAtmosMetadata, DolbyDigitalPlusMetadata
|
from wavinfo.wave_dbmd_reader import SegmentType, DolbyDigitalPlusMetadata
|
||||||
|
|
||||||
class TestDolby(TestCase):
|
class TestDolby(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -19,8 +19,10 @@ class TestDolby(TestCase):
|
|||||||
d = t1.dolby
|
d = t1.dolby
|
||||||
assert d is not None
|
assert d is not None
|
||||||
|
|
||||||
ddp = [x for x in d.segment_list if x[0] == SegmentType.DolbyDigitalPlus]
|
ddp = [x for x in d.segment_list \
|
||||||
atmos = [x for x in d.segment_list if x[0] == SegmentType.DolbyAtmos]
|
if x[0] == SegmentType.DolbyDigitalPlus]
|
||||||
|
atmos = [x for x in d.segment_list \
|
||||||
|
if x[0] == SegmentType.DolbyAtmos]
|
||||||
|
|
||||||
self.assertEqual(len(ddp), 1)
|
self.assertEqual(len(ddp), 1)
|
||||||
self.assertEqual(len(atmos), 1)
|
self.assertEqual(len(atmos), 1)
|
||||||
@@ -38,8 +40,13 @@ class TestDolby(TestCase):
|
|||||||
d = t1.dolby
|
d = t1.dolby
|
||||||
assert d is not None
|
assert d is not None
|
||||||
ddp = d.dolby_digital_plus()
|
ddp = d.dolby_digital_plus()
|
||||||
self.assertEqual(len(ddp), 1, "Failed to find exactly one Dolby Digital Plus metadata segment")
|
self.assertEqual(len(ddp), 1,
|
||||||
self.assertTrue( ddp[0].audio_coding_mode, DolbyDigitalPlusMetadata.AudioCodingMode.CH_ORD_3_2 )
|
("Failed to find exactly one Dolby Digital Plus "
|
||||||
|
"metadata segment")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue( ddp[0].audio_coding_mode,
|
||||||
|
DolbyDigitalPlusMetadata.AudioCodingMode.CH_ORD_3_2 )
|
||||||
self.assertTrue( ddp[0].lfe_on)
|
self.assertTrue( ddp[0].lfe_on)
|
||||||
|
|
||||||
def test_atmos(self):
|
def test_atmos(self):
|
||||||
@@ -47,6 +54,7 @@ class TestDolby(TestCase):
|
|||||||
d = t1.dolby
|
d = t1.dolby
|
||||||
assert d is not None
|
assert d is not None
|
||||||
atmos = d.dolby_atmos()
|
atmos = d.dolby_atmos()
|
||||||
self.assertEqual(len(atmos), 1, "Failed to find exactly one Atmos metadata segment")
|
self.assertEqual(len(atmos), 1,
|
||||||
|
"Failed to find exactly one Atmos metadata segment")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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.
@@ -26,7 +26,8 @@ class MainTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_ixml(self):
|
def test_ixml(self):
|
||||||
with patch.object(sys, 'argv',
|
with patch.object(sys, 'argv',
|
||||||
['TEST', '--ixml', 'tests/test_files/sounddevices/A101_1.WAV']):
|
['TEST', '--ixml',
|
||||||
|
'tests/test_files/sounddevices/A101_1.WAV']):
|
||||||
try:
|
try:
|
||||||
main()
|
main()
|
||||||
except:
|
except:
|
||||||
|
|||||||
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)
|
||||||
@@ -13,7 +13,9 @@ class TestWaveInfo(TestCase):
|
|||||||
def test_sanity(self):
|
def test_sanity(self):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
self.assertEqual(info.__repr__(), 'WavInfoReader({}, latin_1, ascii)'.format(os.path.abspath(wav_file)))
|
self.assertEqual(info.__repr__(),
|
||||||
|
'WavInfoReader({}, latin_1, ascii)'
|
||||||
|
.format(os.path.abspath(wav_file)))
|
||||||
self.assertIsNotNone(info)
|
self.assertIsNotNone(info)
|
||||||
|
|
||||||
def test_fmt_against_ffprobe(self):
|
def test_fmt_against_ffprobe(self):
|
||||||
@@ -24,14 +26,21 @@ class TestWaveInfo(TestCase):
|
|||||||
assert info.fmt is not None
|
assert info.fmt is not None
|
||||||
assert ffprobe_info is not None
|
assert ffprobe_info is not None
|
||||||
|
|
||||||
self.assertEqual(info.fmt.channel_count, ffprobe_info['streams'][0]['channels'])
|
self.assertEqual(info.fmt.channel_count,
|
||||||
self.assertEqual(info.fmt.sample_rate, int(ffprobe_info['streams'][0]['sample_rate']))
|
ffprobe_info['streams'][0]['channels'])
|
||||||
self.assertEqual(info.fmt.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_sample']))
|
self.assertEqual(info.fmt.sample_rate,
|
||||||
|
int(ffprobe_info['streams'][0]['sample_rate']))
|
||||||
|
self.assertEqual(info.fmt.bits_per_sample,
|
||||||
|
int(ffprobe_info['streams'][0]['bits_per_sample']
|
||||||
|
))
|
||||||
|
|
||||||
if info.fmt.audio_format == 1:
|
if info.fmt.audio_format == 1:
|
||||||
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm'))
|
self.assertTrue(ffprobe_info['streams'][0]['codec_name']\
|
||||||
|
.startswith('pcm'))
|
||||||
streams = ffprobe_info['streams'][0]
|
streams = ffprobe_info['streams'][0]
|
||||||
byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_sample']) / 8
|
byte_rate = int(streams['sample_rate']) * \
|
||||||
|
streams['channels'] * \
|
||||||
|
int(streams['bits_per_sample']) / 8
|
||||||
self.assertEqual(info.fmt.byte_rate, byte_rate)
|
self.assertEqual(info.fmt.byte_rate, byte_rate)
|
||||||
|
|
||||||
def test_data_against_ffprobe(self):
|
def test_data_against_ffprobe(self):
|
||||||
@@ -40,7 +49,8 @@ class TestWaveInfo(TestCase):
|
|||||||
ffprobe_info = cast(Dict[str,Any], ffprobe(wav_file))
|
ffprobe_info = cast(Dict[str,Any], ffprobe(wav_file))
|
||||||
assert ffprobe_info is not None
|
assert ffprobe_info is not None
|
||||||
assert info.data is not None
|
assert info.data is not None
|
||||||
self.assertEqual(info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts']))
|
self.assertEqual(info.data.frame_count,
|
||||||
|
int(ffprobe_info['streams'][0]['duration_ts']))
|
||||||
|
|
||||||
def test_bext_against_ffprobe(self):
|
def test_bext_against_ffprobe(self):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
@@ -50,39 +60,63 @@ class TestWaveInfo(TestCase):
|
|||||||
|
|
||||||
if info.bext:
|
if info.bext:
|
||||||
if 'comment' in ffprobe_info['format']['tags']:
|
if 'comment' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual(info.bext.description, ffprobe_info['format']['tags']['comment'])
|
self.assertEqual(info.bext.description,
|
||||||
|
ffprobe_info['format']['tags']\
|
||||||
|
['comment'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.description, '')
|
self.assertEqual(info.bext.description, '')
|
||||||
|
|
||||||
if 'encoded_by' in ffprobe_info['format']['tags']:
|
if 'encoded_by' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual(info.bext.originator, ffprobe_info['format']['tags']['encoded_by'])
|
self.assertEqual(info.bext.originator,
|
||||||
|
ffprobe_info['format']['tags']\
|
||||||
|
['encoded_by'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.originator, '')
|
self.assertEqual(info.bext.originator, '')
|
||||||
|
|
||||||
if 'originator_reference' in ffprobe_info['format']['tags']:
|
if 'originator_reference' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual(info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'])
|
self.assertEqual(info.bext.originator_ref,
|
||||||
|
ffprobe_info['format']['tags']\
|
||||||
|
['originator_reference'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.originator_ref, '')
|
self.assertEqual(info.bext.originator_ref, '')
|
||||||
|
|
||||||
# these don't always reflect the bext info
|
# these don't always reflect the bext info
|
||||||
# self.assertEqual(info.bext.originator_date, ffprobe_info['format']['tags']['date'])
|
# self.assertEqual(info.bext.originator_date,
|
||||||
# self.assertEqual(info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'])
|
# ffprobe_info['format']['tags']['date'])
|
||||||
self.assertEqual(info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']))
|
# self.assertEqual(info.bext.originator_time,
|
||||||
|
# ffprobe_info['format']['tags']['creation_time'])
|
||||||
|
self.assertEqual(info.bext.time_reference,
|
||||||
|
int(ffprobe_info['format']['tags']\
|
||||||
|
['time_reference']))
|
||||||
|
|
||||||
if 'coding_history' in ffprobe_info['format']['tags']:
|
if 'coding_history' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual(info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'])
|
self.assertEqual(info.bext.coding_history,
|
||||||
|
ffprobe_info['format']['tags']\
|
||||||
|
['coding_history'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.coding_history, '')
|
self.assertEqual(info.bext.coding_history, '')
|
||||||
|
|
||||||
def test_ixml(self):
|
def test_ixml(self):
|
||||||
expected = {'A101_4.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '4',
|
expected = {'A101_4.WAV': {'project': 'BMH',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
|
'scene': 'A101', 'take': '4',
|
||||||
'A101_3.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '3',
|
'tape': '18Y12M31',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
|
'family_uid':
|
||||||
'A101_2.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '2',
|
'USSDVGR1112089007124015008231000'},
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
|
'A101_3.WAV': {'project': 'BMH',
|
||||||
'A101_1.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '1',
|
'scene': 'A101', 'take': '3',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
|
'tape': '18Y12M31',
|
||||||
|
'family_uid':
|
||||||
|
'USSDVGR1112089007124014008228300'},
|
||||||
|
'A101_2.WAV': {'project': 'BMH',
|
||||||
|
'scene': 'A101', 'take': '2',
|
||||||
|
'tape': '18Y12M31',
|
||||||
|
'family_uid':
|
||||||
|
'USSDVGR1112089007124004008218600'},
|
||||||
|
'A101_1.WAV': {'project': 'BMH',
|
||||||
|
'scene': 'A101', 'take': '1',
|
||||||
|
'tape': '18Y12M31',
|
||||||
|
'family_uid':
|
||||||
|
'USSDVGR1112089007124001008206300'},
|
||||||
}
|
}
|
||||||
|
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
@@ -112,7 +146,8 @@ class TestWaveInfo(TestCase):
|
|||||||
assert info.ixml.steinberg is not None
|
assert info.ixml.steinberg is not None
|
||||||
self.assertIsNotNone(info.ixml.steinberg.audio_speaker_arrangement)
|
self.assertIsNotNone(info.ixml.steinberg.audio_speaker_arrangement)
|
||||||
self.assertEqual(info.ixml.steinberg.sample_format_size, 3)
|
self.assertEqual(info.ixml.steinberg.sample_format_size, 3)
|
||||||
self.assertEqual(info.ixml.steinberg.media_company, "https://github.com/iluvcapra/wavinfo")
|
self.assertEqual(info.ixml.steinberg.media_company,
|
||||||
|
"https://github.com/iluvcapra/wavinfo")
|
||||||
self.assertFalse(info.ixml.steinberg.media_drop_frames)
|
self.assertFalse(info.ixml.steinberg.media_drop_frames)
|
||||||
self.assertEqual(info.ixml.steinberg.media_duration, 1200.0)
|
self.assertEqual(info.ixml.steinberg.media_duration, 1200.0)
|
||||||
|
|
||||||
@@ -124,7 +159,8 @@ class TestWaveInfo(TestCase):
|
|||||||
self.assertIsNone(info.ixml.steinberg)
|
self.assertIsNone(info.ixml.steinberg)
|
||||||
|
|
||||||
def test_info_metadata(self):
|
def test_info_metadata(self):
|
||||||
file_with_metadata = 'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
file_with_metadata = \
|
||||||
|
'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
||||||
self.assertTrue(os.path.exists(file_with_metadata))
|
self.assertTrue(os.path.exists(file_with_metadata))
|
||||||
info = wavinfo.WavInfoReader(file_with_metadata).info
|
info = wavinfo.WavInfoReader(file_with_metadata).info
|
||||||
|
|
||||||
@@ -138,7 +174,8 @@ class TestWaveInfo(TestCase):
|
|||||||
self.assertEqual(info.software, 'Sound Grinder Pro')
|
self.assertEqual(info.software, 'Sound Grinder Pro')
|
||||||
self.assertEqual(info.created_date, '2010-12-28')
|
self.assertEqual(info.created_date, '2010-12-28')
|
||||||
self.assertEqual(info.engineer, 'JPH')
|
self.assertEqual(info.engineer, 'JPH')
|
||||||
self.assertEqual(info.keywords, 'Sound Effect, movement, microphone, bump')
|
self.assertEqual(info.keywords,
|
||||||
|
'Sound Effect, movement, microphone, bump')
|
||||||
self.assertEqual(info.title, 'camera bumb 1')
|
self.assertEqual(info.title, 'camera bumb 1')
|
||||||
self.assertEqual(type(info.to_dict()), dict)
|
self.assertEqual(type(info.to_dict()), dict)
|
||||||
self.assertEqual(type(info.__repr__()), str)
|
self.assertEqual(type(info.__repr__()), str)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ FFPROBE = 'ffprobe'
|
|||||||
|
|
||||||
|
|
||||||
def ffprobe(path):
|
def ffprobe(path):
|
||||||
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
|
arguments = [FFPROBE, "-of", "json",
|
||||||
|
"-show_format", "-show_streams", path]
|
||||||
if int(sys.version[0]) < 3:
|
if int(sys.version[0]) < 3:
|
||||||
process = subprocess.Popen(arguments, stdout=PIPE)
|
process = subprocess.Popen(arguments, stdout=PIPE)
|
||||||
process.wait()
|
process.wait()
|
||||||
@@ -20,7 +21,8 @@ def ffprobe(path):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE)
|
process = subprocess.run(arguments, stdin=None,
|
||||||
|
stdout=PIPE, stderr=PIPE)
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
output_str = process.stdout.decode('utf-8')
|
output_str = process.stdout.decode('utf-8')
|
||||||
return json.loads(output_str)
|
return json.loads(output_str)
|
||||||
|
|||||||
@@ -4,6 +4,3 @@ Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.
|
|||||||
|
|
||||||
from .wave_reader import WavInfoReader
|
from .wave_reader import WavInfoReader
|
||||||
from .riff_parser import WavInfoEOFError
|
from .riff_parser import WavInfoEOFError
|
||||||
|
|
||||||
__version__ = '2.3.0'
|
|
||||||
__short_version__ = '2.3.0'
|
|
||||||
|
|||||||
@@ -1,37 +1,166 @@
|
|||||||
from optparse import OptionParser, OptionGroup
|
|
||||||
import datetime
|
|
||||||
from . import WavInfoReader
|
from . import WavInfoReader
|
||||||
from . import __version__
|
|
||||||
|
import datetime
|
||||||
|
from optparse import OptionParser
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
from enum import Enum
|
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):
|
class MyJSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
if isinstance(o, Enum):
|
if isinstance(o, Enum):
|
||||||
return o._name_
|
return o._name_
|
||||||
|
elif isinstance(o, bytes):
|
||||||
|
return 'base64:' + b64encode(o).decode('ascii')
|
||||||
else:
|
else:
|
||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
|
|
||||||
class MissingDataError(RuntimeError):
|
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')
|
||||||
|
manpath = os.path.dirname(__file__) + "/man"
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
|
||||||
parser.usage = 'wavinfo (--adm | --ixml) <FILE> +'
|
parser.usage = 'wavinfo (--adm | --ixml) <FILE> +'
|
||||||
|
|
||||||
# parser.add_option('-f', dest='output_format', help='Set the output format',
|
# parser.add_option('--install-manpages',
|
||||||
# default='json',
|
# help="Install manual pages for wavinfo",
|
||||||
# metavar='FORMAT')
|
# default=False,
|
||||||
|
# action='store_true')
|
||||||
|
|
||||||
parser.add_option('--adm', dest='adm', help='Output ADM XML',
|
parser.add_option('--man',
|
||||||
default=False, action='store_true')
|
help="Read the manual and exit.",
|
||||||
|
default=False,
|
||||||
|
action='store_true')
|
||||||
|
|
||||||
parser.add_option('--ixml', dest='ixml', help='Output iXML',
|
parser.add_option('--adm', dest='adm',
|
||||||
default=False, action='store_true')
|
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)
|
(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:]:
|
for arg in args[1:]:
|
||||||
try:
|
try:
|
||||||
this_file = WavInfoReader(path=arg)
|
this_file = WavInfoReader(path=arg)
|
||||||
@@ -48,8 +177,8 @@ def main():
|
|||||||
else:
|
else:
|
||||||
ret_dict = {
|
ret_dict = {
|
||||||
'filename': arg,
|
'filename': arg,
|
||||||
'run_date': datetime.datetime.now().isoformat() ,
|
'run_date': datetime.datetime.now().isoformat(),
|
||||||
'application': "wavinfo " + __version__,
|
'application': f"wavinfo {version}",
|
||||||
'scopes': {}
|
'scopes': {}
|
||||||
}
|
}
|
||||||
for scope, name, value in this_file.walk():
|
for scope, name, value in this_file.walk():
|
||||||
@@ -58,13 +187,24 @@ 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" % (e, arg), file=sys.stderr)
|
print("MissingDataError: Missing metadata (%s) in file %s" %
|
||||||
|
(e, arg), file=sys.stderr)
|
||||||
continue
|
continue
|
||||||
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 ...
|
||||||
@@ -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
|
||||||
380
wavinfo/man/man7/wavinfo.7
Normal file
380
wavinfo/man/man7/wavinfo.7
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
.TH waveinfo 7 "2024-07-10" "Jamie Hardt" "Miscellaneous Information Manuals"
|
||||||
|
.SH NAME
|
||||||
|
wavinfo \- WAVE file metadata
|
||||||
|
.SH SYNOPSIS
|
||||||
|
Everything you ever wanted to know about WAVE metadata but were afraid to ask.
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.PP
|
||||||
|
The WAVE file format is forwards-compatible. Apart from audio data, it can
|
||||||
|
hold arbitrary blocks of bytes which clients will automatically ignore
|
||||||
|
unless they recognize them and know how to read them.
|
||||||
|
.PP
|
||||||
|
Without saying too much about the structure and parsing of WAVE files
|
||||||
|
themselves \- a subject beyond the scope of this document \- WAVE files are
|
||||||
|
divided into segments or
|
||||||
|
.BR chunks ,
|
||||||
|
which a client parser can either read or skip without reading. Chunks have
|
||||||
|
an identifier, or signature: a four-character-code that tells a client what
|
||||||
|
kind of chunk it is, and a length. Based on this information, a client can look
|
||||||
|
at the identifier and decide if it knows how to read a chunk and if it wants
|
||||||
|
to. If it doesn't, it can simply read the length and skip past it.
|
||||||
|
.PP
|
||||||
|
Some chunks are mandated by the Microsoft standard, specifically
|
||||||
|
.I fmt
|
||||||
|
and
|
||||||
|
.I data
|
||||||
|
in the case of PCM-encoded WAVE files. Other chunks, like
|
||||||
|
.I cue
|
||||||
|
or
|
||||||
|
.IR bext ,
|
||||||
|
are optional, and optional chunks usually hold metadata.
|
||||||
|
.PP
|
||||||
|
Chunks can also nest inside other chunks, a special identifier
|
||||||
|
.I LIST
|
||||||
|
is used to indicate these. A WAVE file is a recursive list: a top level
|
||||||
|
list of chunks, where chunks may contain a list of chunks themselves.
|
||||||
|
.SS Order and Arrangement of Metadata Chunks in a WAVE File
|
||||||
|
.PP
|
||||||
|
Chunks in a WAVE file can appear in any order, and a capable parser can accept
|
||||||
|
them appearing in any order. However, authorities give guidance on where chunks
|
||||||
|
should be placed when creating a new WAVE file.
|
||||||
|
.PP
|
||||||
|
.IP 1)
|
||||||
|
For all new WAVE files, clients should always place an empty chunk, a
|
||||||
|
so-called
|
||||||
|
.I JUNK
|
||||||
|
chunk, in the first position in the top-level list of a WAVE file, and
|
||||||
|
it should be sized large enough to hold a
|
||||||
|
.I ds64
|
||||||
|
chunk record. This will allow clients to upgrade the file to a RF64
|
||||||
|
WAVE file
|
||||||
|
.BR in-place ,
|
||||||
|
without having to re-write the file or audio data.
|
||||||
|
.IP 2)
|
||||||
|
Older authorites recommend placing metadata before the audio data, so clients
|
||||||
|
reading the file sequentially will hit it before having to seek through the
|
||||||
|
audio. This may improve metadata read performance on certain architectures.
|
||||||
|
.IP 3)
|
||||||
|
Older authorities also recommend inserting
|
||||||
|
.I JUNK
|
||||||
|
before the
|
||||||
|
.I data
|
||||||
|
chunk, sized so that the first byte of the
|
||||||
|
.I data
|
||||||
|
payload lands immediately at 0x1000 (4096), because this was a common factor of
|
||||||
|
the page boundaries of many operating systems and architectures. This may
|
||||||
|
optimize the audio I/O performance in certain situations.
|
||||||
|
.IP 4)
|
||||||
|
Modern implementations (we're looking at
|
||||||
|
.B Pro Tools
|
||||||
|
here) tend to place the Broadcast-WAVE
|
||||||
|
.I bext
|
||||||
|
metadata before the data, followed by the data itself, and then other data
|
||||||
|
after that.
|
||||||
|
.\" .PP
|
||||||
|
.\" Clients reading WAVE files should be tolerant and accept any configuration of
|
||||||
|
.\" chunks, and should accept any file as long as the obligatory
|
||||||
|
.\" .I fmt
|
||||||
|
.\" and
|
||||||
|
.\" .I data
|
||||||
|
.\" chunks
|
||||||
|
.\" are present.
|
||||||
|
.PP
|
||||||
|
It's not unheard-of to see a naive implementor expect
|
||||||
|
.B only
|
||||||
|
.I fmt
|
||||||
|
and
|
||||||
|
.I data
|
||||||
|
chunks, in this order, and to hard-code the offsets of the short
|
||||||
|
.I fmt
|
||||||
|
chunk and
|
||||||
|
.I data
|
||||||
|
chunk into their program, and this is something that should always be checked
|
||||||
|
when evaluating a new tool, just to make sure the developer didn't do this.
|
||||||
|
Many coding examples and WAVE file explainers from the 90s and early aughts
|
||||||
|
give the basic layout of a WAVE file, and naive devs go along with it.
|
||||||
|
.SS Encoding and Decoding Text Metadata
|
||||||
|
.\" .PP
|
||||||
|
.\" Modern metadata systems, anything developed since the late aughts, will defer
|
||||||
|
.\" encoding to an XML parser, so when dealing with
|
||||||
|
.\" .I ixml
|
||||||
|
.\" or
|
||||||
|
.\" .I axml
|
||||||
|
.\" so a client can mostly ignore this problem.
|
||||||
|
.\" .PP
|
||||||
|
.\" The most established metadata systems are older than this though, and so the
|
||||||
|
.\" entire weight of text encoding history falls upon the client.
|
||||||
|
.\" .PP
|
||||||
|
.\" The original WAVE specification, a part of the Microsoft/IBM Multimedia
|
||||||
|
.\" interface of 1991, was written at a time when Windows was an ascendant and
|
||||||
|
.\" soon-to-be dominant desktop environment. Audio files were almost
|
||||||
|
.\" never shared via LANs or the Internet or any other way. When audio files were
|
||||||
|
.\" shared, among the miniscule number of people who did this, it was via BBS or
|
||||||
|
.\" Usenet. Users at this time may have ripped them from CDs, but the cost of hard
|
||||||
|
.\" drives and low quality of compressed formats at the time made this little more
|
||||||
|
.\" than a curiosity. There was no CDBaby or CDDB to download and populate metadata
|
||||||
|
.\" from at this time.
|
||||||
|
.\" .PP
|
||||||
|
.\" So, the
|
||||||
|
.\" .I INFO
|
||||||
|
.\" and
|
||||||
|
.\" .I cue
|
||||||
|
.\" metadata systems, which are by far the most prevalent and supported, were
|
||||||
|
.\" published two years before the so-called "Endless September" of 1993 when the
|
||||||
|
.\" Internet became mainstream, when Unicode was still a twinkle in the eye, and
|
||||||
|
.\" two years before Ariana Grande was born.
|
||||||
|
.PP
|
||||||
|
The safest assumption, and the mandate of the Microsoft, is that all text
|
||||||
|
metadata, by default, be encoded in Windows codepage 819, a.k.a. ISO Latin
|
||||||
|
alphabet 1, or ISO 8859-1. This covers most Western European scripts but
|
||||||
|
excludes all of Asia, Russia, most of the European Near East, the Middle
|
||||||
|
East.
|
||||||
|
.PP
|
||||||
|
To account for this, Microsoft proposed a few conventions, none of which have
|
||||||
|
been adopted with any consistency among clients of the WAVE file standard.
|
||||||
|
.IP 1)
|
||||||
|
The RIFF standard defines a
|
||||||
|
.I cset
|
||||||
|
chunk which declares a Windows codepage for character encoding, along with a
|
||||||
|
native country code, language and dialect, which clients should use for
|
||||||
|
determining text information. We have never seen a WAVE
|
||||||
|
file with a
|
||||||
|
.I cest
|
||||||
|
chunk.
|
||||||
|
.IP 2)
|
||||||
|
Certain RIFF chunks allow the writing client to override the default encoding.
|
||||||
|
Relevant to audio files are the
|
||||||
|
.I ltxt
|
||||||
|
chunk, which encodes a country, language, dialect and codepage along with a
|
||||||
|
time range text note. We have never seen the text field on one of these
|
||||||
|
filled-out either.
|
||||||
|
.PP
|
||||||
|
Some clients, in our experience, simply write UTF-8 into
|
||||||
|
.IR cue ,
|
||||||
|
.IR labl ,
|
||||||
|
and
|
||||||
|
.I note
|
||||||
|
fields without any kind of framing.
|
||||||
|
.PP
|
||||||
|
A practical solution is to assume either ISO Latin 1, Windows CP 859 or Windows
|
||||||
|
CP 1252, and allow the client or user to override this based on its own
|
||||||
|
inferences. The
|
||||||
|
.I chardet
|
||||||
|
python package may provide useable guesses for text encoding, YMMV.
|
||||||
|
.SH CHUNK MENAGERIE
|
||||||
|
A list of chunks that you may find in a wave file from our experience.
|
||||||
|
.SS Essential WAV Chunks
|
||||||
|
.IP fmt
|
||||||
|
Defines the format of the audio in the
|
||||||
|
.I data
|
||||||
|
chunk: the audio codec, the sample rate, bit depth, channel count, block
|
||||||
|
alignment and other data. May take an "extended" form, with additional data
|
||||||
|
(such as channel speaker assignments) if there are more than two channels in
|
||||||
|
the file or if it is a compressed format.
|
||||||
|
.IP data
|
||||||
|
The audio data itself. PCM audio data is always stored as interleaved samples.
|
||||||
|
.SS Optional WAVE Chunks
|
||||||
|
.IP JUNK
|
||||||
|
A region of the file not currently in use. Clients sometimes add these before
|
||||||
|
the
|
||||||
|
.I data
|
||||||
|
chunk in order to align the beginning of the audio data with a memory page
|
||||||
|
boundary (this can make memory-mapped reads from a wave file a little more
|
||||||
|
efficient). A
|
||||||
|
.I JUNK
|
||||||
|
chunk is often placed at the beginning of a WAVE file to reserve space for
|
||||||
|
a
|
||||||
|
.I ds64
|
||||||
|
chunk that will be written to the file at the end of recording, in the event
|
||||||
|
that after the file is finalized, it exceeds the RIFF size limit. Thus a WAVE
|
||||||
|
file can be upgraded in-place to an RF64 without re-writing the audio data.
|
||||||
|
.IP fact
|
||||||
|
Fact chunks record the number of samples in the decoded audio stream. It's only
|
||||||
|
present in WAVE files that contain compressed audio.
|
||||||
|
.IP "LIST or list"
|
||||||
|
(Both have been seen) Not a chunk type itself but signals to a RIFF parser that
|
||||||
|
this chunk contains chunks itself. A LIST chunk's payload will begin with a
|
||||||
|
four-character code identifying the form of the list, and is then followed
|
||||||
|
by chunks of the standard key-length-data form, which may themselves be
|
||||||
|
LISTs that themselves contain child chunks. WAVE files don't tend to have a
|
||||||
|
very deep heirarchy of chunks, compared to AVI files.
|
||||||
|
.SS RIFF Metadata
|
||||||
|
The RIFF container format has a metadata system common to all RIFF files, WAVE
|
||||||
|
being the most common at present, AVI being another very common format
|
||||||
|
historically.
|
||||||
|
.IP "LIST form INFO"
|
||||||
|
A flat list of chunks, each containing text metadata. The role
|
||||||
|
of the string, like "Artist", "Composer", "Comment", "Engineer" etc. are given
|
||||||
|
by the four-character code: "Artist" is
|
||||||
|
.IR IART ,
|
||||||
|
Composer is
|
||||||
|
.IR ICMP ,
|
||||||
|
engineer is
|
||||||
|
.IR IENG ,
|
||||||
|
Comment is
|
||||||
|
.IR ICMT ,
|
||||||
|
etc.
|
||||||
|
.IP cue
|
||||||
|
A binary list of cues, which are timed points within the audio data.
|
||||||
|
.IP "LIST form adtl"
|
||||||
|
Contains text labels
|
||||||
|
.RI ( labl )
|
||||||
|
for the cues in the
|
||||||
|
.I cue
|
||||||
|
chunk, "notes"
|
||||||
|
.RI ( note ,
|
||||||
|
which are structurally identical to
|
||||||
|
.I labl
|
||||||
|
but hosts tend to use notes for longer text), and "length text"
|
||||||
|
.I ltxt
|
||||||
|
metadata records, which can give a cue a length, making it a range, and a text
|
||||||
|
field that defines its own encoding.
|
||||||
|
.IP cset
|
||||||
|
Defines the character set for all text fields in
|
||||||
|
.IR INFO ,
|
||||||
|
.I adtl
|
||||||
|
and other RIFF-defined text fields. By default, all of the text in RIFF
|
||||||
|
metadata fields is Windows Latin 1/ISO 8859-1, though as time passes many
|
||||||
|
clients have simply taken to sticking UTF-8 into these fields. The
|
||||||
|
.I cset
|
||||||
|
cannot represent UTF-8 as a valid option for text encoding, it only speaks
|
||||||
|
Windows codepages, and we've never seen one in a WAVE file in any event, and
|
||||||
|
it's unlikely an audio app would recognize one if it saw it.
|
||||||
|
.SS Broadcast-WAVE Metadata
|
||||||
|
Broadcast-WAVE is a set of extensions to WAVE files to facilitate media
|
||||||
|
production maintained by the EBU.
|
||||||
|
.IP bext
|
||||||
|
A multi-field structure containing mostly fixed-width text data capturing
|
||||||
|
essential production information: a 256 character free description field,
|
||||||
|
originator name and a unique reference, recording date and time, a frame-based
|
||||||
|
timestamp for sample-accurate recording time, and a coding history record. The
|
||||||
|
extended form of the structure can hold a SMPTE UMID (a kind of UUID, which
|
||||||
|
may also contain timestamp and geolocation data) and pre-computed program
|
||||||
|
loudness measurements.
|
||||||
|
.IP peak
|
||||||
|
A binary data structure containing the peak envelope for the audio data, for
|
||||||
|
use by clients to generate a waveform overview.
|
||||||
|
.SS Audio Definition Model Metadata
|
||||||
|
Audio Definition Model (ADM) metadata is a metadata standard for audio
|
||||||
|
broadcast and distribution maintained by the ITU.
|
||||||
|
.IP chna
|
||||||
|
A binary list that associates individual channels in the file to entities
|
||||||
|
in the ADM XML document stored in the
|
||||||
|
.I axml
|
||||||
|
chunk. A
|
||||||
|
.I chna
|
||||||
|
chunk will always appear with an
|
||||||
|
.I axml
|
||||||
|
chunk and vice versa.
|
||||||
|
.IP axml
|
||||||
|
Contains an XML document with Audio Definition Model metadata. ADM metadata
|
||||||
|
describes the program the WAVE file belongs to, role, channel assignment,
|
||||||
|
and encoding properties of individual channels in the WAVE file, and if the
|
||||||
|
WAVE file contains object-based audio, it will also give all of the positioning
|
||||||
|
and panning automation envelopes.
|
||||||
|
.IP bxml
|
||||||
|
This is defined by the ITU as a gzip-compressed version of the
|
||||||
|
.I axml
|
||||||
|
chunk.
|
||||||
|
.IP sxml
|
||||||
|
This is a hybrid binary/gzip-compressed-XML chunk that associates ADM
|
||||||
|
documents with timed ranges of a WAVE file.
|
||||||
|
.SS Dolby Metadata
|
||||||
|
Dolby metadata is present in Dolby Atmos master ADM WAVE files.
|
||||||
|
.IP dbmd
|
||||||
|
Records hints for Dolby playback applications for downmixing, level
|
||||||
|
normalization and other things.
|
||||||
|
.SS Proprietary Chunks
|
||||||
|
.IP ovwf
|
||||||
|
.B (Pro Tools)
|
||||||
|
Pre-computed waveform overview data.
|
||||||
|
.IP regn
|
||||||
|
.B (Pro Tools)
|
||||||
|
Region and cue point metadata.
|
||||||
|
.SS Chunks of Unknown Purpose
|
||||||
|
.IP elm1
|
||||||
|
.IP minf
|
||||||
|
.IP umid
|
||||||
|
.SH REFERENCES
|
||||||
|
(Note: We're not including URLs in this list, the title and standard number
|
||||||
|
should be sufficient to find almost all of these documents. The ITU, EBU and
|
||||||
|
IETF standards documents are freely-available.)
|
||||||
|
.SS Essential File Format
|
||||||
|
.TP
|
||||||
|
.B Multimedia Programming Interface and Data Specifications 1.0. Microsoft Corporation, 1991.
|
||||||
|
The original definition of the
|
||||||
|
.I RIFF
|
||||||
|
container, the
|
||||||
|
.I WAVE
|
||||||
|
form, the original metadata facilites (like
|
||||||
|
.IR INFO " and " cue ),
|
||||||
|
and things like language, country and
|
||||||
|
dialect enumerations. This document also contains descriptions of certain
|
||||||
|
variations on the WAVE, such as
|
||||||
|
.I LIST wavl
|
||||||
|
and compressed WAVE files that are so rare in practice as to be virtually
|
||||||
|
non-existent.
|
||||||
|
.TP
|
||||||
|
.B ITU Recommendation BS.2088-1-2019 \- Long-form file format for the international exchange of audio programme mterials with metadata. ITU 2019.
|
||||||
|
Formalized the RF64 file format, ADM carrier chunks like
|
||||||
|
.IR axml
|
||||||
|
and
|
||||||
|
.IR chna .
|
||||||
|
Formally supercedes the previous standard for RF64,
|
||||||
|
.BR "EBU 3306 v1" .
|
||||||
|
One oddity with this standard is it defines the file header for an extended
|
||||||
|
WAVE file to be
|
||||||
|
.IR BW64 ,
|
||||||
|
but this is never seen in practice.
|
||||||
|
.TP
|
||||||
|
.B RFC 2361 \- WAVE and AVI Codec Registries. IETF Network Working Group, 1998.
|
||||||
|
Gives an exhaustive list of all of the codecs that Microsoft had assigned to
|
||||||
|
vendor WAVE files as of 1998. At the time, numerous hardware vendors, sound
|
||||||
|
card and chip manufacturers, sound software developers and others all provided
|
||||||
|
their own slightly-different adaptive PCM codecs, linear predictive compression
|
||||||
|
codes, DCTs and other things, and Microsoft would issue these vendors WAVE
|
||||||
|
codec magic numbers. Almost all of these are no longer in use, the only ones
|
||||||
|
one ever encounters in the modern era are integer PCM (0x01), floating-point
|
||||||
|
PCM (0x03) and the extended format marker (0xFFFFFFFF). There are over a
|
||||||
|
hundred codecs assigned, however, a roll-call of failed software and hardware
|
||||||
|
brands.
|
||||||
|
.SS Broadcast WAVE Format
|
||||||
|
.TP
|
||||||
|
.B EBU Tech 3285 \- Specification of the Broadcast Wave Format (BWF). EBU, 2011.
|
||||||
|
Defines the elements of a Broadcast WAVE file, the
|
||||||
|
.I bext
|
||||||
|
metadata chunk structure, allowed sample formats and other things. Over the
|
||||||
|
years the EBU has published numerous supplements covering extensions to the
|
||||||
|
format, such as embedding SMPTE UMIDs, pre-calculated loudness data (EBU Tech
|
||||||
|
3285 v2),
|
||||||
|
.I peak
|
||||||
|
waveform overview data (Suppl. 3), ADM metadata (Suppl. 5 and 7), Dolby master
|
||||||
|
metadata (Suppl. 6), and other things.
|
||||||
|
.TP
|
||||||
|
.B SMPTE 330M-2011 \- Unique Material Identifier. SMPTE, 2011.
|
||||||
|
Describes the format of the SMPTE UMID field, a 32- or 64-byte UUID used to
|
||||||
|
identify media files. UMIDs are usually a dumb number in their 32-byte form,
|
||||||
|
but the extended form can encode a high-precision timestamp (with options for
|
||||||
|
epoch and timescale) and geolocation information. Broadcast-WAVE files
|
||||||
|
conforming to
|
||||||
|
.B "EBU 3285 v2"
|
||||||
|
have a SMPTE UMID embedded in the
|
||||||
|
.I bext
|
||||||
|
chunk.
|
||||||
|
.SS Audio Definition Model
|
||||||
|
.TP
|
||||||
|
.B ITU Recommendation BS.2076-2-2019 \- Audio definition model. ITU, 2019.
|
||||||
|
Defines the Audio Definition Model, entities, relationships and properties. If
|
||||||
|
you ever had any questions about how ADM works, this is where you would start.
|
||||||
|
.SS iXML Metadata
|
||||||
|
.TP
|
||||||
|
.B iXML Specification v3.01. Gallery Software, 2021.
|
||||||
|
iXML is a standard for embedding mostly human-created metadata into WAVE files,
|
||||||
|
and mostly with an emphasis on location sound recorders used on film and
|
||||||
|
television productions. Frustratingly the developer has never published a DTD
|
||||||
|
or schema validation or strict formal standard, and encourages vendors to just
|
||||||
|
do whatever, but most of the heavily-traveled metadata fields are standardized,
|
||||||
|
for recording information like a recording's scene, take, recording notes,
|
||||||
|
circled or alt status. iXML also has a system of
|
||||||
|
.B "families"
|
||||||
|
for associating several WAVE files together into one recording.
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
import struct
|
import struct
|
||||||
from collections import namedtuple
|
# from collections import namedtuple
|
||||||
|
from typing import NamedTuple, Dict
|
||||||
|
|
||||||
from . import riff_parser
|
from . import riff_parser
|
||||||
|
|
||||||
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
|
|
||||||
|
class RF64Context(NamedTuple):
|
||||||
|
sample_count: int
|
||||||
|
bigchunk_table: Dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
def parse_rf64(stream, signature = b'RF64') -> RF64Context:
|
def parse_rf64(stream, signature=b'RF64') -> RF64Context:
|
||||||
start = stream.tell()
|
start = stream.tell()
|
||||||
assert( stream.read(4) == b'WAVE' )
|
assert stream.read(4) == b'WAVE'
|
||||||
|
|
||||||
ds64_chunk = riff_parser.parse_chunk(stream)
|
ds64_chunk = riff_parser.parse_chunk(stream)
|
||||||
assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \
|
assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \
|
||||||
@@ -16,27 +20,28 @@ def parse_rf64(stream, signature = b'RF64') -> RF64Context:
|
|||||||
|
|
||||||
ds64_field_spec = "<QQQI"
|
ds64_field_spec = "<QQQI"
|
||||||
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
||||||
assert(ds64_chunk.ident == b'ds64')
|
assert ds64_chunk.ident == b'ds64'
|
||||||
|
|
||||||
ds64_data = ds64_chunk.read_data(stream)
|
ds64_data = ds64_chunk.read_data(stream)
|
||||||
assert(len(ds64_data) >= ds64_fields_size)
|
assert len(ds64_data) >= ds64_fields_size
|
||||||
|
|
||||||
riff_size, data_size, sample_count, length_lookup_table = struct.unpack(
|
riff_size, data_size, sample_count, length_lookup_table = struct.unpack(
|
||||||
ds64_field_spec, ds64_data[0:ds64_fields_size])
|
ds64_field_spec, ds64_data[0:ds64_fields_size]
|
||||||
|
)
|
||||||
|
|
||||||
bigchunk_table = {}
|
bigchunk_table = {}
|
||||||
chunksize64format = "<4sL"
|
chunksize64format = "<4sL"
|
||||||
# chunksize64size = struct.calcsize(chunksize64format)
|
# chunksize64size = struct.calcsize(chunksize64format)
|
||||||
|
|
||||||
for _ in range(length_lookup_table):
|
for _ in range(length_lookup_table):
|
||||||
bigname, bigsize = struct.unpack_from(chunksize64format, ds64_data,
|
bigname, bigsize = struct.unpack_from(chunksize64format,
|
||||||
offset= ds64_fields_size)
|
ds64_data,
|
||||||
|
offset=ds64_fields_size)
|
||||||
bigchunk_table[bigname] = bigsize
|
bigchunk_table[bigname] = bigsize
|
||||||
|
|
||||||
bigchunk_table[b'data'] = data_size
|
bigchunk_table[b'data'] = data_size
|
||||||
bigchunk_table[signature] = riff_size
|
bigchunk_table[signature] = riff_size
|
||||||
|
|
||||||
stream.seek(start, 0)
|
stream.seek(start, 0)
|
||||||
return RF64Context( sample_count=sample_count,
|
return RF64Context(sample_count=sample_count,
|
||||||
bigchunk_table=bigchunk_table)
|
bigchunk_table=bigchunk_table)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
# from optparse import Option
|
||||||
import struct
|
import struct
|
||||||
from collections import namedtuple
|
from .rf64_parser import parse_rf64, RF64Context
|
||||||
from .rf64_parser import parse_rf64
|
from typing import NamedTuple, Union, List, Optional
|
||||||
|
|
||||||
|
|
||||||
class WavInfoEOFError(EOFError):
|
class WavInfoEOFError(EOFError):
|
||||||
@@ -10,11 +10,17 @@ class WavInfoEOFError(EOFError):
|
|||||||
self.chunk_start = chunk_start
|
self.chunk_start = chunk_start
|
||||||
|
|
||||||
|
|
||||||
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
|
class ListChunkDescriptor(NamedTuple):
|
||||||
pass
|
signature: bytes
|
||||||
|
children: List[Union['ChunkDescriptor', 'ListChunkDescriptor']]
|
||||||
|
|
||||||
|
|
||||||
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
|
class ChunkDescriptor(NamedTuple):
|
||||||
|
ident: bytes
|
||||||
|
start: int
|
||||||
|
length: int
|
||||||
|
rf64_context: Optional[RF64Context]
|
||||||
|
|
||||||
def read_data(self, from_stream) -> bytes:
|
def read_data(self, from_stream) -> bytes:
|
||||||
from_stream.seek(self.start)
|
from_stream.seek(self.start)
|
||||||
return from_stream.read(self.length)
|
return from_stream.read(self.length)
|
||||||
@@ -49,7 +55,7 @@ def parse_chunk(stream, rf64_context=None):
|
|||||||
rf64_context = parse_rf64(stream=stream, signature=ident)
|
rf64_context = parse_rf64(stream=stream, signature=ident)
|
||||||
|
|
||||||
assert rf64_context is not None, \
|
assert rf64_context is not None, \
|
||||||
f"Sentinel data size 0xFFFFFFFF found outside of RF64 context"
|
"Sentinel data size 0xFFFFFFFF found outside of RF64 context"
|
||||||
|
|
||||||
data_size = rf64_context.bigchunk_table[ident]
|
data_size = rf64_context.bigchunk_table[ident]
|
||||||
|
|
||||||
@@ -64,5 +70,7 @@ def parse_chunk(stream, rf64_context=None):
|
|||||||
else:
|
else:
|
||||||
data_start = stream.tell()
|
data_start = stream.tell()
|
||||||
stream.seek(displacement, 1)
|
stream.seek(displacement, 1)
|
||||||
return ChunkDescriptor(ident=ident, start=data_start, length=data_size,
|
return ChunkDescriptor(ident=ident,
|
||||||
|
start=data_start,
|
||||||
|
length=data_size,
|
||||||
rf64_context=rf64_context)
|
rf64_context=rf64_context)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
|
|
||||||
# def binary_to_string(binary_value):
|
# def binary_to_string(binary_value):
|
||||||
# return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
|
# return reduce(lambda val, el: val + "{:02x}".format(el),
|
||||||
|
# binary_value, '')
|
||||||
|
|
||||||
# class UMIDParser:
|
# class UMIDParser:
|
||||||
# """
|
# """
|
||||||
@@ -13,109 +13,109 @@
|
|||||||
# """
|
# """
|
||||||
# def __init__(self, raw_umid: bytes):
|
# def __init__(self, raw_umid: bytes):
|
||||||
# self.raw_umid = raw_umid
|
# self.raw_umid = raw_umid
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def universal_label(self) -> bytearray:
|
# def universal_label(self) -> bytearray:
|
||||||
# return self.raw_umid[0:12]
|
# return self.raw_umid[0:12]
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def basic_umid(self):
|
# def basic_umid(self):
|
||||||
# return self.raw_umid[0:32]
|
# return self.raw_umid[0:32]
|
||||||
|
|
||||||
# def basic_umid_to_str(self):
|
# def basic_umid_to_str(self):
|
||||||
# return binary_to_string(self.raw_umid[0:32])
|
# return binary_to_string(self.raw_umid[0:32])
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def universal_label_is_valid(self) -> bool:
|
# def universal_label_is_valid(self) -> bool:
|
||||||
# valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
|
# valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
|
||||||
# return self.universal_label[0:len(valid_preamble)] == valid_preamble
|
# return self.universal_label[0:len(valid_preamble)] == valid_preamble
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def material_type(self) -> str:
|
# def material_type(self) -> str:
|
||||||
# material_byte = self.raw_umid[10]
|
# material_byte = self.raw_umid[10]
|
||||||
# if material_byte == 0x1:
|
# if material_byte == 0x1:
|
||||||
# return 'picture'
|
# return 'picture'
|
||||||
# elif material_byte == 0x2:
|
# elif material_byte == 0x2:
|
||||||
# return 'audio'
|
# return 'audio'
|
||||||
# elif material_byte == 0x3:
|
# elif material_byte == 0x3:
|
||||||
# return 'data'
|
# return 'data'
|
||||||
# elif material_byte == 0x4:
|
# elif material_byte == 0x4:
|
||||||
# return 'other'
|
# return 'other'
|
||||||
# elif material_byte == 0x5:
|
# elif material_byte == 0x5:
|
||||||
# return 'picture_single_component'
|
# return 'picture_single_component'
|
||||||
# elif material_byte == 0x6:
|
# elif material_byte == 0x6:
|
||||||
# return 'picture_multiple_component'
|
# return 'picture_multiple_component'
|
||||||
# elif material_byte == 0x7:
|
# elif material_byte == 0x7:
|
||||||
# return 'audio_single_component'
|
# return 'audio_single_component'
|
||||||
# elif material_byte == 0x9:
|
# elif material_byte == 0x9:
|
||||||
# return 'audio_multiple_component'
|
# return 'audio_multiple_component'
|
||||||
# elif material_byte == 0xb:
|
# elif material_byte == 0xb:
|
||||||
# return 'auxiliary_single_component'
|
# return 'auxiliary_single_component'
|
||||||
# elif material_byte == 0xc:
|
# elif material_byte == 0xc:
|
||||||
# return 'auxiliary_multiple_component'
|
# return 'auxiliary_multiple_component'
|
||||||
# elif material_byte == 0xd:
|
# elif material_byte == 0xd:
|
||||||
# return 'mixed_components'
|
# return 'mixed_components'
|
||||||
# elif material_byte == 0xf:
|
# elif material_byte == 0xf:
|
||||||
# return 'not_identified'
|
# return 'not_identified'
|
||||||
# else:
|
# else:
|
||||||
# return 'not_recognized'
|
# return 'not_recognized'
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def material_number_creation_method(self) -> str:
|
# def material_number_creation_method(self) -> str:
|
||||||
# method_byte = self.raw_umid[11]
|
# method_byte = self.raw_umid[11]
|
||||||
# method_byte = (method_byte << 4) & 0xf
|
# method_byte = (method_byte << 4) & 0xf
|
||||||
# if method_byte == 0x0:
|
# if method_byte == 0x0:
|
||||||
# return 'undefined'
|
# return 'undefined'
|
||||||
# elif method_byte == 0x1:
|
# elif method_byte == 0x1:
|
||||||
# return 'smpte'
|
# return 'smpte'
|
||||||
# elif method_byte == 0x2:
|
# elif method_byte == 0x2:
|
||||||
# return 'uuid'
|
# return 'uuid'
|
||||||
# elif method_byte == 0x3:
|
# elif method_byte == 0x3:
|
||||||
# return 'masked'
|
# return 'masked'
|
||||||
# elif method_byte == 0x4:
|
# elif method_byte == 0x4:
|
||||||
# return 'ieee1394'
|
# return 'ieee1394'
|
||||||
# elif 0x5 <= method_byte <= 0x7:
|
# elif 0x5 <= method_byte <= 0x7:
|
||||||
# return 'reserved_undefined'
|
# return 'reserved_undefined'
|
||||||
# else:
|
# else:
|
||||||
# return 'unrecognized'
|
# return 'unrecognized'
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def instance_number_creation_method(self) -> str:
|
# def instance_number_creation_method(self) -> str:
|
||||||
# method_byte = self.raw_umid[11]
|
# method_byte = self.raw_umid[11]
|
||||||
# method_byte = method_byte & 0xf
|
# method_byte = method_byte & 0xf
|
||||||
# if method_byte == 0x0:
|
# if method_byte == 0x0:
|
||||||
# return 'undefined'
|
# return 'undefined'
|
||||||
# elif method_byte == 0x01:
|
# elif method_byte == 0x01:
|
||||||
# return 'local_registration'
|
# return 'local_registration'
|
||||||
# elif method_byte == 0x02:
|
# elif method_byte == 0x02:
|
||||||
# return '24_bit_prs'
|
# return '24_bit_prs'
|
||||||
# elif method_byte == 0x03:
|
# elif method_byte == 0x03:
|
||||||
# return 'copy_number_and_16_bit_prs'
|
# return 'copy_number_and_16_bit_prs'
|
||||||
# elif 0x04 <= method_byte <= 0x0e:
|
# elif 0x04 <= method_byte <= 0x0e:
|
||||||
# return 'reserved_undefined'
|
# return 'reserved_undefined'
|
||||||
# elif method_byte == 0x0f:
|
# elif method_byte == 0x0f:
|
||||||
# return 'live_stream'
|
# return 'live_stream'
|
||||||
# else:
|
# else:
|
||||||
# return 'unrecognized'
|
# return 'unrecognized'
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def indicated_length(self) -> str:
|
# def indicated_length(self) -> str:
|
||||||
# if self.raw_umid[12] == 0x13:
|
# if self.raw_umid[12] == 0x13:
|
||||||
# return 'basic'
|
# return 'basic'
|
||||||
# elif self.raw_umid[12] == 0x33:
|
# elif self.raw_umid[12] == 0x33:
|
||||||
# return 'extended'
|
# return 'extended'
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def instance_number(self) -> bytearray:
|
# def instance_number(self) -> bytearray:
|
||||||
# return self.raw_umid[13:3]
|
# return self.raw_umid[13:3]
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def material_number(self) -> bytearray:
|
# def material_number(self) -> bytearray:
|
||||||
# return self.raw_umid[16:16]
|
# return self.raw_umid[16:16]
|
||||||
#
|
#
|
||||||
# @property
|
# @property
|
||||||
# def source_pack(self) -> Union[bytearray, None]:
|
# def source_pack(self) -> Union[bytearray, None]:
|
||||||
# if self.indicated_length == 'extended':
|
# if self.indicated_length == 'extended':
|
||||||
# return self.raw_umid[32:32]
|
# return self.raw_umid[32:32]
|
||||||
# else:
|
# else:
|
||||||
# return None
|
# return None
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ ADM Reader
|
|||||||
from struct import unpack, unpack_from, calcsize
|
from struct import unpack, unpack_from, calcsize
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Iterable, Tuple
|
from typing import Optional
|
||||||
|
|
||||||
from lxml import etree as ET
|
from lxml import etree as ET
|
||||||
|
|
||||||
|
|
||||||
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
|
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
|
||||||
|
|
||||||
|
|
||||||
class WavADMReader:
|
class WavADMReader:
|
||||||
"""
|
"""
|
||||||
Reads XML data from an EBU ADM (Audio Definiton Model) WAV File.
|
Reads XML data from an EBU ADM (Audio Definiton Model) WAV File.
|
||||||
@@ -26,24 +28,24 @@ class WavADMReader:
|
|||||||
|
|
||||||
_, uid_count = unpack(header_fmt, chna_data[0:4])
|
_, uid_count = unpack(header_fmt, chna_data[0:4])
|
||||||
|
|
||||||
#: A list of :class:`ChannelEntry` objects parsed from the
|
|
||||||
#: `chna` metadata chunk.
|
|
||||||
#:
|
|
||||||
#: .. note::
|
|
||||||
#: In-file, the `chna` track indexes start at 1. However, this interface
|
|
||||||
#: numbers the first track 0, in order to maintain consistency with other
|
|
||||||
#: libraries.
|
|
||||||
self.channel_uids = []
|
self.channel_uids = []
|
||||||
|
|
||||||
offset = calcsize(header_fmt)
|
offset = calcsize(header_fmt)
|
||||||
for _ in range(uid_count):
|
for _ in range(uid_count):
|
||||||
|
|
||||||
track_index, uid, track_ref, pack_ref = unpack_from(uid_fmt, chna_data, offset)
|
track_index, uid, track_ref, pack_ref = unpack_from(uid_fmt,
|
||||||
|
chna_data,
|
||||||
|
offset)
|
||||||
|
|
||||||
# these values are either ascii or all null
|
# these values are either ascii or all null
|
||||||
|
|
||||||
self.channel_uids.append(ChannelEntry(track_index - 1,
|
self.channel_uids.append(
|
||||||
uid.decode('ascii') , track_ref.decode('ascii'), pack_ref.decode('ascii')))
|
ChannelEntry(track_index - 1,
|
||||||
|
uid.decode('ascii'),
|
||||||
|
track_ref.decode('ascii'),
|
||||||
|
pack_ref.decode('ascii')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
offset += calcsize(uid_fmt)
|
offset += calcsize(uid_fmt)
|
||||||
|
|
||||||
@@ -53,7 +55,8 @@ class WavADMReader:
|
|||||||
|
|
||||||
def programme(self) -> dict:
|
def programme(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Read the ADM `audioProgramme` data structure and some of its reference properties.
|
Read the ADM `audioProgramme` data structure and some of its reference
|
||||||
|
properties.
|
||||||
"""
|
"""
|
||||||
ret_dict = dict()
|
ret_dict = dict()
|
||||||
|
|
||||||
@@ -68,17 +71,21 @@ class WavADMReader:
|
|||||||
ret_dict['programme_end'] = program.get("end")
|
ret_dict['programme_end'] = program.get("end")
|
||||||
ret_dict['contents'] = []
|
ret_dict['contents'] = []
|
||||||
|
|
||||||
for content_ref in program.findall("audioContentIDRef", namespaces=nsmap):
|
for content_ref in program.findall("audioContentIDRef",
|
||||||
|
namespaces=nsmap):
|
||||||
content_dict = dict()
|
content_dict = dict()
|
||||||
content_dict['content_id'] = cid = content_ref.text
|
content_dict['content_id'] = cid = content_ref.text
|
||||||
content = afext.find("audioContent[@audioContentID='%s']" % cid, namespaces=nsmap)
|
content = afext.find("audioContent[@audioContentID='%s']" % cid,
|
||||||
|
namespaces=nsmap)
|
||||||
content_dict['content_name'] = content.get("audioContentName")
|
content_dict['content_name'] = content.get("audioContentName")
|
||||||
content_dict['objects'] = []
|
content_dict['objects'] = []
|
||||||
|
|
||||||
for object_ref in content.findall("audioObjectIDRef", namespaces=nsmap):
|
for object_ref in content.findall("audioObjectIDRef",
|
||||||
|
namespaces=nsmap):
|
||||||
object_dict = dict()
|
object_dict = dict()
|
||||||
object_dict['object_id'] = oid = object_ref.text
|
object_dict['object_id'] = oid = object_ref.text
|
||||||
object = afext.find("audioObject[@audioObjectID='%s']" % oid, namespaces=nsmap)
|
object = afext.find("audioObject[@audioObjectID='%s']" % oid,
|
||||||
|
namespaces=nsmap)
|
||||||
pack = object.find("audioPackFormatIDRef", namespaces=nsmap)
|
pack = object.find("audioPackFormatIDRef", namespaces=nsmap)
|
||||||
object_dict['object_name'] = object.get("audioObjectName")
|
object_dict['object_name'] = object.get("audioObjectName")
|
||||||
object_dict['object_start'] = object.get("start")
|
object_dict['object_start'] = object.get("start")
|
||||||
@@ -95,15 +102,17 @@ class WavADMReader:
|
|||||||
|
|
||||||
return ret_dict
|
return ret_dict
|
||||||
|
|
||||||
def track_info(self, index) -> dict:
|
def track_info(self, index) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Information about a track in the WAV file.
|
Information about a track in the WAV file.
|
||||||
|
|
||||||
:param index: index of audio track (indexed from zero)
|
:param index: index of audio track (indexed from zero)
|
||||||
:returns: a dictionary with *content_name*, *content_id*, *object_name*, *object_id*,
|
:returns: a dictionary with *content_name*, *content_id*,
|
||||||
|
*object_name*, *object_id*,
|
||||||
*pack_format_name*, *pack_type*, *channel_format_name*
|
*pack_format_name*, *pack_type*, *channel_format_name*
|
||||||
"""
|
"""
|
||||||
channel_info = next((x for x in self.channel_uids if x.track_index == index), None)
|
channel_info = next((x for x in self.channel_uids
|
||||||
|
if x.track_index == index), None)
|
||||||
|
|
||||||
if channel_info is None:
|
if channel_info is None:
|
||||||
return None
|
return None
|
||||||
@@ -112,38 +121,52 @@ class WavADMReader:
|
|||||||
|
|
||||||
nsmap = self.axml.getroot().nsmap
|
nsmap = self.axml.getroot().nsmap
|
||||||
|
|
||||||
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
afext = self.axml.find(".//audioFormatExtended",
|
||||||
|
|
||||||
trackformat_elem = afext.find("audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref,
|
|
||||||
namespaces=nsmap)
|
namespaces=nsmap)
|
||||||
|
|
||||||
|
trackformat_elem = afext.find(
|
||||||
|
"audioTrackFormat[@audioTrackFormatID='%s']"
|
||||||
|
% channel_info.track_ref, namespaces=nsmap)
|
||||||
|
|
||||||
stream_id = trackformat_elem[0].text
|
stream_id = trackformat_elem[0].text
|
||||||
|
|
||||||
channelformatref_elem = afext.find("audioStreamFormat[@audioStreamFormatID='%s']/audioChannelFormatIDRef" % stream_id,
|
channelformatref_elem = afext.find(
|
||||||
|
("audioStreamFormat[@audioStreamFormatID='%s']"
|
||||||
|
"/audioChannelFormatIDRef") % stream_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap)
|
||||||
channelformat_id = channelformatref_elem.text
|
channelformat_id = channelformatref_elem.text
|
||||||
|
|
||||||
packformatref_elem = afext.find("audioStreamFormat[@audioStreamFormatID='%s']/audioPackFormatIDRef" % stream_id,
|
packformatref_elem = afext.find(
|
||||||
|
("audioStreamFormat[@audioStreamFormatID='%s']"
|
||||||
|
"/audioPackFormatIDRef") % stream_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap)
|
||||||
packformat_id = packformatref_elem.text
|
packformat_id = packformatref_elem.text
|
||||||
|
|
||||||
channelformat_elem = afext.find("audioChannelFormat[@audioChannelFormatID='%s']" % channelformat_id,
|
channelformat_elem = afext\
|
||||||
|
.find("audioChannelFormat[@audioChannelFormatID='%s']"
|
||||||
|
% channelformat_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap)
|
||||||
ret_dict['channel_format_name'] = channelformat_elem.get("audioChannelFormatName")
|
ret_dict['channel_format_name'] = channelformat_elem.get(
|
||||||
|
"audioChannelFormatName")
|
||||||
|
|
||||||
packformat_elem = afext.find("audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
|
packformat_elem = afext.find(
|
||||||
|
"audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap)
|
||||||
ret_dict['pack_type'] = packformat_elem.get("typeDefinition")
|
ret_dict['pack_type'] = packformat_elem.get(
|
||||||
ret_dict['pack_format_name'] = packformat_elem.get("audioPackFormatName")
|
"typeDefinition")
|
||||||
|
ret_dict['pack_format_name'] = packformat_elem.get(
|
||||||
|
"audioPackFormatName")
|
||||||
|
|
||||||
object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']" % packformat_id,
|
object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']"
|
||||||
|
% packformat_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap)
|
||||||
|
|
||||||
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
|
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
|
||||||
object_id = object_elem.get("audioObjectID")
|
object_id = object_elem.get("audioObjectID")
|
||||||
ret_dict['object_id'] = object_id
|
ret_dict['object_id'] = object_id
|
||||||
|
|
||||||
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']" % object_id,
|
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']"
|
||||||
|
% object_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap)
|
||||||
|
|
||||||
ret_dict['content_name'] = content_elem.get("audioContentName")
|
ret_dict['content_name'] = content_elem.get("audioContentName")
|
||||||
@@ -151,7 +174,7 @@ class WavADMReader:
|
|||||||
|
|
||||||
return ret_dict
|
return ret_dict
|
||||||
|
|
||||||
def to_dict(self) -> dict: #FIXME should be "asdict"
|
def to_dict(self) -> dict: # FIXME should be "asdict"
|
||||||
"""
|
"""
|
||||||
Get ADM metadata as a dictionary.
|
Get ADM metadata as a dictionary.
|
||||||
"""
|
"""
|
||||||
@@ -161,5 +184,6 @@ class WavADMReader:
|
|||||||
rd.update(self.track_info(channel_uid_rec.track_index))
|
rd.update(self.track_info(channel_uid_rec.track_index))
|
||||||
return rd
|
return rd
|
||||||
|
|
||||||
return dict(channel_entries=list(map(lambda z: make_entry(z), self.channel_uids)),
|
return dict(channel_entries=list(map(lambda z: make_entry(z),
|
||||||
|
self.channel_uids)),
|
||||||
programme=self.programme())
|
programme=self.programme())
|
||||||
@@ -3,71 +3,75 @@ import struct
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class WavBextReader:
|
class WavBextReader:
|
||||||
def __init__(self, bext_data, encoding):
|
def __init__(self, bext_data, encoding):
|
||||||
"""
|
"""
|
||||||
Read Broadcast-WAV extended metadata.
|
Read Broadcast-WAV extended metadata.
|
||||||
:param bext_data: The bytes-like data.
|
:param bext_data: The bytes-like data.
|
||||||
:param encoding: The encoding to use when decoding the text fields of the
|
:param encoding: The encoding to use when decoding the text fields of
|
||||||
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
|
the BEXT metadata scope. According to EBU Rec 3285 this shall be
|
||||||
|
ASCII.
|
||||||
"""
|
"""
|
||||||
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + \
|
||||||
|
"hhhhh" + "180s"
|
||||||
|
|
||||||
rest_starts = struct.calcsize(packstring)
|
rest_starts = struct.calcsize(packstring)
|
||||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||||
|
|
||||||
def sanitize_bytes(b : bytes) -> str:
|
def sanitize_bytes(b: bytes) -> str:
|
||||||
# honestly can't remember why I'm stripping nulls this way
|
# honestly can't remember why I'm stripping nulls this way
|
||||||
first_null = next((index for index, byte in enumerate(b) if byte == 0), None)
|
first_null = next((index for index, byte in enumerate(b)
|
||||||
|
if byte == 0), None)
|
||||||
trimmed = b if first_null is None else b[:first_null]
|
trimmed = b if first_null is None else b[:first_null]
|
||||||
decoded = trimmed.decode(encoding)
|
decoded = trimmed.decode(encoding)
|
||||||
return decoded
|
return decoded
|
||||||
|
|
||||||
#: Description. A free-text field up to 256 characters long.
|
#: Description. A free-text field up to 256 characters long.
|
||||||
self.description : str = sanitize_bytes(unpacked[0])
|
self.description: str = sanitize_bytes(unpacked[0])
|
||||||
|
|
||||||
#: Originator. Usually the name of the encoding application, sometimes
|
#: Originator. Usually the name of the encoding application, sometimes
|
||||||
#: an artist name.
|
#: an artist name.
|
||||||
self.originator : str = sanitize_bytes(unpacked[1])
|
self.originator: str = sanitize_bytes(unpacked[1])
|
||||||
|
|
||||||
#: A unique identifier for the file, a serial number.
|
#: A unique identifier for the file, a serial number.
|
||||||
self.originator_ref : str = sanitize_bytes(unpacked[2])
|
self.originator_ref: str = sanitize_bytes(unpacked[2])
|
||||||
|
|
||||||
#: Date of the recording, in the format YYYY-MM-DD.
|
#: Date of the recording, in the format YYYY-MM-DD.
|
||||||
self.originator_date : str = sanitize_bytes(unpacked[3])
|
self.originator_date: str = sanitize_bytes(unpacked[3])
|
||||||
|
|
||||||
#: Time of the recording, in the format HH:MM:SS.
|
#: Time of the recording, in the format HH:MM:SS.
|
||||||
self.originator_time : str = sanitize_bytes(unpacked[4])
|
self.originator_time: str = sanitize_bytes(unpacked[4])
|
||||||
|
|
||||||
#: The sample offset of the start, usually relative
|
#: The sample offset of the start, usually relative
|
||||||
#: to midnight.
|
#: to midnight.
|
||||||
self.time_reference : int = unpacked[5]
|
self.time_reference: int = unpacked[5]
|
||||||
|
|
||||||
#: A variable-length text field containing a list of processes and
|
#: A variable-length text field containing a list of processes and
|
||||||
#: and conversions performed on the file.
|
#: and conversions performed on the file.
|
||||||
self.coding_history : str = sanitize_bytes(bext_data[rest_starts:])
|
self.coding_history: str = sanitize_bytes(bext_data[rest_starts:])
|
||||||
|
|
||||||
#: BEXT version.
|
#: BEXT version.
|
||||||
self.version : int = unpacked[6]
|
self.version: int = unpacked[6]
|
||||||
|
|
||||||
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
|
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though
|
||||||
#: may only be 32 bytes long.
|
#: the UMID may only be 32 bytes long.
|
||||||
self.umid : Optional[bytes] = None
|
self.umid: Optional[bytes] = None
|
||||||
|
|
||||||
#: EBU R128 Integrated loudness, in LUFS.
|
#: EBU R128 Integrated loudness, in LUFS.
|
||||||
self.loudness_value : Optional[float] = None
|
self.loudness_value: Optional[float] = None
|
||||||
|
|
||||||
#: EBU R128 Loudness range, in LUFS.
|
#: EBU R128 Loudness range, in LUFS.
|
||||||
self.loudness_range : Optional[float] = None
|
self.loudness_range: Optional[float] = None
|
||||||
|
|
||||||
#: True peak level, in dBFS TP
|
#: True peak level, in dBFS TP
|
||||||
self.max_true_peak : Optional[float] = None
|
self.max_true_peak: Optional[float] = None
|
||||||
|
|
||||||
#: EBU R128 Maximum momentary loudness, in LUFS
|
#: EBU R128 Maximum momentary loudness, in LUFS
|
||||||
self.max_momentary_loudness : Optional[float] = None
|
self.max_momentary_loudness: Optional[float] = None
|
||||||
|
|
||||||
#: EBU R128 Maximum short-term loudness, in LUFS.
|
#: EBU R128 Maximum short-term loudness, in LUFS.
|
||||||
self.max_shortterm_loudness : Optional[float] = None
|
self.max_shortterm_loudness: Optional[float] = None
|
||||||
|
|
||||||
if self.version > 0:
|
if self.version > 0:
|
||||||
self.umid = unpacked[7]
|
self.umid = unpacked[7]
|
||||||
@@ -85,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,
|
||||||
@@ -94,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,6 @@ IBM Corporation and Microsoft Corporation
|
|||||||
https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import encodings
|
|
||||||
from .riff_parser import ChunkDescriptor
|
from .riff_parser import ChunkDescriptor
|
||||||
|
|
||||||
from struct import unpack, calcsize
|
from struct import unpack, calcsize
|
||||||
@@ -98,7 +97,14 @@ LanguageDialectCodes = """0 0 None Indicated
|
|||||||
|
|
||||||
|
|
||||||
class CueEntry(NamedTuple):
|
class CueEntry(NamedTuple):
|
||||||
|
"""
|
||||||
|
A ``cue`` element structure.
|
||||||
|
"""
|
||||||
|
#: Cue "name" or id number
|
||||||
name: int
|
name: int
|
||||||
|
#: Cue position, as a frame count in the play order of the WAVE file. In
|
||||||
|
#: principle this can be affected by playlists and ``wavl`` chunk
|
||||||
|
#: placement.
|
||||||
position: int
|
position: int
|
||||||
chunk_id: bytes
|
chunk_id: bytes
|
||||||
chunk_start: int
|
chunk_start: int
|
||||||
@@ -114,7 +120,8 @@ class CueEntry(NamedTuple):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def read(cls, data: bytes) -> 'CueEntry':
|
def read(cls, data: bytes) -> 'CueEntry':
|
||||||
assert len(data) == cls.format_size(), \
|
assert len(data) == cls.format_size(), \
|
||||||
f"cue data size incorrect, expected {calcsize(cls.Format)} found {len(data)}"
|
(f"cue data size incorrect, expected {calcsize(cls.Format)} "
|
||||||
|
"found {len(data)}")
|
||||||
|
|
||||||
parsed = unpack(cls.Format, data)
|
parsed = unpack(cls.Format, data)
|
||||||
|
|
||||||
@@ -124,6 +131,9 @@ class CueEntry(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class LabelEntry(NamedTuple):
|
class LabelEntry(NamedTuple):
|
||||||
|
"""
|
||||||
|
A ``labl`` structure.
|
||||||
|
"""
|
||||||
name: int
|
name: int
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
@@ -137,6 +147,9 @@ NoteEntry = LabelEntry
|
|||||||
|
|
||||||
|
|
||||||
class RangeLabel(NamedTuple):
|
class RangeLabel(NamedTuple):
|
||||||
|
"""
|
||||||
|
A ``ltxt`` structure.
|
||||||
|
"""
|
||||||
name: int
|
name: int
|
||||||
length: int
|
length: int
|
||||||
purpose: str
|
purpose: str
|
||||||
@@ -163,9 +176,17 @@ class RangeLabel(NamedTuple):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WavCuesReader:
|
class WavCuesReader:
|
||||||
|
|
||||||
|
#: Every ``cue`` entry in the file
|
||||||
cues: List[CueEntry]
|
cues: List[CueEntry]
|
||||||
|
|
||||||
|
#: Every ``labl`` in the file
|
||||||
labels: List[LabelEntry]
|
labels: List[LabelEntry]
|
||||||
|
|
||||||
|
#: Every ``ltxt`` in the file
|
||||||
ranges: List[RangeLabel]
|
ranges: List[RangeLabel]
|
||||||
|
|
||||||
|
#: Every ``note`` in the file
|
||||||
notes: List[NoteEntry]
|
notes: List[NoteEntry]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -230,8 +251,8 @@ class WavCuesReader:
|
|||||||
:returns: a tuple of the the cue's label (if present) and note (if
|
:returns: a tuple of the the cue's label (if present) and note (if
|
||||||
present)
|
present)
|
||||||
"""
|
"""
|
||||||
label = next((l.text for l in self.labels
|
label = next((label.text for label in self.labels
|
||||||
if l.name == cue_ident), None)
|
if label.name == cue_ident), None)
|
||||||
note = next((n.text for n in self.notes
|
note = next((n.text for n in self.notes
|
||||||
if n.name == cue_ident), None)
|
if n.name == cue_ident), None)
|
||||||
return (label, note)
|
return (label, note)
|
||||||
@@ -263,10 +284,3 @@ class WavCuesReader:
|
|||||||
retval[n]['length'] = r
|
retval[n]['length'] = r
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
# return dict(cues=[c._asdict() for c in self.cues],
|
|
||||||
# labels=[l._asdict() for l in self.labels],
|
|
||||||
# ranges=[r._asdict() for r in self.ranges],
|
|
||||||
# notes=[n._asdict() for n in self.notes])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ Unless otherwise stated, all § references here are to
|
|||||||
from enum import IntEnum, Enum
|
from enum import IntEnum, Enum
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from typing import List, Optional, Tuple, Any, Union
|
from typing import List, Tuple, Any, Union
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
class SegmentType(IntEnum):
|
class SegmentType(IntEnum):
|
||||||
"""
|
"""
|
||||||
Metadata segment type.
|
Metadata segment type.
|
||||||
@@ -31,7 +32,7 @@ class SegmentType(IntEnum):
|
|||||||
DolbyAtmosSupplemental = 0xa
|
DolbyAtmosSupplemental = 0xa
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _missing_(cls,val):
|
def _missing_(cls, val):
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
@@ -77,7 +78,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
MUTE = 0b111
|
MUTE = 0b111
|
||||||
"-∞ dB"
|
"-∞ dB"
|
||||||
|
|
||||||
|
|
||||||
class DolbySurroundEncodingMode(Enum):
|
class DolbySurroundEncodingMode(Enum):
|
||||||
"""
|
"""
|
||||||
Dolby surround endcoding mode.
|
Dolby surround endcoding mode.
|
||||||
@@ -87,7 +87,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
NOT_IN_USE = 0b01
|
NOT_IN_USE = 0b01
|
||||||
NOT_INDICATED = 0b00
|
NOT_INDICATED = 0b00
|
||||||
|
|
||||||
|
|
||||||
class BitStreamMode(Enum):
|
class BitStreamMode(Enum):
|
||||||
"""
|
"""
|
||||||
Dolby Digital Plus `bsmod` field
|
Dolby Digital Plus `bsmod` field
|
||||||
@@ -122,7 +121,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
should be interpreted as karaoke.
|
should be interpreted as karaoke.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class AudioCodingMode(Enum):
|
class AudioCodingMode(Enum):
|
||||||
"""
|
"""
|
||||||
Dolby Digital Plus `acmod` field
|
Dolby Digital Plus `acmod` field
|
||||||
@@ -144,7 +142,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
CH_ORD_3_2 = 0b111
|
CH_ORD_3_2 = 0b111
|
||||||
"LCR + LR surround"
|
"LCR + LR surround"
|
||||||
|
|
||||||
|
|
||||||
class CenterDownMixLevel(Enum):
|
class CenterDownMixLevel(Enum):
|
||||||
"""
|
"""
|
||||||
§ 4.3.3.1
|
§ 4.3.3.1
|
||||||
@@ -161,7 +158,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
|
|
||||||
RESERVED = 0b11
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
class SurroundDownMixLevel(Enum):
|
class SurroundDownMixLevel(Enum):
|
||||||
"""
|
"""
|
||||||
Dolby Digital Plus `surmixlev` field
|
Dolby Digital Plus `surmixlev` field
|
||||||
@@ -172,7 +168,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
MUTE = 0b10
|
MUTE = 0b10
|
||||||
RESERVED = 0b11
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
class LanguageCode(int):
|
class LanguageCode(int):
|
||||||
"""
|
"""
|
||||||
§ 4.3.4.1
|
§ 4.3.4.1
|
||||||
@@ -181,21 +176,18 @@ class DolbyDigitalPlusMetadata:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MixLevel(int):
|
class MixLevel(int):
|
||||||
"""
|
"""
|
||||||
§ 4.3.6.2
|
§ 4.3.6.2
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DialnormLevel(int):
|
class DialnormLevel(int):
|
||||||
"""
|
"""
|
||||||
§ 4.3.4.4
|
§ 4.3.4.4
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RoomType(Enum):
|
class RoomType(Enum):
|
||||||
"""
|
"""
|
||||||
`roomtyp` 4.3.6.3
|
`roomtyp` 4.3.6.3
|
||||||
@@ -205,11 +197,10 @@ class DolbyDigitalPlusMetadata:
|
|||||||
SMALL_ROOM_FLAT_CURVE = 0b10
|
SMALL_ROOM_FLAT_CURVE = 0b10
|
||||||
RESERVED = 0b11
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
class PreferredDownMixMode(Enum):
|
class PreferredDownMixMode(Enum):
|
||||||
"""
|
"""
|
||||||
Indicates the creating engineer's preference of what the receiver should
|
Indicates the creating engineer's preference of what the receiver
|
||||||
downmix.
|
should downmix.
|
||||||
§ 4.3.8.1
|
§ 4.3.8.1
|
||||||
"""
|
"""
|
||||||
NOT_INDICATED = 0b00
|
NOT_INDICATED = 0b00
|
||||||
@@ -217,7 +208,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
STEREO = 0b10
|
STEREO = 0b10
|
||||||
PRO_LOGIC_2 = 0b11
|
PRO_LOGIC_2 = 0b11
|
||||||
|
|
||||||
|
|
||||||
class SurroundEXMode(IntEnum):
|
class SurroundEXMode(IntEnum):
|
||||||
"""
|
"""
|
||||||
Dolby Surround-EX mode.
|
Dolby Surround-EX mode.
|
||||||
@@ -228,7 +218,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
SEX = 0b10
|
SEX = 0b10
|
||||||
PRO_LOGIC_2 = 0b11
|
PRO_LOGIC_2 = 0b11
|
||||||
|
|
||||||
|
|
||||||
class HeadphoneMode(IntEnum):
|
class HeadphoneMode(IntEnum):
|
||||||
"""
|
"""
|
||||||
`dheadphonmod` § 4.3.9.2
|
`dheadphonmod` § 4.3.9.2
|
||||||
@@ -238,12 +227,10 @@ class DolbyDigitalPlusMetadata:
|
|||||||
DOLBY_HEADPHONE = 0b10
|
DOLBY_HEADPHONE = 0b10
|
||||||
RESERVED = 0b11
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
class ADConverterType(Enum):
|
class ADConverterType(Enum):
|
||||||
STANDARD = 0
|
STANDARD = 0
|
||||||
HDCD = 1
|
HDCD = 1
|
||||||
|
|
||||||
|
|
||||||
class StreamDependency(Enum):
|
class StreamDependency(Enum):
|
||||||
"""
|
"""
|
||||||
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
|
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
|
||||||
@@ -254,7 +241,6 @@ class DolbyDigitalPlusMetadata:
|
|||||||
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
|
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
|
||||||
RESERVED = 3
|
RESERVED = 3
|
||||||
|
|
||||||
|
|
||||||
class RFCompressionProfile(Enum):
|
class RFCompressionProfile(Enum):
|
||||||
"""
|
"""
|
||||||
`compr1` RF compression profile
|
`compr1` RF compression profile
|
||||||
@@ -363,12 +349,14 @@ class DolbyDigitalPlusMetadata:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def surround_config(b):
|
def surround_config(b):
|
||||||
return DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4), \
|
return (
|
||||||
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2), \
|
DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4),
|
||||||
|
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2),
|
||||||
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3)
|
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3)
|
||||||
|
)
|
||||||
|
|
||||||
def dialnorm_info(b):
|
def dialnorm_info(b):
|
||||||
return (b & 0x80) > 0 , b & 0x40 > 0, b & 0x20 > 0, \
|
return (b & 0x80) > 0, b & 0x40 > 0, b & 0x20 > 0, \
|
||||||
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
|
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
|
||||||
|
|
||||||
def langcod(b) -> int:
|
def langcod(b) -> int:
|
||||||
@@ -386,15 +374,16 @@ class DolbyDigitalPlusMetadata:
|
|||||||
|
|
||||||
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
|
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
|
||||||
def ext_bsi1_word2(b):
|
def ext_bsi1_word2(b):
|
||||||
return DolbyDigitalPlusMetadata.PreferredDownMixMode(b & 0xC0 >> 6), \
|
return DolbyDigitalPlusMetadata\
|
||||||
|
.PreferredDownMixMode(b & 0xC0 >> 6), \
|
||||||
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
||||||
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
||||||
|
|
||||||
#surround_ex_mode, dolby_headphone_encoded, ad_converter_type
|
# surround_ex_mode, dolby_headphone_encoded, ad_converter_type
|
||||||
def ext_bsi2_word1(b):
|
def ext_bsi2_word1(b):
|
||||||
return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \
|
return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \
|
||||||
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \
|
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \
|
||||||
DolbyDigitalPlusMetadata.ADConverterType( b & 0x4 >> 2)
|
DolbyDigitalPlusMetadata.ADConverterType(b & 0x4 >> 2)
|
||||||
|
|
||||||
def ddplus_reserved2(_):
|
def ddplus_reserved2(_):
|
||||||
pass
|
pass
|
||||||
@@ -423,14 +412,19 @@ class DolbyDigitalPlusMetadata:
|
|||||||
pid = program_id(buffer[0])
|
pid = program_id(buffer[0])
|
||||||
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
|
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
|
||||||
ddplus_reserved1(buffer[2:2])
|
ddplus_reserved1(buffer[2:2])
|
||||||
center_downmix_level, surround_downmix_level, dolby_surround_encoded = surround_config(buffer[4])
|
center_downmix_level, surround_downmix_level, \
|
||||||
langcode_present, copyright_bitstream, original_bitstream, dialnorm = dialnorm_info(buffer[5])
|
dolby_surround_encoded = surround_config(buffer[4])
|
||||||
|
langcode_present, copyright_bitstream, original_bitstream, \
|
||||||
|
dialnorm = dialnorm_info(buffer[5])
|
||||||
langcode = langcod(buffer[6])
|
langcode = langcod(buffer[6])
|
||||||
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
|
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
|
||||||
|
|
||||||
loro_center_downmix_level, loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
|
loro_center_downmix_level, \
|
||||||
downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9])
|
loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
|
||||||
surround_ex_mode, dolby_headphone_encoded, ad_converter_type = ext_bsi2_word1(buffer[10])
|
downmix_mode, ltrt_center_downmix_level, \
|
||||||
|
ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9])
|
||||||
|
surround_ex_mode, dolby_headphone_encoded, \
|
||||||
|
ad_converter_type = ext_bsi2_word1(buffer[10])
|
||||||
|
|
||||||
ddplus_reserved2(buffer[11:14])
|
ddplus_reserved2(buffer[11:14])
|
||||||
compression = compr1(buffer[14])
|
compression = compr1(buffer[14])
|
||||||
@@ -441,8 +435,8 @@ class DolbyDigitalPlusMetadata:
|
|||||||
data_rate = datarate(buffer[25:27])
|
data_rate = datarate(buffer[25:27])
|
||||||
reserved(buffer[27:69])
|
reserved(buffer[27:69])
|
||||||
|
|
||||||
return DolbyDigitalPlusMetadata(program_id=pid,
|
return DolbyDigitalPlusMetadata(
|
||||||
lfe_on=lfe_on,
|
program_id=pid, lfe_on=lfe_on,
|
||||||
bitstream_mode=bitstream_mode,
|
bitstream_mode=bitstream_mode,
|
||||||
audio_coding_mode=audio_coding_mode,
|
audio_coding_mode=audio_coding_mode,
|
||||||
center_downmix_level=center_downmix_level,
|
center_downmix_level=center_downmix_level,
|
||||||
@@ -486,7 +480,7 @@ class DolbyAtmosMetadata:
|
|||||||
NOT_INDICATED = 0x04
|
NOT_INDICATED = 0x04
|
||||||
|
|
||||||
tool_name: str
|
tool_name: str
|
||||||
tool_version: Tuple[int,int,int]
|
tool_version: Tuple[int, int, int]
|
||||||
warp_mode: WarpMode
|
warp_mode: WarpMode
|
||||||
|
|
||||||
SEGMENT_LENGTH = 248
|
SEGMENT_LENGTH = 248
|
||||||
@@ -494,8 +488,10 @@ class DolbyAtmosMetadata:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, data: bytes):
|
def load(cls, data: bytes):
|
||||||
assert len(data) == cls.SEGMENT_LENGTH, "DolbyAtmosMetadata segment "\
|
|
||||||
"is incorrect length, expected %i actual was %i" % (cls.SEGMENT_LENGTH, len(data))
|
assert len(data) == cls.SEGMENT_LENGTH
|
||||||
|
# (f"DolbyAtmosMetadata segment is incorrect length, "
|
||||||
|
# f"expected {cls.SEGMENT_LENGTH} actual was {len(data)}")
|
||||||
|
|
||||||
h = BytesIO(data)
|
h = BytesIO(data)
|
||||||
|
|
||||||
@@ -513,7 +509,9 @@ class DolbyAtmosMetadata:
|
|||||||
warp_mode = a_val & 0x7
|
warp_mode = a_val & 0x7
|
||||||
|
|
||||||
return DolbyAtmosMetadata(tool_name=toolname,
|
return DolbyAtmosMetadata(tool_name=toolname,
|
||||||
tool_version=(major, minor, fix), warp_mode=DolbyAtmosMetadata.WarpMode(warp_mode))
|
tool_version=(major, minor, fix),
|
||||||
|
warp_mode=DolbyAtmosMetadata
|
||||||
|
.WarpMode(warp_mode))
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -521,7 +519,8 @@ class DolbyAtmosSupplementalMetadata:
|
|||||||
"""
|
"""
|
||||||
Dolby Atmos supplemental metadata segment.
|
Dolby Atmos supplemental metadata segment.
|
||||||
|
|
||||||
https://github.com/DolbyLaboratories/dbmd-atmos-parser/blob/master/dbmd_atmos_parse/src/dbmd_atmos_parse.c
|
https://github.com/DolbyLaboratories/dbmd-atmos-parser/blob/
|
||||||
|
master/dbmd_atmos_parse/src/dbmd_atmos_parse.c
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class BinauralRenderMode(Enum):
|
class BinauralRenderMode(Enum):
|
||||||
@@ -531,12 +530,10 @@ class DolbyAtmosSupplementalMetadata:
|
|||||||
MID = 0x03
|
MID = 0x03
|
||||||
NOT_INDICATED = 0x04
|
NOT_INDICATED = 0x04
|
||||||
|
|
||||||
|
|
||||||
object_count: int
|
object_count: int
|
||||||
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
|
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
|
||||||
trim_modes: List[int]
|
trim_modes: List[int]
|
||||||
|
|
||||||
|
|
||||||
MAGIC = 0xf8726fbd
|
MAGIC = 0xf8726fbd
|
||||||
TRIM_CONFIG_COUNT = 9
|
TRIM_CONFIG_COUNT = 9
|
||||||
|
|
||||||
@@ -552,13 +549,13 @@ class DolbyAtmosSupplementalMetadata:
|
|||||||
|
|
||||||
object_count = unpack("<H", h.read(2))[0]
|
object_count = unpack("<H", h.read(2))[0]
|
||||||
|
|
||||||
h.read(1) #skip 1
|
h.read(1) # skip 1
|
||||||
|
|
||||||
for _ in range(cls.TRIM_CONFIG_COUNT):
|
for _ in range(cls.TRIM_CONFIG_COUNT):
|
||||||
auto_trim = unpack("B", h.read(1))
|
auto_trim = unpack("B", h.read(1))
|
||||||
trim_modes.append(auto_trim)
|
trim_modes.append(auto_trim)
|
||||||
|
|
||||||
h.read(14) #skip 14
|
h.read(14) # skip 14
|
||||||
|
|
||||||
h.read(object_count) # skip object_count bytes
|
h.read(object_count) # skip object_count bytes
|
||||||
|
|
||||||
@@ -568,7 +565,8 @@ class DolbyAtmosSupplementalMetadata:
|
|||||||
render_modes.append(binaural_mode)
|
render_modes.append(binaural_mode)
|
||||||
|
|
||||||
return DolbyAtmosSupplementalMetadata(object_count=object_count,
|
return DolbyAtmosSupplementalMetadata(object_count=object_count,
|
||||||
render_modes=render_modes,trim_modes=trim_modes)
|
render_modes=render_modes,
|
||||||
|
trim_modes=trim_modes)
|
||||||
|
|
||||||
|
|
||||||
class WavDolbyMetadataReader:
|
class WavDolbyMetadataReader:
|
||||||
@@ -584,7 +582,7 @@ class WavDolbyMetadataReader:
|
|||||||
#: not recognized).
|
#: not recognized).
|
||||||
segment_list: List[Tuple[Union[SegmentType, int], bool, Any]]
|
segment_list: List[Tuple[Union[SegmentType, int], bool, Any]]
|
||||||
|
|
||||||
version: Tuple[int,int,int,int]
|
version: Tuple[int, int, int, int]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def segment_checksum(bs: bytes, size: int):
|
def segment_checksum(bs: bytes, size: int):
|
||||||
@@ -597,7 +595,6 @@ class WavDolbyMetadataReader:
|
|||||||
|
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, dbmd_data):
|
def __init__(self, dbmd_data):
|
||||||
self.segment_list = []
|
self.segment_list = []
|
||||||
|
|
||||||
@@ -606,18 +603,19 @@ class WavDolbyMetadataReader:
|
|||||||
v_vec = []
|
v_vec = []
|
||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
b = h.read(1)
|
b = h.read(1)
|
||||||
v_vec.insert(0, unpack("B",b)[0])
|
v_vec.insert(0, unpack("B", b)[0])
|
||||||
|
|
||||||
self.version = tuple(v_vec)
|
self.version = tuple(v_vec)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
stype= SegmentType(unpack("B", h.read(1))[0])
|
stype = SegmentType(unpack("B", h.read(1))[0])
|
||||||
if stype == SegmentType.EndMarker:
|
if stype == SegmentType.EndMarker:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
seg_size = unpack("<H", h.read(2))[0]
|
seg_size = unpack("<H", h.read(2))[0]
|
||||||
seg_payload = h.read(seg_size)
|
seg_payload = h.read(seg_size)
|
||||||
expected_checksum = WavDolbyMetadataReader.segment_checksum(seg_payload, seg_size)
|
expected_checksum = WavDolbyMetadataReader\
|
||||||
|
.segment_checksum(seg_payload, seg_size)
|
||||||
checksum = unpack("B", h.read(1))[0]
|
checksum = unpack("B", h.read(1))[0]
|
||||||
|
|
||||||
segment = seg_payload
|
segment = seg_payload
|
||||||
@@ -628,34 +626,35 @@ class WavDolbyMetadataReader:
|
|||||||
elif stype == SegmentType.DolbyAtmosSupplemental:
|
elif stype == SegmentType.DolbyAtmosSupplemental:
|
||||||
segment = DolbyAtmosSupplementalMetadata.load(segment)
|
segment = DolbyAtmosSupplementalMetadata.load(segment)
|
||||||
|
|
||||||
self.segment_list.append( (stype, checksum == expected_checksum, segment) )
|
self.segment_list\
|
||||||
|
.append((stype, checksum == expected_checksum, segment))
|
||||||
|
|
||||||
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
|
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
|
||||||
"""
|
"""
|
||||||
Every valid Dolby Digital Plus metadata segment in the file.
|
Every valid Dolby Digital Plus metadata segment in the file.
|
||||||
"""
|
"""
|
||||||
return [x[2] for x in self.segment_list \
|
return [x[2] for x in self.segment_list
|
||||||
if x[0] == SegmentType.DolbyDigitalPlus and x[1]]
|
if x[0] == SegmentType.DolbyDigitalPlus and x[1]]
|
||||||
|
|
||||||
def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
|
def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
|
||||||
"""
|
"""
|
||||||
Every valid Dolby Atmos metadata segment in the file.
|
Every valid Dolby Atmos metadata segment in the file.
|
||||||
"""
|
"""
|
||||||
return [x[2] for x in self.segment_list \
|
return [x[2] for x in self.segment_list
|
||||||
if x[0] == SegmentType.DolbyAtmos and x[1]]
|
if x[0] == SegmentType.DolbyAtmos and x[1]]
|
||||||
|
|
||||||
def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
||||||
"""
|
"""
|
||||||
Every valid Dolby Atmos Supplemental metadata segment in the file.
|
Every valid Dolby Atmos Supplemental metadata segment in the file.
|
||||||
"""
|
"""
|
||||||
return [x[2] for x in self.segment_list \
|
return [x[2] for x in self.segment_list
|
||||||
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
|
|
||||||
ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
|
ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
|
||||||
atmos = map(lambda x: asdict(x), self.dolby_atmos())
|
atmos = map(lambda x: asdict(x), self.dolby_atmos())
|
||||||
#atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
|
# atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
|
||||||
|
|
||||||
return dict(dolby_digital_plus=list(ddp),
|
return dict(dolby_digital_plus=list(ddp),
|
||||||
dolby_atmos=list(atmos))
|
dolby_atmos=list(atmos))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from .riff_parser import parse_chunk, ListChunkDescriptor
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class WavInfoChunkReader:
|
class WavInfoChunkReader:
|
||||||
|
|
||||||
def __init__(self, f, encoding):
|
def __init__(self, f, encoding):
|
||||||
@@ -9,47 +10,52 @@ class WavInfoChunkReader:
|
|||||||
|
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
parsed_chunks = parse_chunk(f)
|
parsed_chunks = parse_chunk(f)
|
||||||
|
assert type(parsed_chunks) is ListChunkDescriptor
|
||||||
|
|
||||||
list_chunks = [chunk for chunk in parsed_chunks.children if type(chunk) is ListChunkDescriptor]
|
list_chunks = [chunk for chunk in parsed_chunks.children
|
||||||
|
if type(chunk) is ListChunkDescriptor]
|
||||||
|
|
||||||
self.info_chunk = next((chunk for chunk in list_chunks if chunk.signature == b'INFO'), None)
|
self.info_chunk = next((chunk for chunk in list_chunks
|
||||||
|
if chunk.signature == b'INFO'), None)
|
||||||
|
|
||||||
#: 'ICOP' Copyright
|
#: 'ICOP' Copyright
|
||||||
self.copyright : Optional[str] = self._get_field(f, b'ICOP')
|
self.copyright: Optional[str] = self._get_field(f, b'ICOP')
|
||||||
#: 'IPRD' Product
|
#: 'IPRD' Product
|
||||||
self.product : Optional[str]= self._get_field(f, b'IPRD')
|
self.product: Optional[str] = self._get_field(f, b'IPRD')
|
||||||
self.album : Optional[str] = self.product
|
self.album: Optional[str] = self.product
|
||||||
#: 'IGNR' Genre
|
#: 'IGNR' Genre
|
||||||
self.genre : Optional[str] = self._get_field(f, b'IGNR')
|
self.genre: Optional[str] = self._get_field(f, b'IGNR')
|
||||||
#: 'ISBJ' Subject
|
#: 'ISBJ' Subject
|
||||||
self.subject : Optional[str] = self._get_field(f, b'ISBJ')
|
self.subject: Optional[str] = self._get_field(f, b'ISBJ')
|
||||||
#: 'IART' Artist, composer, author
|
#: 'IART' Artist, composer, author
|
||||||
self.artist : Optional[str] = self._get_field(f, b'IART')
|
self.artist: Optional[str] = self._get_field(f, b'IART')
|
||||||
#: 'ICMT' Comment
|
#: 'ICMT' Comment
|
||||||
self.comment : Optional[str] = self._get_field(f, b'ICMT')
|
self.comment: Optional[str] = self._get_field(f, b'ICMT')
|
||||||
#: 'ISFT' Software, encoding application
|
#: 'ISFT' Software, encoding application
|
||||||
self.software : Optional[str] = self._get_field(f, b'ISFT')
|
self.software: Optional[str] = self._get_field(f, b'ISFT')
|
||||||
#: 'ICRD' Created date
|
#: 'ICRD' Created date
|
||||||
self.created_date : Optional[str] = self._get_field(f, b'ICRD')
|
self.created_date: Optional[str] = self._get_field(f, b'ICRD')
|
||||||
#: 'IENG' Engineer
|
#: 'IENG' Engineer
|
||||||
self.engineer : Optional[str] = self._get_field(f, b'IENG')
|
self.engineer: Optional[str] = self._get_field(f, b'IENG')
|
||||||
#: 'ITCH' Technician
|
#: 'ITCH' Technician
|
||||||
self.technician : Optional[str] = self._get_field(f, b'ITCH')
|
self.technician: Optional[str] = self._get_field(f, b'ITCH')
|
||||||
#: 'IKEY' Keywords, keyword list
|
#: 'IKEY' Keywords, keyword list
|
||||||
self.keywords : Optional[str] = self._get_field(f, b'IKEY')
|
self.keywords: Optional[str] = self._get_field(f, b'IKEY')
|
||||||
#: 'INAM' Name, title
|
#: 'INAM' Name, title
|
||||||
self.title : Optional[str] = self._get_field(f, b'INAM')
|
self.title: Optional[str] = self._get_field(f, b'INAM')
|
||||||
#: 'ISRC' Source
|
#: 'ISRC' Source
|
||||||
self.source : Optional[str] = self._get_field(f, b'ISRC')
|
self.source: Optional[str] = self._get_field(f, b'ISRC')
|
||||||
#: 'TAPE' Tape
|
#: 'TAPE' Tape
|
||||||
self.tape : Optional[str] = self._get_field(f, b'TAPE')
|
self.tape: Optional[str] = self._get_field(f, b'TAPE')
|
||||||
#: 'IARL' Archival Location
|
#: 'IARL' Archival Location
|
||||||
self.archival_location : Optional[str] = self._get_field(f, b'IARL')
|
self.archival_location: Optional[str] = self._get_field(f, b'IARL')
|
||||||
#: 'ICSM' Commissioned
|
#: 'ICSM' Commissioned
|
||||||
self.commissioned : Optional[str] = self._get_field(f, b'ICMS')
|
self.commissioned: Optional[str] = self._get_field(f, b'ICMS')
|
||||||
|
|
||||||
def _get_field(self, f, field_ident) -> Optional[str]:
|
def _get_field(self, f, field_ident) -> Optional[str]:
|
||||||
search = next(((chunk.start, chunk.length) for chunk in self.info_chunk.children if chunk.ident == field_ident),
|
search = next(((chunk.start, chunk.length)
|
||||||
|
for chunk in self.info_chunk.children
|
||||||
|
if chunk.ident == field_ident),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
if search is not None:
|
if search is not None:
|
||||||
@@ -59,7 +65,7 @@ class WavInfoChunkReader:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def to_dict(self) -> dict: #FIXME should be asdict
|
def to_dict(self) -> dict: # FIXME should be asdict
|
||||||
"""
|
"""
|
||||||
A dictionary with all of the key/values read from the INFO scope.
|
A dictionary with all of the key/values read from the INFO scope.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
from lxml import etree as ET
|
from lxml import etree as ET
|
||||||
import io
|
import io
|
||||||
from collections import namedtuple
|
# from collections import namedtuple
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
|
|
||||||
|
class IXMLTrack(NamedTuple):
|
||||||
|
channel_index: int
|
||||||
|
interleave_index: int
|
||||||
|
name: str
|
||||||
|
function: str
|
||||||
|
|
||||||
|
|
||||||
class SteinbergMetadata:
|
class SteinbergMetadata:
|
||||||
@@ -29,7 +35,7 @@ class SteinbergMetadata:
|
|||||||
CINE_71 = 27
|
CINE_71 = 27
|
||||||
SDDS_70 = 24
|
SDDS_70 = 24
|
||||||
SDDS_71 = 26
|
SDDS_71 = 26
|
||||||
MUSIC_60 = 21 #??
|
MUSIC_60 = 21 # ??
|
||||||
MUSIC_61 = 23
|
MUSIC_61 = 23
|
||||||
ATMOS_712 = 33
|
ATMOS_712 = 33
|
||||||
ATMOS_504 = 35
|
ATMOS_504 = 35
|
||||||
@@ -72,7 +78,8 @@ class SteinbergMetadata:
|
|||||||
"""
|
"""
|
||||||
`AudioSpeakerArrangement` property
|
`AudioSpeakerArrangement` property
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE")
|
val = self.parsed.find(
|
||||||
|
"./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE")
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return type(self).AudioSpeakerArrangement(int(val.text))
|
return type(self).AudioSpeakerArrangement(int(val.text))
|
||||||
|
|
||||||
@@ -81,7 +88,8 @@ class SteinbergMetadata:
|
|||||||
"""
|
"""
|
||||||
AudioSampleFormatSize
|
AudioSampleFormatSize
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
|
val = self.parsed.find(
|
||||||
|
"./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return int(val.text)
|
return int(val.text)
|
||||||
|
|
||||||
@@ -90,7 +98,8 @@ class SteinbergMetadata:
|
|||||||
"""
|
"""
|
||||||
MediaCompany
|
MediaCompany
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
|
val = self.parsed.find(
|
||||||
|
"./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return val.text
|
return val.text
|
||||||
|
|
||||||
@@ -99,7 +108,8 @@ class SteinbergMetadata:
|
|||||||
"""
|
"""
|
||||||
MediaDropFrames
|
MediaDropFrames
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
|
val = self.parsed.find(
|
||||||
|
"./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return val.text == "1"
|
return val.text == "1"
|
||||||
|
|
||||||
@@ -108,7 +118,8 @@ class SteinbergMetadata:
|
|||||||
"""
|
"""
|
||||||
MediaDuration
|
MediaDuration
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
|
val = self.parsed.find(
|
||||||
|
"./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return float(val.text)
|
return float(val.text)
|
||||||
|
|
||||||
@@ -145,6 +156,7 @@ class WavIXMLFormat:
|
|||||||
"""
|
"""
|
||||||
iXML recorder metadata.
|
iXML recorder metadata.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, xml):
|
def __init__(self, xml):
|
||||||
"""
|
"""
|
||||||
Parse iXML.
|
Parse iXML.
|
||||||
@@ -153,7 +165,7 @@ class WavIXMLFormat:
|
|||||||
self.source = xml
|
self.source = xml
|
||||||
xml_bytes = io.BytesIO(xml)
|
xml_bytes = io.BytesIO(xml)
|
||||||
parser = ET.XMLParser(recover=True)
|
parser = ET.XMLParser(recover=True)
|
||||||
self.parsed : ET.ElementTree = ET.parse(xml_bytes, parser=parser)
|
self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser)
|
||||||
|
|
||||||
def _get_text_value(self, xpath) -> Optional[str]:
|
def _get_text_value(self, xpath) -> Optional[str]:
|
||||||
e = self.parsed.find("./" + xpath)
|
e = self.parsed.find("./" + xpath)
|
||||||
@@ -181,10 +193,13 @@ class WavIXMLFormat:
|
|||||||
"""
|
"""
|
||||||
for track in self.parsed.find("./TRACK_LIST").iter():
|
for track in self.parsed.find("./TRACK_LIST").iter():
|
||||||
if track.tag == 'TRACK':
|
if track.tag == 'TRACK':
|
||||||
yield IXMLTrack(channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
|
yield IXMLTrack(
|
||||||
interleave_index=track.xpath('string(INTERLEAVE_INDEX/text())'),
|
channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
|
||||||
|
interleave_index=track.xpath(
|
||||||
|
'string(INTERLEAVE_INDEX/text())'),
|
||||||
name=track.xpath('string(NAME/text())'),
|
name=track.xpath('string(NAME/text())'),
|
||||||
function=track.xpath('string(FUNCTION/text())'))
|
function=track.xpath('string(FUNCTION/text())')
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project(self) -> Optional[str]:
|
def project(self) -> Optional[str]:
|
||||||
@@ -218,7 +233,8 @@ class WavIXMLFormat:
|
|||||||
def family_uid(self) -> Optional[str]:
|
def family_uid(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
The globally-unique ID for this file family. This may be in the format
|
The globally-unique ID for this file family. This may be in the format
|
||||||
of a GUID, or an EBU Rec 9 source identifier, or some other dumb number.
|
of a GUID, or an EBU Rec 9 source identifier, or some other dumb
|
||||||
|
number.
|
||||||
"""
|
"""
|
||||||
return self._get_text_value("FILE_SET/FAMILY_UID")
|
return self._get_text_value("FILE_SET/FAMILY_UID")
|
||||||
|
|
||||||
@@ -240,11 +256,8 @@ class WavIXMLFormat:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return dict(track_list=list(map(lambda x: x._asdict(), self.track_list)),
|
return dict(
|
||||||
project=self.project,
|
track_list=list(map(lambda x: x._asdict(), self.track_list)),
|
||||||
scene=self.scene,
|
project=self.project, scene=self.scene, take=self.take,
|
||||||
take=self.take,
|
tape=self.tape, family_uid=self.family_uid,
|
||||||
tape=self.tape,
|
family_name=self.family_name)
|
||||||
family_uid=self.family_uid,
|
|
||||||
family_name=self.family_name
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#-*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import struct
|
import struct
|
||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from typing import Optional, Generator, Any, NamedTuple
|
||||||
|
|
||||||
from typing import Optional, Generator, Any
|
|
||||||
|
|
||||||
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
|
||||||
@@ -14,14 +13,24 @@ 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.
|
||||||
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
|
|
||||||
|
|
||||||
|
class WavDataDescriptor(NamedTuple):
|
||||||
|
byte_count: int
|
||||||
|
frame_count: int
|
||||||
|
|
||||||
|
|
||||||
#: The format of the audio samples.
|
#: The format of the audio samples.
|
||||||
WavAudioFormat = namedtuple('WavAudioFormat',
|
class WavAudioFormat(NamedTuple):
|
||||||
['audio_format', 'channel_count', 'sample_rate',
|
audio_format: int
|
||||||
'byte_rate', 'block_align', 'bits_per_sample'])
|
channel_count: int
|
||||||
|
sample_rate: int
|
||||||
|
byte_rate: int
|
||||||
|
block_align: int
|
||||||
|
bits_per_sample: int
|
||||||
|
|
||||||
|
|
||||||
class WavInfoReader:
|
class WavInfoReader:
|
||||||
@@ -51,28 +60,31 @@ class WavInfoReader:
|
|||||||
self.bext_encoding = bext_encoding
|
self.bext_encoding = bext_encoding
|
||||||
|
|
||||||
#: Wave audio data format.
|
#: Wave audio data format.
|
||||||
self.fmt :Optional[WavAudioFormat] = None
|
self.fmt: Optional[WavAudioFormat] = None
|
||||||
|
|
||||||
#: Statistics of the `data` section.
|
#: Statistics of the `data` section.
|
||||||
self.data :Optional[WavDataDescriptor] = None
|
self.data: Optional[WavDataDescriptor] = None
|
||||||
|
|
||||||
#: Broadcast-Wave metadata.
|
#: Broadcast-Wave metadata.
|
||||||
self.bext :Optional[WavBextReader] = None
|
self.bext: Optional[WavBextReader] = None
|
||||||
|
|
||||||
#: iXML metadata.
|
#: iXML metadata.
|
||||||
self.ixml :Optional[WavIXMLFormat] = None
|
self.ixml: Optional[WavIXMLFormat] = None
|
||||||
|
|
||||||
#: ADM Audio Definiton Model metadata.
|
#: ADM Audio Definiton Model metadata.
|
||||||
self.adm :Optional[WavADMReader]= None
|
self.adm: Optional[WavADMReader] = None
|
||||||
|
|
||||||
#: Dolby bitstream metadata.
|
#: Dolby bitstream metadata.
|
||||||
self.dolby :Optional[WavDolbyMetadataReader] = None
|
self.dolby: Optional[WavDolbyMetadataReader] = None
|
||||||
|
|
||||||
#: RIFF INFO metadata.
|
#: RIFF INFO metadata.
|
||||||
self.info :Optional[WavInfoChunkReader]= None
|
self.info: Optional[WavInfoChunkReader] = None
|
||||||
|
|
||||||
#: 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)
|
||||||
@@ -87,8 +99,8 @@ class WavInfoReader:
|
|||||||
|
|
||||||
self.path = absolute_path
|
self.path = absolute_path
|
||||||
|
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as path:
|
||||||
self.get_wav_info(f)
|
self.get_wav_info(path)
|
||||||
|
|
||||||
def get_wav_info(self, wavfile):
|
def get_wav_info(self, wavfile):
|
||||||
chunks = parse_chunk(wavfile)
|
chunks = parse_chunk(wavfile)
|
||||||
@@ -104,11 +116,14 @@ 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, default_none=False) -> Optional[bytes]:
|
def _find_chunk_data(self, ident, from_stream,
|
||||||
top_chunks = (chunk for chunk in self.main_list \
|
default_none=False) -> Optional[bytes]:
|
||||||
if type(chunk) is ChunkDescriptor and chunk.ident == ident)
|
top_chunks = (chunk for chunk in self.main_list
|
||||||
|
if type(chunk) is ChunkDescriptor and
|
||||||
|
chunk.ident == ident)
|
||||||
|
|
||||||
chunk_descriptor = next(top_chunks, None) \
|
chunk_descriptor = next(top_chunks, None) \
|
||||||
if default_none else next(top_chunks)
|
if default_none else next(top_chunks)
|
||||||
@@ -117,14 +132,14 @@ class WavInfoReader:
|
|||||||
if chunk_descriptor else None
|
if chunk_descriptor else None
|
||||||
|
|
||||||
def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]:
|
def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]:
|
||||||
top_chunks = (chunk for chunk in self.main_list \
|
top_chunks = (chunk for chunk in self.main_list
|
||||||
if type(chunk) is ListChunkDescriptor and \
|
if type(chunk) is ListChunkDescriptor and
|
||||||
chunk.signature == signature)
|
chunk.signature == signature)
|
||||||
|
|
||||||
return next(top_chunks, None)
|
return next(top_chunks, None)
|
||||||
|
|
||||||
def _describe_data(self):
|
def _describe_data(self):
|
||||||
data_chunk = next(c for c in self.main_list \
|
data_chunk = next(c for c in self.main_list
|
||||||
if type(c) is ChunkDescriptor and c.ident == b'data')
|
if type(c) is ChunkDescriptor and c.ident == b'data')
|
||||||
|
|
||||||
assert isinstance(self.fmt, WavAudioFormat)
|
assert isinstance(self.fmt, WavAudioFormat)
|
||||||
@@ -150,7 +165,7 @@ class WavInfoReader:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _get_info(self, f, encoding):
|
def _get_info(self, f, encoding):
|
||||||
finder = (chunk.signature for chunk in self.main_list \
|
finder = (chunk.signature for chunk in self.main_list
|
||||||
if type(chunk) is ListChunkDescriptor)
|
if type(chunk) is ListChunkDescriptor)
|
||||||
|
|
||||||
if b'INFO' in finder:
|
if b'INFO' in finder:
|
||||||
@@ -176,8 +191,8 @@ class WavInfoReader:
|
|||||||
return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
|
return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
|
||||||
|
|
||||||
def _get_cue(self, f):
|
def _get_cue(self, f):
|
||||||
cue = next((cue_chunk for cue_chunk in self.main_list if \
|
cue = next((cue_chunk for cue_chunk in self.main_list if
|
||||||
type(cue_chunk) is ChunkDescriptor and \
|
type(cue_chunk) is ChunkDescriptor and
|
||||||
cue_chunk.ident == b'cue '), None)
|
cue_chunk.ident == b'cue '), None)
|
||||||
|
|
||||||
adtl = self._find_list_chunk(b'adtl')
|
adtl = self._find_list_chunk(b'adtl')
|
||||||
@@ -185,24 +200,33 @@ class WavInfoReader:
|
|||||||
ltxts = []
|
ltxts = []
|
||||||
notes = []
|
notes = []
|
||||||
if adtl is not None:
|
if adtl is not None:
|
||||||
labls = [c for c in adtl.children if c.ident == b'labl']
|
labls = [c for c in adtl.children
|
||||||
ltxts = [c for c in adtl.children if c.ident == b'ltxt']
|
if type(c) is ChunkDescriptor and c.ident == b'labl']
|
||||||
notes = [c for c in adtl.children if c.ident == b'note']
|
ltxts = [c for c in adtl.children
|
||||||
|
if type(c) is ChunkDescriptor and c.ident == b'ltxt']
|
||||||
|
notes = [c for c in adtl.children
|
||||||
|
if type(c) is ChunkDescriptor and c.ident == b'note']
|
||||||
|
|
||||||
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 walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()"
|
def _get_sampler_loops(self, f):
|
||||||
|
sampler_data = self._find_chunk_data(b'smpl', f, default_none=True)
|
||||||
|
return WavSmplReader(sampler_data) if sampler_data else None
|
||||||
|
|
||||||
|
# FIXME: this should probably be named "iter()"
|
||||||
|
def walk(self) -> Generator[str, str, Any]:
|
||||||
"""
|
"""
|
||||||
Walk all of the available metadata fields.
|
Walk all of the available metadata fields.
|
||||||
|
|
||||||
: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']:
|
||||||
@@ -211,9 +235,12 @@ class WavInfoReader:
|
|||||||
yield scope, field, attr.__getattribute__(field)
|
yield scope, field, attr.__getattribute__(field)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
dict = self.__getattribute__(scope).to_dict() if self.__getattribute__(scope) else {}
|
mdict = self.__getattribute__(scope).to_dict(
|
||||||
for key in dict.keys():
|
) if self.__getattribute__(scope) else {}
|
||||||
yield scope, key, dict[key]
|
for key in mdict.keys():
|
||||||
|
yield scope, key, mdict[key]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)
|
return 'WavInfoReader({}, {}, {})'.format(self.path,
|
||||||
|
self.info_encoding,
|
||||||
|
self.bext_encoding)
|
||||||
|
|||||||
114
wavinfo/wave_smpl_reader.py
Normal file
114
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,
|
||||||
|
}
|
||||||
@@ -9,10 +9,12 @@ import sys
|
|||||||
def main():
|
def main():
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
|
||||||
parser.usage = "wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] <PATH> +"
|
parser.usage = ("wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] "
|
||||||
|
"<PATH> +")
|
||||||
|
|
||||||
primaries = OptionGroup(parser, title="Search Predicates",
|
primaries = OptionGroup(parser, title="Search Predicates",
|
||||||
description="Argument values can be globs, and are logically-AND'ed.")
|
description="Argument values can be globs, "
|
||||||
|
"and are logically-AND'ed.")
|
||||||
|
|
||||||
primaries.add_option("--scene",
|
primaries.add_option("--scene",
|
||||||
help='Search for this scene',
|
help='Search for this scene',
|
||||||
@@ -26,7 +28,6 @@ def main():
|
|||||||
help='Search descriptions',
|
help='Search descriptions',
|
||||||
metavar='DESC')
|
metavar='DESC')
|
||||||
|
|
||||||
|
|
||||||
(options, args) = parser.parse_args(sys.argv)
|
(options, args) = parser.parse_args(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user