mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
321 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ce7b9ad9 | ||
|
|
00728f5af3 | ||
|
|
4e061a85f1 | ||
|
|
af5c83b8fc | ||
|
|
a7f77a49f7 | ||
|
|
0acbe58f0b | ||
|
|
8d908a3e34 | ||
|
|
c897d080bb | ||
|
|
710473f2aa | ||
|
|
cf48763b13 | ||
|
|
5651367df7 | ||
|
|
6d7373391e | ||
|
|
ff60f26f78 | ||
|
|
cc49df8f08 | ||
|
|
bdf5fc9349 | ||
|
|
4109f77372 | ||
|
|
7b9b64d799 | ||
|
|
e5cd098d44 | ||
|
|
957b23db92 | ||
|
|
733113819e | ||
|
|
df4cc8822e | ||
|
|
d5b6f15e28 | ||
|
|
b830b8cdc2 | ||
|
|
b23470ac19 | ||
|
|
8fe7eefb4a | ||
|
|
f0b7a0ddf6 | ||
|
|
e83603cb47 | ||
|
|
b6acdb1f7f | ||
|
|
faf809b8e2 | ||
|
|
f7a1896f99 | ||
|
|
40aee91162 | ||
|
|
9f8fc87d17 | ||
|
|
b2323a126f | ||
|
|
8fcc9787f6 | ||
|
|
52ea6fdb60 | ||
|
|
c26942db04 | ||
|
|
12eff79e5f | ||
|
|
d9e3e8deee | ||
|
|
c17fb242e3 | ||
|
|
64f3a640e3 | ||
|
|
5d4f97f6cc | ||
|
|
f9e5f28f7d | ||
|
|
3e6c485eb9 | ||
|
|
436bbe1686 | ||
|
|
ddb4d5cdca | ||
|
|
cec8165919 | ||
|
|
73a5034e02 | ||
|
|
9a46db4ae5 | ||
|
|
ccca30e234 | ||
|
|
c367acc185 | ||
|
|
2266cc5032 | ||
|
|
ec5b796181 | ||
|
|
97bdb23441 | ||
|
|
8f2fd69b00 | ||
|
|
a063fffb41 | ||
|
|
5b9d326e94 | ||
|
|
85775055a9 | ||
|
|
59509e4399 | ||
|
|
3a63ce9c8c | ||
|
|
5bfe0bd95b | ||
|
|
992de72cc9 | ||
|
|
ee305cebf4 | ||
|
|
ea4f484488 | ||
|
|
d00e07be36 | ||
|
|
68931348a6 | ||
|
|
68c75fc43f | ||
|
|
1eca249ba4 | ||
|
|
2052fa385a | ||
|
|
3096f02971 | ||
|
|
be47786439 | ||
|
|
ecde5359f1 | ||
|
|
8ae73213bc | ||
|
|
53217ce293 | ||
|
|
f9969d32cc | ||
|
|
04c402680b | ||
|
|
f10a546fe9 | ||
|
|
ec42ee1d3d | ||
|
|
bba4d67641 | ||
|
|
4bc7f94198 | ||
|
|
14eb8df496 | ||
|
|
a3aee8e785 | ||
|
|
9e9b6b512b | ||
|
|
a3365c113d | ||
|
|
fbf4d72915 | ||
|
|
90f273cf99 | ||
|
|
bec3d98ee7 | ||
|
|
a87bc71755 | ||
|
|
4817e7eb49 | ||
|
|
07832f7133 | ||
|
|
f3d03296d5 | ||
|
|
9496912b15 | ||
|
|
1077b49ce0 | ||
|
|
90042d57b2 | ||
|
|
f12d7dfea0 | ||
|
|
c2327568a8 | ||
|
|
722df48f9b | ||
|
|
17c0357364 | ||
|
|
7e88c46d54 | ||
|
|
df90c67a73 | ||
|
|
cc107bf65d | ||
|
|
f9c68e0995 | ||
|
|
a98dd2668a | ||
|
|
d8ff4ed63b | ||
|
|
084c3d7ae5 | ||
|
|
e69b71e989 | ||
|
|
6fe6126f3a | ||
|
|
e57c76a722 | ||
|
|
e40a2c5471 | ||
|
|
dec6180744 | ||
|
|
edbe748718 | ||
|
|
2019a4ec63 | ||
|
|
fb43838c7d | ||
|
|
4cd58b8ddd | ||
|
|
38dab7723f | ||
|
|
354d88a5b2 | ||
|
|
d8a405b6d2 | ||
|
|
5f7e467fbd | ||
|
|
3377ddb4b9 | ||
|
|
9cd6cf7f12 | ||
|
|
1f8ebe253b | ||
|
|
fe46d1b242 | ||
|
|
b213933ad8 | ||
|
|
7e314f7475 | ||
|
|
b2d6fd3c92 | ||
|
|
c4d8608c8f | ||
|
|
5605b05f9f | ||
|
|
5d71cabda7 | ||
|
|
c2f87b1fef | ||
|
|
3db40d4f12 | ||
|
|
40b30f5bd8 | ||
|
|
048f20c64c | ||
|
|
6a69df2ee8 | ||
|
|
ec327ee76f | ||
|
|
62a34cfee8 | ||
|
|
c966097e7d | ||
|
|
35311e394d | ||
|
|
0633a8685c | ||
|
|
f65665a06c | ||
|
|
261572cff3 | ||
|
|
623a8569fd | ||
|
|
31493f7cf4 | ||
|
|
4809bb4844 | ||
|
|
a76f3b1518 | ||
|
|
b8cb585d50 | ||
|
|
84a76f9c74 | ||
|
|
5a1a12e21e | ||
|
|
06835ffe11 | ||
|
|
1b25b8214d | ||
|
|
cfc1a451bc | ||
|
|
0788613ea3 | ||
|
|
e9e4b4bcbb | ||
|
|
c114eb7cf3 | ||
|
|
4576d65da6 | ||
|
|
65994db36d | ||
|
|
1a3417d3e5 | ||
|
|
7589d5fb82 | ||
|
|
6014d1d48b | ||
|
|
f8bf6cb4a0 | ||
|
|
6d8e717f42 | ||
|
|
ba232605db | ||
|
|
9a90a0c310 | ||
|
|
add390c0a0 | ||
|
|
7351623e3a | ||
|
|
c23ca4bded | ||
|
|
8fe799b211 | ||
|
|
18ebd22ec1 | ||
|
|
cf8aa36fc3 | ||
|
|
12d16a472f | ||
|
|
39210738e3 | ||
|
|
966da7c4a2 | ||
|
|
5a30ce3afc | ||
|
|
1b9547e8c2 | ||
|
|
1507898b9e | ||
|
|
4f2a6689f5 | ||
|
|
597acb2122 | ||
|
|
9d9592e9e1 | ||
|
|
b0c5a7de72 | ||
|
|
c36b53c5c5 | ||
|
|
25485d9601 | ||
|
|
ffa51eaff4 | ||
|
|
93a9ca0fd3 | ||
|
|
b930fc6d6e | ||
|
|
5f7803fd00 | ||
|
|
e37a37221b | ||
|
|
d0b0b06ecb | ||
|
|
5824406ae6 | ||
|
|
dbbc0683f5 | ||
|
|
24c6871bbc | ||
|
|
c62dfb7a8a | ||
|
|
6c8cc47788 | ||
|
|
a90d3f4b38 | ||
|
|
3ede4de06a | ||
|
|
5d4bb13ad6 | ||
|
|
e132c5846c | ||
|
|
b87b43aab4 | ||
|
|
6b42a6bb09 | ||
|
|
7a315be242 | ||
|
|
eb3e2adc27 | ||
|
|
3e7faefedb | ||
|
|
54ce5abe77 | ||
|
|
2f124b0d56 | ||
|
|
cd11b0924b | ||
|
|
ff862aafe9 | ||
|
|
e0432458cc | ||
|
|
b4613ed6f4 | ||
|
|
8d44d411d7 | ||
|
|
6005f79e60 | ||
|
|
841b86f3f4 | ||
|
|
5c90d5ff47 | ||
|
|
60e329fdb4 | ||
|
|
15db4c9ffa | ||
|
|
49ac961b94 | ||
|
|
7fc530b2cd | ||
|
|
4dfc1ab33c | ||
|
|
4770c781b2 | ||
|
|
45c2aae640 | ||
|
|
792e5505b6 | ||
|
|
8f575c5131 | ||
|
|
128dffef4e | ||
|
|
ae1594d496 | ||
|
|
b5713479cd | ||
|
|
f1be6852b6 | ||
|
|
242fa51b32 | ||
|
|
177e913c86 | ||
|
|
1827b46e34 | ||
|
|
4b6407c1f4 | ||
|
|
99bfa99b3b | ||
|
|
b7aeccacf5 | ||
|
|
9d5f8899d5 | ||
|
|
0ce18d9f13 | ||
|
|
3f3fbc7632 | ||
|
|
809ff71bb4 | ||
|
|
a6ed0152db | ||
|
|
526bc0c29c | ||
|
|
d3c8207a33 | ||
|
|
185c810e7c | ||
|
|
ebc224d5d2 | ||
|
|
976d5844e2 | ||
|
|
f1fb83f208 | ||
|
|
8d0d51b7fa | ||
|
|
188ba855b2 | ||
|
|
2f1511d935 | ||
|
|
8c8fdc1bb3 | ||
|
|
8564fd1fd8 | ||
|
|
77aadd563f | ||
|
|
c2ddee8f6a | ||
|
|
dc08bd39fe | ||
|
|
12de7b078f | ||
|
|
7992640fb8 | ||
|
|
88da763ca1 | ||
|
|
1e0f31a794 | ||
|
|
0e1094421e | ||
|
|
4f0c26f5ca | ||
|
|
71f0aed5ff | ||
|
|
f8feec8119 | ||
|
|
16bc4b016c | ||
|
|
83bf656ad3 | ||
|
|
9e259e9d6c | ||
|
|
c20b17e82c | ||
|
|
5315575e35 | ||
|
|
52613b78f3 | ||
|
|
0c24c2d986 | ||
|
|
2a86d7824d | ||
|
|
c2be0e77c5 | ||
|
|
9b8b964e74 | ||
|
|
488c0f2aa9 | ||
|
|
da51b22c59 | ||
|
|
a4ffe7dd6b | ||
|
|
0ec5425cd8 | ||
|
|
8e965f53e5 | ||
|
|
9e16f6ab1f | ||
|
|
477c71830e | ||
|
|
ed8b5f167e | ||
|
|
97c25ab61f | ||
|
|
910b3854c7 | ||
|
|
f12bb0eea4 | ||
|
|
c88599f4fd | ||
|
|
68ccb09f53 | ||
|
|
b7d9f6758d | ||
|
|
e3adde7498 | ||
|
|
96885dfb4e | ||
|
|
126f1ea7c3 | ||
|
|
0bb664357e | ||
|
|
9e2c60caf8 | ||
|
|
82d73b0316 | ||
|
|
0612e62a7b | ||
|
|
8fd509482c | ||
|
|
a9dbfdf5ec | ||
|
|
413826b18f | ||
|
|
71201adee4 | ||
|
|
d88117878c | ||
|
|
b20c5dd1bd | ||
|
|
6c9fb38482 | ||
|
|
f5c0700e47 | ||
|
|
3c101badac | ||
|
|
50962aefcc | ||
|
|
f52b2a195a | ||
|
|
4586d19a5f | ||
|
|
3e897d030c | ||
|
|
9f943aeb61 | ||
|
|
a9c3600ad2 | ||
|
|
27d9a2f005 | ||
|
|
0c5c0a2088 | ||
|
|
b150cd6d8e | ||
|
|
849ade92a4 | ||
|
|
482a3f86d1 | ||
|
|
991a12cbb5 | ||
|
|
37b816045d | ||
|
|
99aa29c5f3 | ||
|
|
41b599923a | ||
|
|
ae09897abf | ||
|
|
d37726f090 | ||
|
|
ae52152111 | ||
|
|
004249773a | ||
|
|
ebbdb99c46 | ||
|
|
32454039bf | ||
|
|
830c702376 | ||
|
|
0723f21e4f | ||
|
|
08743be3fa | ||
|
|
bdb39684c7 | ||
|
|
c3c3c12d38 |
26
.devcontainer/devcontainer.json
Normal file
26
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/anaconda:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/python:1": {
|
||||||
|
"version":"3.11"
|
||||||
|
},
|
||||||
|
"ghcr.io/meaningful-ooo/devcontainer-features/fish:1": {}
|
||||||
|
},
|
||||||
|
"postCreateCommand": "pip3 install -r requirements.txt && pip3 install -r docs/requirements.txt && pip3 install pytest && conda install -y ffmpeg",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.isort",
|
||||||
|
"ms-toolsai.jupyter",
|
||||||
|
"ms-toolsai.vscode-jupyter-cell-tags",
|
||||||
|
"ms-toolsai.jupyter-keymap",
|
||||||
|
"ms-toolsai.jupyter-renderers",
|
||||||
|
"ms-toolsai.vscode-jupyter-slideshow",
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"george-alisson.html-preview-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
.github/workflows/python-package.yml
vendored
Normal file
42
.github/workflows/python-package.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 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: Python Lint and Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.8", "3.9", "3.10", "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 pytest
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
|
- name: Setup FFmpeg
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
pytest
|
||||||
36
.github/workflows/pythonpublish.yml
vendored
Normal file
36
.github/workflows/pythonpublish.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Upload Python Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install setuptools wheel twine lxml
|
||||||
|
- name: Build and publish
|
||||||
|
env:
|
||||||
|
TWINE_USERNAME: __token__
|
||||||
|
TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }}
|
||||||
|
run: |
|
||||||
|
python setup.py sdist bdist_wheel
|
||||||
|
twine upload dist/*
|
||||||
|
- name: Report to Mastodon
|
||||||
|
uses: cbrgm/mastodon-github-action@v1.0.1
|
||||||
|
with:
|
||||||
|
message: |
|
||||||
|
I just released a new version of wavinfo, my library for reading WAVE file metadata!
|
||||||
|
#sounddesign #filmmaking #audio #python
|
||||||
|
${{ github.server_url }}/${{ github.repository }}
|
||||||
|
env:
|
||||||
|
MASTODON_URL: ${{ secrets.MASTODON_URL }}
|
||||||
|
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -89,6 +89,8 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
docs_venv/
|
||||||
|
venv_docs/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@@ -105,3 +107,6 @@ venv.bak/
|
|||||||
|
|
||||||
# vim swap
|
# vim swap
|
||||||
*.swp
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
|||||||
29
.readthedocs.yaml
Normal file
29
.readthedocs.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-20.04
|
||||||
|
tools:
|
||||||
|
python: "3.10"
|
||||||
|
# You can also specify other tool versions:
|
||||||
|
# nodejs: "16"
|
||||||
|
# rust: "1.55"
|
||||||
|
# golang: "1.17"
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/source/conf.py
|
||||||
|
|
||||||
|
#If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||||
|
formats:
|
||||||
|
- pdf
|
||||||
|
|
||||||
|
#Optionally declare the Python requirements required to build your docs
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: requirements.txt
|
||||||
13
.travis.yml
13
.travis.yml
@@ -1,13 +0,0 @@
|
|||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.6"
|
|
||||||
- "3.5"
|
|
||||||
- "3.4"
|
|
||||||
- "3.3"
|
|
||||||
- "3.2"
|
|
||||||
- "3.1"
|
|
||||||
- "3.0"
|
|
||||||
script:
|
|
||||||
- "python3 setup.py test"
|
|
||||||
install:
|
|
||||||
- "pip3 install setuptools"
|
|
||||||
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Contributions to this project are very welcome!
|
||||||
|
|
||||||
|
If you discover a bug or would like better support for a feature, please do the following:
|
||||||
|
|
||||||
|
1. Submit an Issue.
|
||||||
|
|
||||||
|
I'm actively developing this project and will review incoming issues.
|
||||||
|
1. Check out the source code and submit a PR.
|
||||||
|
|
||||||
|
If you're facile with Python and understand what you'd like to fix, submit a PR and I'll
|
||||||
|
review it as soon as I can. There's a `.devcontainer` available so you can creates commits
|
||||||
|
on this project in a GitHub codespace.
|
||||||
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 Jamie Hardt
|
Copyright (c) 2022 Jamie Hardt
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
113
README.md
113
README.md
@@ -1,115 +1,58 @@
|
|||||||
[](https://travis-ci.com/iluvcapra/wavinfo)
|
|
||||||
[](https://wavinfo.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/wavinfo/) 
|
[](https://wavinfo.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/wavinfo/) 
|
||||||
|
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
|
||||||
|
|
||||||
# wavinfo
|
# wavinfo
|
||||||
|
|
||||||
|
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
|
||||||
|
|
||||||
The `wavinfo` package allows you to probe WAVE files and extract extended metadata, with an emphasis on
|
|
||||||
production metadata.
|
## Metadata Support
|
||||||
|
|
||||||
`wavinfo` reads:
|
`wavinfo` reads:
|
||||||
|
|
||||||
* __Broadcast-WAVE__ metadata, compliant with [EBU Tech 3285v2 (2011)][ebu], including embedded program loudness and coding history, if extant.
|
* [__Broadcast-WAVE__][ebu] metadata, including embedded program
|
||||||
* [__iXML__ production recorder metadata][ixml], including project, scene, and take tags, recorder notes and file family information.
|
loudness, coding history and [__SMPTE UMID__][smpte_330m2011].
|
||||||
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count information.
|
* [__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 take tags, recorder notes
|
||||||
|
and file family information.
|
||||||
|
* Most of the common [__RIFF INFO__][info-tags] metadata fields.
|
||||||
|
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count
|
||||||
|
information.
|
||||||
|
|
||||||
In progress:
|
In progress:
|
||||||
* [SMPTE 330M __UMID__][smpte_330m2011] Unique Materials Identifier.
|
|
||||||
* iXML `STEINBERG` sound library attributes.
|
|
||||||
* Most of the common __RIFF INFO__ metadata fields.
|
|
||||||
* Pro Tools __embedded regions__.
|
* Pro Tools __embedded regions__.
|
||||||
|
* iXML `STEINBERG` sound library attributes.
|
||||||
This module is presently under construction and not sutiable for production at this time.
|
|
||||||
|
|
||||||
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
|
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
|
||||||
|
[ebu3285s6]:https://tech.ebu.ch/docs/tech/tech3285s6.pdf
|
||||||
|
[adm]:https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2076-2-201910-I!!PDF-E.pdf
|
||||||
[smpte_330m2011]:http://standards.smpte.org/content/978-1-61482-678-1/st-330-2011/SEC1.abstract
|
[smpte_330m2011]:http://standards.smpte.org/content/978-1-61482-678-1/st-330-2011/SEC1.abstract
|
||||||
[ixml]:http://www.ixml.infoi
|
[ixml]:http://www.ixml.info
|
||||||
|
[eburf64]:https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
|
||||||
|
[info-tags]:https://exiftool.org/TagNames/RIFF.html#Info
|
||||||
|
|
||||||
## Demonstration
|
|
||||||
|
|
||||||
|
## How To Use
|
||||||
|
|
||||||
The entry point for wavinfo is the WavInfoReader class.
|
The entry point for wavinfo is the WavInfoReader class.
|
||||||
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from wavinfo import WavInfoReader
|
from wavinfo import WavInfoReader
|
||||||
|
|
||||||
path = '../tests/test_files/A101_1.WAV'
|
path = '../tests/test_files/A101_1.WAV'
|
||||||
|
|
||||||
info = WavInfoReader(path)
|
info = WavInfoReader(path)
|
||||||
|
|
||||||
|
adm_metadata = info.adm
|
||||||
|
ixml_metadata = info.ixml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Basic WAV Data
|
The package also installs a shell command:
|
||||||
|
|
||||||
|
```sh
|
||||||
The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk.
|
$ wavinfo test_files/A101_1.WAV
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
(info.data.frame_count, info.data.byte_count)
|
|
||||||
>>> (240239, 1441434)
|
|
||||||
(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)
|
|
||||||
>>> (48000, 2, 6, 24)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Broadcast WAV Extension
|
## Other Resources
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
print(info.bext.description)
|
|
||||||
print("----------")
|
|
||||||
print("Originator:", info.bext.originator)
|
|
||||||
print("Originator Ref:", info.bext.originator_ref)
|
|
||||||
print("Originator Date:", info.bext.originator_date)
|
|
||||||
print("Originator Time:", info.bext.originator_time)
|
|
||||||
print("Time Reference:", info.bext.time_reference)
|
|
||||||
print(info.bext.coding_history)
|
|
||||||
```
|
|
||||||
|
|
||||||
sSPEED=023.976-ND
|
|
||||||
sTAKE=1
|
|
||||||
sUBITS=$12311801
|
|
||||||
sSWVER=2.67
|
|
||||||
sPROJECT=BMH
|
|
||||||
sSCENE=A101
|
|
||||||
sFILENAME=A101_1.WAV
|
|
||||||
sTAPE=18Y12M31
|
|
||||||
sTRK1=MKH516 A
|
|
||||||
sTRK2=Boom
|
|
||||||
sNOTE=
|
|
||||||
|
|
||||||
----------
|
|
||||||
Originator: Sound Dev: 702T S#GR1112089007
|
|
||||||
Originator Ref: USSDVGR1112089007124001008206301
|
|
||||||
Originator Date: 2018-12-31
|
|
||||||
Originator Time: 12:40:00
|
|
||||||
Time Reference: 2190940753
|
|
||||||
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## iXML Production Recorder Metadata
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
print("iXML Project:", info.ixml.project)
|
|
||||||
print("iXML Scene:", info.ixml.scene)
|
|
||||||
print("iXML Take:", info.ixml.take)
|
|
||||||
print("iXML Tape:", info.ixml.tape)
|
|
||||||
print("iXML File Family Name:", info.ixml.family_name)
|
|
||||||
print("iXML File Family UID:", info.ixml.family_uid)
|
|
||||||
```
|
|
||||||
|
|
||||||
iXML Project: BMH
|
|
||||||
iXML Scene: A101
|
|
||||||
iXML Take: 1
|
|
||||||
iXML Tape: 18Y12M31
|
|
||||||
iXML File Family Name: None
|
|
||||||
iXML File Family UID: USSDVGR1112089007124001008206300
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).
|
||||||
|
|||||||
104
demo.md
104
demo.md
@@ -1,104 +0,0 @@
|
|||||||
|
|
||||||
# `wavinfo` Demonstration
|
|
||||||
|
|
||||||
The entry point for wavinfo is the WavInfoReader class.
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
from wavinfo import WavInfoReader
|
|
||||||
|
|
||||||
path = '../tests/test_files/A101_1.WAV'
|
|
||||||
|
|
||||||
info = WavInfoReader(path)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Basic WAV Data
|
|
||||||
|
|
||||||
The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk.
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
(info.data.frame_count, info.data.byte_count)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(240239, 1441434)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(48000, 2, 6, 24)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Broadcast WAV Extension
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
print(info.bext.description)
|
|
||||||
print("----------")
|
|
||||||
print("Originator:", info.bext.originator)
|
|
||||||
print("Originator Ref:", info.bext.originator_ref)
|
|
||||||
print("Originator Date:", info.bext.originator_date)
|
|
||||||
print("Originator Time:", info.bext.originator_time)
|
|
||||||
print("Time Reference:", info.bext.time_reference)
|
|
||||||
print(info.bext.coding_history)
|
|
||||||
```
|
|
||||||
|
|
||||||
sSPEED=023.976-ND
|
|
||||||
sTAKE=1
|
|
||||||
sUBITS=$12311801
|
|
||||||
sSWVER=2.67
|
|
||||||
sPROJECT=BMH
|
|
||||||
sSCENE=A101
|
|
||||||
sFILENAME=A101_1.WAV
|
|
||||||
sTAPE=18Y12M31
|
|
||||||
sTRK1=MKH516 A
|
|
||||||
sTRK2=Boom
|
|
||||||
sNOTE=
|
|
||||||
|
|
||||||
----------
|
|
||||||
Originator: Sound Dev: 702T S#GR1112089007
|
|
||||||
Originator Ref: USSDVGR1112089007124001008206301
|
|
||||||
Originator Date: 2018-12-31
|
|
||||||
Originator Time: 12:40:00
|
|
||||||
Time Reference: 2190940753
|
|
||||||
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## iXML Production Recorder Metadata
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
print("iXML Project:", info.ixml.project)
|
|
||||||
print("iXML Scene:", info.ixml.scene)
|
|
||||||
print("iXML Take:", info.ixml.take)
|
|
||||||
print("iXML Tape:", info.ixml.tape)
|
|
||||||
print("iXML File Family Name:", info.ixml.family_name)
|
|
||||||
print("iXML File Family UID:", info.ixml.family_uid)
|
|
||||||
```
|
|
||||||
|
|
||||||
iXML Project: BMH
|
|
||||||
iXML Scene: A101
|
|
||||||
iXML Take: 1
|
|
||||||
iXML Tape: 18Y12M31
|
|
||||||
iXML File Family Name: None
|
|
||||||
iXML File Family UID: USSDVGR1112089007124001008206300
|
|
||||||
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
|
|
||||||
```
|
|
||||||
19
docs/Makefile
Normal file
19
docs/Makefile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SOURCEDIR = source
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
25
docs/requirements.txt
Normal file
25
docs/requirements.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
alabaster==0.7.12
|
||||||
|
Babel==2.11.0
|
||||||
|
certifi==2022.9.24
|
||||||
|
charset-normalizer==2.1.1
|
||||||
|
docutils==0.17.1
|
||||||
|
idna==3.4
|
||||||
|
imagesize==1.4.1
|
||||||
|
Jinja2==3.1.2
|
||||||
|
lxml==4.9.1
|
||||||
|
MarkupSafe==2.1.1
|
||||||
|
packaging==21.3
|
||||||
|
Pygments==2.13.0
|
||||||
|
pyparsing==3.0.9
|
||||||
|
pytz==2022.6
|
||||||
|
requests==2.28.1
|
||||||
|
snowballstemmer==2.2.0
|
||||||
|
Sphinx==5.3.0
|
||||||
|
sphinx-rtd-theme==1.1.1
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
|
urllib3==1.26.12
|
||||||
10
docs/source/classes.rst
Normal file
10
docs/source/classes.rst
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Other wavinfo Classes
|
||||||
|
=====================
|
||||||
|
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_reader.WavAudioFormat
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
|
||||||
|
:members:
|
||||||
|
|
||||||
94
docs/source/command_line.rst
Normal file
94
docs/source/command_line.rst
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
Using `wavinfo` from the Command Line
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
`wavinfo` installs a command-line entry point that will read wav files
|
||||||
|
from the command line and output metadata to stdout.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
$ wavinfo [--ixml | --adm] INFILE +
|
||||||
|
|
||||||
|
By default, `wavinfo` will output a JSON dictionary for each file argument.
|
||||||
|
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
|
||||||
|
Two option flags will change the behavior of the command:
|
||||||
|
|
||||||
|
``--ixml``
|
||||||
|
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload
|
||||||
|
of each input wave file, or will emit an error message to stderr if iXML
|
||||||
|
metadata is not present.
|
||||||
|
|
||||||
|
``--adm``
|
||||||
|
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
|
||||||
|
ADM XML metadata is not present.
|
||||||
|
|
||||||
|
These options are mutually-exclusive, with `\-\-adm` taking precedence.
|
||||||
|
|
||||||
|
|
||||||
|
Example Output
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"filename": "tests/test_files/sounddevices/A101_1.WAV",
|
||||||
|
"run_date": "2022-11-26T17:56:38.342935",
|
||||||
|
"application": "wavinfo 2.1.0",
|
||||||
|
"scopes": {
|
||||||
|
"fmt": {
|
||||||
|
"audio_format": 1,
|
||||||
|
"channel_count": 2,
|
||||||
|
"sample_rate": 48000,
|
||||||
|
"byte_rate": 288000,
|
||||||
|
"block_align": 6,
|
||||||
|
"bits_per_sample": 24
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"byte_count": 1441434,
|
||||||
|
"frame_count": 240239
|
||||||
|
},
|
||||||
|
"ixml": {
|
||||||
|
"track_list": [
|
||||||
|
{
|
||||||
|
"channel_index": "1",
|
||||||
|
"interleave_index": "1",
|
||||||
|
"name": "MKH516 A",
|
||||||
|
"function": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel_index": "2",
|
||||||
|
"interleave_index": "2",
|
||||||
|
"name": "Boom",
|
||||||
|
"function": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"project": "BMH",
|
||||||
|
"scene": "A101",
|
||||||
|
"take": "1",
|
||||||
|
"tape": "18Y12M31",
|
||||||
|
"family_uid": "USSDVGR1112089007124001008206300",
|
||||||
|
"family_name": null
|
||||||
|
},
|
||||||
|
"bext": {
|
||||||
|
"description": "sSPEED=023.976-ND\r\nsTAKE=1\r\nsUBITS=$12311801\r\nsSWVER=2.67\r\nsPROJECT=BMH\r\nsSCENE=A101\r\nsFILENAME=A101_1.WAV\r\nsTAPE=18Y12M31\r\nsTRK1=MKH516 A\r\nsTRK2=Boom\r\nsNOTE=\r\n",
|
||||||
|
"originator": "Sound Dev: 702T S#GR1112089007",
|
||||||
|
"originator_ref": "USSDVGR1112089007124001008206301",
|
||||||
|
"originator_date": "2018-12-31",
|
||||||
|
"originator_time": "12:40:00",
|
||||||
|
"time_reference": 2190940753,
|
||||||
|
"version": 1,
|
||||||
|
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
|
||||||
|
"loudness_value": null,
|
||||||
|
"loudness_range": null,
|
||||||
|
"max_true_peak": null,
|
||||||
|
"max_momentary_loudness": null,
|
||||||
|
"max_shortterm_loudness": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
187
docs/source/conf.py
Normal file
187
docs/source/conf.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# This file does only contain a selection of the most common options. For a
|
||||||
|
# full list see the documentation:
|
||||||
|
# http://www.sphinx-doc.org/en/master/config
|
||||||
|
|
||||||
|
# -- Path setup --------------------------------------------------------------
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
sys.path.insert(0, os.path.abspath("../../.."))
|
||||||
|
print(sys.path)
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
project = u'wavinfo'
|
||||||
|
copyright = u'2022, Jamie Hardt'
|
||||||
|
author = u'Jamie Hardt'
|
||||||
|
|
||||||
|
# The short X.Y version
|
||||||
|
version = wavinfo.__version__
|
||||||
|
# The full version, including alpha/beta/rc tags
|
||||||
|
release = wavinfo.__version__
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.todo',
|
||||||
|
'sphinx.ext.coverage',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = 'en'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
|
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = None
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
|
# to template names.
|
||||||
|
#
|
||||||
|
# The default sidebars (for documents that don't match any pattern) are
|
||||||
|
# defined by theme itself. Builtin themes are using these templates by
|
||||||
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||||
|
# 'searchbox.html']``.
|
||||||
|
#
|
||||||
|
# html_sidebars = {}
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'wavinfodoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'wavinfo.tex', u'wavinfo Documentation',
|
||||||
|
u'Jamie Hardt', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'wavinfo', u'wavinfo Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ----------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'wavinfo', u'wavinfo Documentation',
|
||||||
|
author, 'wavinfo', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Epub output -------------------------------------------------
|
||||||
|
|
||||||
|
# Bibliographic Dublin Core info.
|
||||||
|
epub_title = project
|
||||||
|
|
||||||
|
# The unique identifier of the text. This can be a ISBN number
|
||||||
|
# or the project homepage.
|
||||||
|
#
|
||||||
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
# A unique identification for the text.
|
||||||
|
#
|
||||||
|
# epub_uid = ''
|
||||||
|
|
||||||
|
# A list of files that should not be packed into the epub file.
|
||||||
|
epub_exclude_files = ['search.html']
|
||||||
|
|
||||||
|
|
||||||
|
# -- Extension configuration -------------------------------------------------
|
||||||
|
|
||||||
|
# -- Options for todo extension ----------------------------------------------
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = True
|
||||||
31
docs/source/index.rst
Normal file
31
docs/source/index.rst
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.. wavinfo documentation master file, created by
|
||||||
|
sphinx-quickstart on Thu Jan 3 17:09:28 2019.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
Welcome to wavinfo's documentation!
|
||||||
|
===================================
|
||||||
|
|
||||||
|
The `wavinfo` package allows you to probe WAVE and RF64/WAVE files and
|
||||||
|
extract extended metadata, with an emphasis on film, video and professional
|
||||||
|
music production metadata.
|
||||||
|
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:glob:
|
||||||
|
:numbered:
|
||||||
|
|
||||||
|
quickstart
|
||||||
|
command_line
|
||||||
|
scopes/*
|
||||||
|
|
||||||
|
classes
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
24
docs/source/quickstart.rst
Normal file
24
docs/source/quickstart.rst
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
wavinfo Quickstart
|
||||||
|
====================
|
||||||
|
|
||||||
|
All metadata is read by an instance of :class:`WaveInfoReader<wavinfo.wave_reader.WavInfoReader>`.
|
||||||
|
Each type of metadata, iXML, Broadcast-WAV etc. is accessible through *scopes*, properties on an
|
||||||
|
instance of :class:`WaveInfoReader`.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: Using wavinfo
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
path = 'path/to/your/wave/audio.wav'
|
||||||
|
|
||||||
|
info = wavinfo.WavInfoReader(path)
|
||||||
|
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_reader.WavInfoReader
|
||||||
|
:members:
|
||||||
|
|
||||||
30
docs/source/scopes/adm.rst
Normal file
30
docs/source/scopes/adm.rst
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
ADM (Audio Definition Model) Metadata
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
`ADM metadata`_ is used in master recordings to describe the format and content
|
||||||
|
of the tracks. In practice on wave files, ADM tells a client which tracks are
|
||||||
|
members of multichannel stems or "beds" and their speaker assignment, and which
|
||||||
|
tracks are freely-positioned 3D objects. ADM also records the panning moves on
|
||||||
|
object tracks and their content group ("Dialogue", "Music", "Effects" etc.)
|
||||||
|
|
||||||
|
ADM wave files created with a Dolby Rendering and Mastering Unit are a common
|
||||||
|
deliverable in feature film and television production. The `Dolby Atmos ADM Profile`_
|
||||||
|
describes how the RMU translates its native Master format into ADM.
|
||||||
|
|
||||||
|
|
||||||
|
.. _ADM metadata: https://adm.ebu.io
|
||||||
|
.. _Dolby Atmos ADM Profile: https://developer.dolby.com/globalassets/documentation/technology/dolby_atmos_master_adm_profile_v1.0.pdf
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_adm_reader.WavADMReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_adm_reader.ChannelEntry
|
||||||
|
:members:
|
||||||
75
docs/source/scopes/bext.rst
Normal file
75
docs/source/scopes/bext.rst
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
Broadcast WAV Extension Metadata
|
||||||
|
================================
|
||||||
|
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
A WAV file produced to Broadcast-WAV specifications will have the broadcast metadata extension,
|
||||||
|
which includes a 256-character free text descrption, creating entity identifier (usually the
|
||||||
|
recording application or equipment), the date and time of recording and a time reference for
|
||||||
|
timecode synchronization.
|
||||||
|
|
||||||
|
The :py:attr:`coding_history<wavinfo.wave_bext_reader.WavBextReader.coding_history>`
|
||||||
|
is designed to contain a record of every conversion performed on the audio file.
|
||||||
|
|
||||||
|
In this example (from a Sound Devices 702T) the bext metadata contains scene/take slating
|
||||||
|
information in the :py:attr:`description<wavinfo.wave_bext_reader.WavBextReader.description>`.
|
||||||
|
Here also the :py:attr:`originator_ref<wavinfo.wave_bext_reader.WavBextReader.originator_ref>`
|
||||||
|
is a serial number conforming to EBU Rec 99.
|
||||||
|
|
||||||
|
If the bext metadata conforms to `EBU 3285 v1`_, it will contain the WAV's 32 or 64 byte `SMPTE
|
||||||
|
ST 330 UMID`_. The 32-byte version of the UMID is usually just a random number, while the 64-byte
|
||||||
|
UMID will also have information on the recording date and time, recording equipment and entity,
|
||||||
|
and geolocation data.
|
||||||
|
|
||||||
|
If the bext metadata conforms to `EBU 3285 v2`_, it will hold precomputed program loudness values
|
||||||
|
as described by `EBU Rec 128`_.
|
||||||
|
|
||||||
|
.. _EBU 3285 v1: https://tech.ebu.ch/publications/tech3285s1
|
||||||
|
.. _SMPTE ST 330 UMID: https://standards.globalspec.com/std/1396751/smpte-st-330
|
||||||
|
.. _EBU 3285 v2: https://tech.ebu.ch/publications/tech3285s2
|
||||||
|
.. _EBU Rec 128: https://tech.ebu.ch/publications/r128
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
print(info.bext.description)
|
||||||
|
print("----------")
|
||||||
|
print("Originator:", info.bext.originator)
|
||||||
|
print("Originator Ref:", info.bext.originator_ref)
|
||||||
|
print("Originator Date:", info.bext.originator_date)
|
||||||
|
print("Originator Time:", info.bext.originator_time)
|
||||||
|
print("Time Reference:", info.bext.time_reference)
|
||||||
|
print(info.bext.coding_history)
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
sSPEED=023.976-ND
|
||||||
|
sTAKE=1
|
||||||
|
sUBITS=$12311801
|
||||||
|
sSWVER=2.67
|
||||||
|
sPROJECT=BMH
|
||||||
|
sSCENE=A101
|
||||||
|
sFILENAME=A101_1.WAV
|
||||||
|
sTAPE=18Y12M31
|
||||||
|
sTRK1=MKH516 A
|
||||||
|
sTRK2=Boom
|
||||||
|
sNOTE=
|
||||||
|
|
||||||
|
----------
|
||||||
|
Originator: Sound Dev: 702T S#GR1112089007
|
||||||
|
Originator Ref: USSDVGR1112089007124001008206301
|
||||||
|
Originator Date: 2018-12-31
|
||||||
|
Originator Time: 12:40:00
|
||||||
|
Time Reference: 2190940753
|
||||||
|
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
||||||
|
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
21
docs/source/scopes/dolby.rst
Normal file
21
docs/source/scopes/dolby.rst
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Dolby Metadata
|
||||||
|
==============
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
Dolby software and equipment creates detailed hinting metadata that can help
|
||||||
|
receiving applications decide how to present the audio content, particularly
|
||||||
|
how it should be downmixed, and dialogue normalization settings.
|
||||||
|
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: wavinfo.wave_dbmd_reader
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_dbmd_reader.WavDolbyMetadataReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_dbmd_reader.DolbyDigitalPlusMetadata
|
||||||
|
:members:
|
||||||
42
docs/source/scopes/info.rst
Normal file
42
docs/source/scopes/info.rst
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
INFO Metadata
|
||||||
|
=============
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
INFO Metadata is a standard method for saving tagged text data in a WAV or AVI
|
||||||
|
file. INFO fields are often read by the file explorer and host OS, and used in
|
||||||
|
music library software.
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
bullet_path = '../tests/test_files/BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'
|
||||||
|
|
||||||
|
bullet = WavInfoReader(bullet_path)
|
||||||
|
|
||||||
|
print("INFO Artist:", bullet.info.artist)
|
||||||
|
print("INFO Copyright:", bullet.info.copyright)
|
||||||
|
print("INFO Comment:", bullet.info.comment)
|
||||||
|
|
||||||
|
|
||||||
|
On Encodings
|
||||||
|
""""""""""""
|
||||||
|
According to Microsoft, the original developers of the RIFF file and RIFF INFO
|
||||||
|
metadata, these fields are always to be interpreted as ISO Latin 1 characters,
|
||||||
|
and this is the default encoding used by `wavinfo` for these fields. You can
|
||||||
|
select a different encoding (like Shift-JIS) by passing an encoding name (as
|
||||||
|
would be used by `string.encode()`) to `WavInfoReader.__init__()`'s
|
||||||
|
`info_encoding=` parameter.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
43
docs/source/scopes/ixml.rst
Normal file
43
docs/source/scopes/ixml.rst
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
iXML Production Recorder Metadata
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
iXML allows an XML document to be embedded in a WAV file.
|
||||||
|
|
||||||
|
The iXML website recommends a schema for recorder information but
|
||||||
|
there is no official DTD and vendors mostly do their own thing, apart from
|
||||||
|
hitting a few key xpaths. iXML is used by most location/production recorders
|
||||||
|
to save slating information, timecode and sync points in a reliable way.
|
||||||
|
|
||||||
|
iXML is also used to link "families" of WAV files together, so WAV files
|
||||||
|
recorded simultaneously or contiguously can be related by a receiving client.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
print("iXML Project:", info.ixml.project)
|
||||||
|
print("iXML Scene:", info.ixml.scene)
|
||||||
|
print("iXML Take:", info.ixml.take)
|
||||||
|
print("iXML Tape:", info.ixml.tape)
|
||||||
|
print("iXML File Family Name:", info.ixml.family_name)
|
||||||
|
print("iXML File Family UID:", info.ixml.family_uid)
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
iXML Project: BMH
|
||||||
|
iXML Scene: A101
|
||||||
|
iXML Take: 1
|
||||||
|
iXML Tape: 18Y12M31
|
||||||
|
iXML File Family Name: None
|
||||||
|
iXML File Family UID: USSDVGR1112089007124001008206300
|
||||||
|
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 2,
|
"execution_count": 1,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"from wavinfo import WavInfoReader\n",
|
"from wavinfo import WavInfoReader\n",
|
||||||
"\n",
|
"\n",
|
||||||
"path = '../tests/test_files/A101_1.WAV'\n",
|
"path = '../tests/test_files/sounddevices/A101_1.WAV'\n",
|
||||||
"\n",
|
"\n",
|
||||||
"info = WavInfoReader(path)"
|
"info = WavInfoReader(path)"
|
||||||
]
|
]
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 4,
|
"execution_count": 2,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"(240239, 1441434)"
|
"(240239, 1441434)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 4,
|
"execution_count": 2,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 3,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
"(48000, 2, 6, 24)"
|
"(48000, 2, 6, 24)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 7,
|
"execution_count": 3,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 13,
|
"execution_count": 4,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 14,
|
"execution_count": 5,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -142,9 +142,7 @@
|
|||||||
"iXML Take: 1\n",
|
"iXML Take: 1\n",
|
||||||
"iXML Tape: 18Y12M31\n",
|
"iXML Tape: 18Y12M31\n",
|
||||||
"iXML File Family Name: None\n",
|
"iXML File Family Name: None\n",
|
||||||
"iXML File Family UID: USSDVGR1112089007124001008206300\n",
|
"iXML File Family UID: USSDVGR1112089007124001008206300\n"
|
||||||
"A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
|
|
||||||
"\n"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -157,6 +155,13 @@
|
|||||||
"print(\"iXML File Family UID:\", info.ixml.family_uid)"
|
"print(\"iXML File Family UID:\", info.ixml.family_uid)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
|
|||||||
@@ -25,86 +25,17 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"testfile_path = \"../tests/test_files/\"\n",
|
"path = '../tests/test_files/protools/PT A101_4.A1.wav'\n",
|
||||||
"sound_devices_file = testfile_path + \"A101_1.WAV\"\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"info = wavinfo.WavInfoReader(sound_devices_file)"
|
"info = wavinfo.WavInfoReader(path)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 4,
|
"execution_count": 4,
|
||||||
"metadata": {},
|
"metadata": {
|
||||||
"outputs": [
|
"scrolled": true
|
||||||
{
|
},
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"WavInfoFormat(audio_format=1, channel_count=2, sample_rate=48000, byte_rate=288000, block_align=6, bits_per_sample=24)\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"pp.pprint(info.fmt)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 5,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "stdout",
|
|
||||||
"output_type": "stream",
|
|
||||||
"text": [
|
|
||||||
"WavBextFormat(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', originator='Sound Dev: 702T S#GR1112089007', originator_ref='USSDVGR1112089007124001008206301', originator_date='2018-12-31', originator_time='12:40:00', time_reference=2190940753, version=1, umid=b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', loudness_value=0.0, loudness_range=0.0, max_true_peak=0.0, max_momentary_loudness=0.0, max_shortterm_loudness=0.0, coding_history='A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\\r\\n')\n"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"pp.pprint(info.bext)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 6,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"('BMH', 'A101', '1', 240239)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 6,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"info.ixml.project, info.ixml.scene, info.ixml.take, info.data.frame_count"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 7,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"pro_tools_file = testfile_path + \"PT A101_4.A1.wav\""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 8,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
@@ -123,7 +54,7 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"import wavinfo.wave_parser\n",
|
"import wavinfo.wave_parser\n",
|
||||||
"\n",
|
"\n",
|
||||||
"with open(pro_tools_file,'rb') as f:\n",
|
"with open(path,'rb') as f:\n",
|
||||||
" chunk_tree = wavinfo.wave_parser.parse_chunk(f)\n",
|
" chunk_tree = wavinfo.wave_parser.parse_chunk(f)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"pp.pprint(chunk_tree.children)"
|
"pp.pprint(chunk_tree.children)"
|
||||||
@@ -131,75 +62,127 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 9,
|
"execution_count": 5,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"text": [
|
||||||
"WavBextFormat(description='dUBITS=12311804\\r\\ndSCENE=A101\\r\\ndTAKE=4\\r\\ndTAPE=18Y12M31\\r\\ndFRAMERATE=23.976ND\\r\\ndSPEED=023.976-NDF\\r\\ndTRK1=MKH516 A\\r\\ndTRK2=Boom\\r\\n', originator='Sound Dev: 702T S#GR1112089007', originator_ref='aa4CKtcd13Vk', originator_date='2018-12-31', originator_time='12:40:07', time_reference=2191709524, version=0, umid=b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', loudness_value=0.0, loudness_range=0.0, max_true_peak=0.0, max_momentary_loudness=0.0, max_shortterm_loudness=0.0, coding_history='A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\\r\\n')\n"
|
"b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00*\\xfd\\xf5\\x0c$\\xe4s\\x80\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'\n",
|
||||||
|
"000000000000002afdf50c24e47380000000000000000000\n",
|
||||||
|
"24\n"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"ptinfo = wavinfo.WavInfoReader(pro_tools_file)\n",
|
"with open(path,'rb') as f:\n",
|
||||||
"\n",
|
" f.seek( chunk_tree.children[4].start )\n",
|
||||||
"print(ptinfo.bext)"
|
" umid_bin = f.read(chunk_tree.children[4].length)\n",
|
||||||
|
" f.seek( chunk_tree.children[6].start )\n",
|
||||||
|
" regn_bin = f.read(chunk_tree.children[6].length)\n",
|
||||||
|
" \n",
|
||||||
|
"print(umid_bin)\n",
|
||||||
|
"print(umid_bin.hex())\n",
|
||||||
|
"print(len(umid_bin))"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 10,
|
"execution_count": 6,
|
||||||
"metadata": {},
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"text/plain": [
|
|
||||||
"'<BWFXML><IXML_VERSION>1.61</IXML_VERSION><STEINBERG><ATTR_LIST><ATTR><TYPE>string</TYPE><NAME>MediaLibrary</NAME><VALUE>The Recordist Christmas 2018</VALUE></ATTR><ATTR><TYPE>string</TYPE><NAME>MediaCategoryPost</NAME><VALUE>Bullets</VALUE></ATTR><ATTR><TYPE>string</TYPE><NAME>MediaLibraryManufacturerName</NAME><VALUE>Creative Sound Design, LLC</VALUE></ATTR><ATTR><TYPE>string</TYPE><NAME>AudioSoundEditor</NAME><VALUE>Frank Bry</VALUE></ATTR><ATTR><TYPE>string</TYPE><NAME>MediaComment</NAME><VALUE>BULLET Impact Plastic LCD TV Screen Shatter Debris 2x</VALUE></ATTR><ATTR><TYPE>string</TYPE><NAME>MusicalCategory</NAME><VALUE>Bullets</VALUE></ATTR></ATTR_LIST></STEINBERG></BWFXML>'"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"execution_count": 10,
|
|
||||||
"metadata": {},
|
|
||||||
"output_type": "execute_result"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": [
|
|
||||||
"library_sound = testfile_path + 'BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'\n",
|
|
||||||
"\n",
|
|
||||||
"recinfo = wavinfo.WavInfoReader(library_sound)\n",
|
|
||||||
"\n",
|
|
||||||
"recinfo.ixml.source"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": 11,
|
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"text": [
|
||||||
"[ ChunkDescriptor(ident=b'fmt ', start=20, length=40),\n",
|
"<wavinfo.wave_bext_reader.WavBextReader object at 0x10d5f8ac8>\n"
|
||||||
" ChunkDescriptor(ident=b'bext', start=68, length=604),\n",
|
|
||||||
" ChunkDescriptor(ident=b'data', start=680, length=2833404),\n",
|
|
||||||
" ChunkDescriptor(ident=b'ID3 ', start=2834092, length=2048),\n",
|
|
||||||
" ChunkDescriptor(ident=b'SMED', start=2836148, length=5468),\n",
|
|
||||||
" ListChunkDescriptor(signature=b'INFO', children=[ChunkDescriptor(ident=b'IPRD', start=2841636, length=30), ChunkDescriptor(ident=b'IGNR', start=2841674, length=8), ChunkDescriptor(ident=b'IART', start=2841690, length=10), ChunkDescriptor(ident=b'ICMT', start=2841708, length=54), ChunkDescriptor(ident=b'ICOP', start=2841770, length=84), ChunkDescriptor(ident=b'ISFT', start=2841862, length=12), ChunkDescriptor(ident=b'ICRD', start=2841882, length=12)]),\n",
|
|
||||||
" ChunkDescriptor(ident=b'iXML', start=2841902, length=686),\n",
|
|
||||||
" ChunkDescriptor(ident=b'umid', start=2842596, length=24),\n",
|
|
||||||
" ChunkDescriptor(ident=b'_PMX', start=2842628, length=3560)]\n"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"with open(library_sound,'rb') as f:\n",
|
"print(info.bext)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00*\\xfd\\xf5\\x0c$\\xe4s\\x80\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c3\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00T\\xd5\\xa2\\x82\\x00\\x00\\x00\\x00\\x10PT A101_4.A1.wavGK\\xaa\\xaf\\x7f\\x00\\x00@ }\\x06\\x00`\\x00\\x00'\n",
|
||||||
|
"01000000000000000000002afdf50c24e473800000000000000000000c330200000000000000000000000000000000000000000054d5a2820000000010505420413130315f342e41312e776176474baaaf7f000040207d0600600000\n",
|
||||||
|
"92\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"print(regn_bin)\n",
|
||||||
|
"print(regn_bin.hex())\n",
|
||||||
|
"print(len(regn_bin))"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"{ 'artist': 'Frank Bry',\n",
|
||||||
|
" 'comment': 'BULLET Impact Plastic LCD TV Screen Shatter Debris 2x',\n",
|
||||||
|
" 'copyright': '2018 Creative Sound Design, LLC (The Recordist Christmas '\n",
|
||||||
|
" '2018) www.therecordist.com',\n",
|
||||||
|
" 'created_date': '2018-11-15',\n",
|
||||||
|
" 'engineer': None,\n",
|
||||||
|
" 'genre': 'Bullets',\n",
|
||||||
|
" 'keywords': None,\n",
|
||||||
|
" 'product': 'The Recordist Christmas 2018',\n",
|
||||||
|
" 'software': 'Soundminer',\n",
|
||||||
|
" 'source': None,\n",
|
||||||
|
" 'tape': None,\n",
|
||||||
|
" 'title': None}\n",
|
||||||
|
"{ 'coding_history': '',\n",
|
||||||
|
" 'description': 'BULLET Impact Plastic LCD TV Screen Shatter Debris 2x',\n",
|
||||||
|
" 'loudness_range': None,\n",
|
||||||
|
" 'loudness_value': None,\n",
|
||||||
|
" 'max_momentary_loudness': None,\n",
|
||||||
|
" 'max_shortterm_loudness': None,\n",
|
||||||
|
" 'max_true_peak': None,\n",
|
||||||
|
" 'originator': 'TheRecordist',\n",
|
||||||
|
" 'originator_date': '2018-12-20',\n",
|
||||||
|
" 'originator_ref': 'aaiAKt3fCGTk',\n",
|
||||||
|
" 'originator_time': '12:15:37',\n",
|
||||||
|
" 'time_reference': 57882,\n",
|
||||||
|
" 'version': 0}\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"path = '../tests/test_files/BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'\n",
|
||||||
|
"\n",
|
||||||
|
"info = wavinfo.WavInfoReader(path)\n",
|
||||||
|
"\n",
|
||||||
|
"with open(path,'rb') as f:\n",
|
||||||
" chunk_tree = wavinfo.wave_parser.parse_chunk(f)\n",
|
" chunk_tree = wavinfo.wave_parser.parse_chunk(f)\n",
|
||||||
"\n",
|
" \n",
|
||||||
"pp.pprint(chunk_tree.children)"
|
"pp.pprint(info.info.to_dict())\n",
|
||||||
|
"pp.pprint(info.bext.to_dict())"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
|
|||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lxml==4.9.1
|
||||||
34
setup.py
34
setup.py
@@ -1,18 +1,40 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
from wavinfo import __author__, __license__, __version__
|
||||||
|
|
||||||
with open("README.md", "r") as fh:
|
with open("README.md", "r") as fh:
|
||||||
long_description = fh.read()
|
long_description = fh.read()
|
||||||
|
|
||||||
setup(name='wavinfo',
|
setup(name='wavinfo',
|
||||||
version='0.1',
|
version=__version__,
|
||||||
author='Jamie Hardt',
|
author=__author__,
|
||||||
author_email='jamiehardt@me.com',
|
author_email='jamiehardt@me.com',
|
||||||
description='WAVE sound file metadata parser.',
|
description='Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.',
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
|
license=__license__,
|
||||||
url='https://github.com/iluvcapra/wavinfo',
|
url='https://github.com/iluvcapra/wavinfo',
|
||||||
classifiers=['Development Status :: 2 - Pre-Alpha',
|
project_urls={
|
||||||
|
'Source':
|
||||||
|
'https://github.com/iluvcapra/wavinfo',
|
||||||
|
'Documentation':
|
||||||
|
'https://wavinfo.readthedocs.io/',
|
||||||
|
'Issues':
|
||||||
|
'https://github.com/iluvcapra/wavinfo/issues',
|
||||||
|
},
|
||||||
|
packages=['wavinfo'],
|
||||||
|
classifiers=['Development Status :: 5 - Production/Stable',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Topic :: Multimedia',
|
'Topic :: Multimedia',
|
||||||
'Topic :: Multimedia :: Sound/Audio'],
|
'Topic :: Multimedia :: Sound/Audio',
|
||||||
packages=['wavinfo'])
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11"],
|
||||||
|
keywords='waveform metadata audio ebu smpte avi library film tv editing editorial',
|
||||||
|
install_requires=['lxml'],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'wavinfo = wavinfo.__main__:main'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from . import test_wave_parsing
|
from . import test_wave_parsing
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
tests/test_adm.py
Normal file
51
tests/test_adm.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
class TestADMWave(TestCase):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.protools_adm_wav = "tests/test_files/protools/Test_ADM_ProTools.wav"
|
||||||
|
return super().setUp()
|
||||||
|
|
||||||
|
def test_chna(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
|
||||||
|
adm = info.adm
|
||||||
|
self.assertIsNotNone(adm)
|
||||||
|
|
||||||
|
self.assertEqual(len(adm.channel_uids), 14)
|
||||||
|
|
||||||
|
def test_to_dict(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
adm = info.adm
|
||||||
|
dict = adm.to_dict()
|
||||||
|
self.assertIsNotNone(dict)
|
||||||
|
|
||||||
|
def test_programme(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
adm = info.adm
|
||||||
|
pdict = adm.programme()
|
||||||
|
self.assertIn("programme_id", pdict.keys())
|
||||||
|
self.assertIn("programme_name", pdict.keys())
|
||||||
|
self.assertEqual(pdict['programme_id'], 'APR_1001')
|
||||||
|
self.assertEqual(pdict['programme_name'], 'Atmos_Master')
|
||||||
|
self.assertIn("contents", pdict.keys())
|
||||||
|
self.assertEqual(len(pdict["contents"]), 3)
|
||||||
|
|
||||||
|
def test_track_info(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
adm = info.adm
|
||||||
|
|
||||||
|
t1 = adm.track_info(0)
|
||||||
|
self.assertTrue("channel_format_name" in t1.keys())
|
||||||
|
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
|
||||||
|
|
||||||
|
self.assertTrue("pack_format_name" in t1.keys())
|
||||||
|
self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"])
|
||||||
|
|
||||||
|
t10 = adm.track_info(10)
|
||||||
|
self.assertTrue("content_name" in t10.keys())
|
||||||
|
self.assertEqual("Dialog", t10["content_name"])
|
||||||
|
|
||||||
49
tests/test_dolby.py
Normal file
49
tests/test_dolby.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
from wavinfo.wave_dbmd_reader import SegmentType, DolbyAtmosMetadata, DolbyDigitalPlusMetadata
|
||||||
|
|
||||||
|
class TestDolby(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.test_file = "tests/test_files/protools/Test_ADM_ProTools.wav"
|
||||||
|
|
||||||
|
def test_version(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
self.assertEqual((1,0,0,6), d.version)
|
||||||
|
|
||||||
|
def test_segments(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
ddp = [x for x in d.segment_list 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(atmos), 1)
|
||||||
|
|
||||||
|
def test_checksums(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
for seg in d.segment_list:
|
||||||
|
self.assertTrue(seg[1])
|
||||||
|
|
||||||
|
def test_ddp(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
ddp = d.dolby_digital_plus()
|
||||||
|
self.assertEqual(len(ddp), 1, "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)
|
||||||
|
|
||||||
|
def test_atmos(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
atmos = d.dolby_atmos()
|
||||||
|
self.assertEqual(len(atmos), 1, "Failed to find exactly one Atmos metadata segment")
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
SOUND REPORT
|
SOUND REPORT
|
||||||
|
Can't render this file because it contains an unexpected character in line 1 and column 53.
|
BIN
tests/test_files/metacorder/Sr001-001-06_01.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-001-06_01.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-001-06_02.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-001-06_02.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-001-06_03.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-001-06_03.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-001-06_04.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-001-06_04.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-001-06_05.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-001-06_05.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-2C-01_01.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-2C-01_01.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-2C-01_02.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-2C-01_02.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-2C-01_03.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-2C-01_03.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-2C-01_04.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-2C-01_04.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/metacorder/Sr001-2C-01_05.WAV
Normal file
BIN
tests/test_files/metacorder/Sr001-2C-01_05.WAV
Normal file
Binary file not shown.
BIN
tests/test_files/problems/DinerAmbience VAL085101-glued.wav
Normal file
BIN
tests/test_files/problems/DinerAmbience VAL085101-glued.wav
Normal file
Binary file not shown.
BIN
tests/test_files/protools/Test_ADM_ProTools.wav
Normal file
BIN
tests/test_files/protools/Test_ADM_ProTools.wav
Normal file
Binary file not shown.
BIN
tests/test_files/protools/umid.wav
Normal file
BIN
tests/test_files/protools/umid.wav
Normal file
Binary file not shown.
BIN
tests/test_files/pyramix/Testfile_PYR_BWF.wav
Normal file
BIN
tests/test_files/pyramix/Testfile_PYR_BWF.wav
Normal file
Binary file not shown.
BIN
tests/test_files/rf64/Pro_Tools_2019_RF64.wav.gz
Normal file
BIN
tests/test_files/rf64/Pro_Tools_2019_RF64.wav.gz
Normal file
Binary file not shown.
BIN
tests/test_files/rf64/Testfile_SEQ_RF64_Big.wav.gz
Executable file
BIN
tests/test_files/rf64/Testfile_SEQ_RF64_Big.wav.gz
Executable file
Binary file not shown.
BIN
tests/test_files/sound_grinder_pro/new_camera bumb 1.wav
Normal file
BIN
tests/test_files/sound_grinder_pro/new_camera bumb 1.wav
Normal file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr1.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr1.WAV
Executable file
Binary file not shown.
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr2.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr2.WAV
Executable file
Binary file not shown.
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr3.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr3.WAV
Executable file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr4.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr4.WAV
Executable file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr7.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr7.WAV
Executable file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr8.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr8.WAV
Executable file
Binary file not shown.
Binary file not shown.
BIN
tests/test_files/zoom_F8/MULTI/Scene_1_2AG-T001.WAV
Executable file
BIN
tests/test_files/zoom_F8/MULTI/Scene_1_2AG-T001.WAV
Executable file
Binary file not shown.
25
tests/test_walk.py
Normal file
25
tests/test_walk.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import unittest
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
|
||||||
|
class TestWalk(unittest.TestCase):
|
||||||
|
def test_walk_metadata(self):
|
||||||
|
test_file = 'tests/test_files/protools/PT A101_4.A1.wav'
|
||||||
|
info = wavinfo.WavInfoReader(test_file)
|
||||||
|
|
||||||
|
tested_data , tested_format = False, False
|
||||||
|
for scope, key, value in info.walk():
|
||||||
|
if scope == 'fmt':
|
||||||
|
if key == 'channel_count':
|
||||||
|
tested_format = True
|
||||||
|
self.assertEqual(value, 2)
|
||||||
|
if scope == 'data':
|
||||||
|
if key == 'frame_count':
|
||||||
|
tested_data = True
|
||||||
|
self.assertEqual(value, 144140)
|
||||||
|
|
||||||
|
self.assertTrue(tested_data and tested_format)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -1,99 +1,113 @@
|
|||||||
import os.path
|
import os.path
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from .utils import all_files, ffprobe
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
FFPROBE='/usr/local/bin/ffprobe'
|
|
||||||
|
|
||||||
|
|
||||||
def ffprobe(path):
|
|
||||||
|
|
||||||
arguments = [ FFPROBE , "-of", "json" , "-show_format", "-show_streams", path ]
|
|
||||||
|
|
||||||
process = subprocess.run(arguments, stdin=None, capture_output=True)
|
|
||||||
|
|
||||||
if process.returncode == 0:
|
|
||||||
return json.loads(process.stdout)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class TestWaveInfo(TestCase):
|
class TestWaveInfo(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def all_files(self):
|
|
||||||
for dirpath, dirnames, filenames in os.walk('tests/test_files'):
|
|
||||||
for filename in filenames:
|
|
||||||
name, ext = os.path.splitext(filename)
|
|
||||||
if ext == '.wav':
|
|
||||||
yield os.path.join(dirpath, filename)
|
|
||||||
|
|
||||||
|
|
||||||
def test_sanity(self):
|
def test_sanity(self):
|
||||||
for wav_file in self.all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
self.assertTrue(info is not None)
|
self.assertEqual(info.__repr__(), 'WavInfoReader({}, latin_1, ascii)'.format(os.path.abspath(wav_file)))
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
|
||||||
def test_fmt_against_ffprobe(self):
|
def test_fmt_against_ffprobe(self):
|
||||||
for wav_file in self.all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
ffprobe_info = ffprobe(wav_file)
|
ffprobe_info = ffprobe(wav_file)
|
||||||
|
|
||||||
self.assertEqual( info.fmt.channel_count , ffprobe_info['streams'][0]['channels'] )
|
self.assertEqual(info.fmt.channel_count, ffprobe_info['streams'][0]['channels'])
|
||||||
self.assertEqual( info.fmt.sample_rate , int(ffprobe_info['streams'][0]['sample_rate']) )
|
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_raw_sample']) )
|
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'))
|
||||||
byte_rate = int(ffprobe_info['streams'][0]['sample_rate']) \
|
streams = ffprobe_info['streams'][0]
|
||||||
* ffprobe_info['streams'][0]['channels'] \
|
byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_sample']) / 8
|
||||||
* int(ffprobe_info['streams'][0]['bits_per_raw_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):
|
||||||
for wav_file in self.all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
ffprobe_info = ffprobe(wav_file)
|
ffprobe_info = ffprobe(wav_file)
|
||||||
|
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 self.all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
ffprobe_info = ffprobe(wav_file)
|
ffprobe_info = ffprobe(wav_file)
|
||||||
|
if info.bext:
|
||||||
|
if 'comment' in ffprobe_info['format']['tags']:
|
||||||
|
self.assertEqual(info.bext.description, ffprobe_info['format']['tags']['comment'])
|
||||||
|
else:
|
||||||
|
self.assertEqual(info.bext.description, '')
|
||||||
|
|
||||||
self.assertEqual( info.bext.description, ffprobe_info['format']['tags']['comment'] )
|
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'])
|
||||||
self.assertEqual( info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'] )
|
else:
|
||||||
|
self.assertEqual(info.bext.originator, '')
|
||||||
|
|
||||||
# these don't always reflect the bext info
|
if 'originator_reference' in ffprobe_info['format']['tags']:
|
||||||
#self.assertEqual( info.bext.originator_date, ffprobe_info['format']['tags']['date'] )
|
self.assertEqual(info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'])
|
||||||
#self.assertEqual( info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'] )
|
else:
|
||||||
self.assertEqual( info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']) )
|
self.assertEqual(info.bext.originator_ref, '')
|
||||||
self.assertEqual( info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'] )
|
|
||||||
|
# these don't always reflect the bext info
|
||||||
|
# self.assertEqual(info.bext.originator_date, ffprobe_info['format']['tags']['date'])
|
||||||
|
# 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']:
|
||||||
|
self.assertEqual(info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'])
|
||||||
|
else:
|
||||||
|
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', 'scene': 'A101', 'take': '4',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
|
||||||
'A101_3.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '3',
|
'A101_3.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '3',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
|
||||||
'A101_2.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '2',
|
'A101_2.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '2',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
|
||||||
'A101_1.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '1',
|
'A101_1.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '1',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
|
||||||
}
|
}
|
||||||
|
|
||||||
for wav_file in self.all_files():
|
for wav_file in all_files():
|
||||||
basename = os.path.basename(wav_file)
|
basename = os.path.basename(wav_file)
|
||||||
if basename in expected:
|
if basename in expected:
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
e = expected[basename]
|
e = expected[basename]
|
||||||
|
|
||||||
self.assertEqual( e['project'], info.ixml.project )
|
self.assertEqual(e['project'], info.ixml.project)
|
||||||
self.assertEqual( e['scene'], info.ixml.scene )
|
self.assertEqual(e['scene'], info.ixml.scene)
|
||||||
self.assertEqual( e['take'], info.ixml.take )
|
self.assertEqual(e['take'], info.ixml.take)
|
||||||
self.assertEqual( e['tape'], info.ixml.tape )
|
self.assertEqual(e['tape'], info.ixml.tape)
|
||||||
self.assertEqual( e['family_uid'], info.ixml.family_uid )
|
self.assertEqual(e['family_uid'], info.ixml.family_uid)
|
||||||
|
|
||||||
|
for track in info.ixml.track_list:
|
||||||
|
self.assertIsNotNone(track.channel_index)
|
||||||
|
if basename == 'A101_4.WAV' and track.channel_index == '1':
|
||||||
|
self.assertEqual(track.name, 'MKH516 A')
|
||||||
|
|
||||||
|
def test_metadata(self):
|
||||||
|
file_with_metadata = 'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
||||||
|
self.assertTrue(os.path.exists(file_with_metadata))
|
||||||
|
info = wavinfo.WavInfoReader(file_with_metadata).info
|
||||||
|
self.assertEqual(info.title, 'camera bumb 1')
|
||||||
|
self.assertEqual(info.artist, 'Jamie Hardt')
|
||||||
|
self.assertEqual(info.copyright, '© 2010 Jamie Hardt')
|
||||||
|
self.assertEqual(info.product, 'Test Sounds') # album
|
||||||
|
self.assertEqual(info.album, info.product)
|
||||||
|
self.assertEqual(info.comment, 'Comments')
|
||||||
|
self.assertEqual(info.software, 'Sound Grinder Pro')
|
||||||
|
self.assertEqual(info.created_date, '2010-12-28')
|
||||||
|
self.assertEqual(info.engineer, 'JPH')
|
||||||
|
self.assertEqual(info.keywords, 'Sound Effect, movement, microphone, bump')
|
||||||
|
self.assertEqual(info.title, 'camera bumb 1')
|
||||||
|
self.assertEqual(type(info.to_dict()), dict)
|
||||||
|
self.assertEqual(type(info.__repr__()), str)
|
||||||
|
|
||||||
|
|||||||
13
tests/test_zoom_f8.py
Normal file
13
tests/test_zoom_f8.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import os.path
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from subprocess import PIPE
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
|
||||||
|
class TestZoomF8(TestCase):
|
||||||
|
pass
|
||||||
36
tests/utils.py
Normal file
36
tests/utils.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import os.path
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from subprocess import PIPE
|
||||||
|
import json
|
||||||
|
|
||||||
|
FFPROBE = 'ffprobe'
|
||||||
|
|
||||||
|
|
||||||
|
def ffprobe(path):
|
||||||
|
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
|
||||||
|
if int(sys.version[0]) < 3:
|
||||||
|
process = subprocess.Popen(arguments, stdout=PIPE)
|
||||||
|
process.wait()
|
||||||
|
if process.returncode == 0:
|
||||||
|
output = process.communicate()[0]
|
||||||
|
if output:
|
||||||
|
output_str = output.decode('utf-8')
|
||||||
|
return json.loads(output_str)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE)
|
||||||
|
if process.returncode == 0:
|
||||||
|
output_str = process.stdout.decode('utf-8')
|
||||||
|
return json.loads(output_str)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def all_files():
|
||||||
|
for dirpath, _, filenames in os.walk('tests/test_files'):
|
||||||
|
for filename in filenames:
|
||||||
|
_, ext = os.path.splitext(filename)
|
||||||
|
if ext in ['.wav', '.WAV']:
|
||||||
|
yield os.path.join(dirpath, filename)
|
||||||
@@ -1 +1,12 @@
|
|||||||
|
"""
|
||||||
|
methods to probe a WAV file for various kinds of production metadata.
|
||||||
|
|
||||||
|
See the documentation for `wavinfo.WavInfoReader` for more information.
|
||||||
|
"""
|
||||||
|
|
||||||
from .wave_reader import WavInfoReader
|
from .wave_reader import WavInfoReader
|
||||||
|
from .riff_parser import WavInfoEOFError
|
||||||
|
|
||||||
|
__version__ = '2.1.0'
|
||||||
|
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
|
||||||
|
__license__ = "MIT"
|
||||||
|
|||||||
70
wavinfo/__main__.py
Normal file
70
wavinfo/__main__.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from optparse import OptionParser, OptionGroup
|
||||||
|
import datetime
|
||||||
|
from . import WavInfoReader
|
||||||
|
from . import __version__
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class MyJSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, Enum):
|
||||||
|
return o._name_
|
||||||
|
else:
|
||||||
|
return super().default(o)
|
||||||
|
|
||||||
|
class MissingDataError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = OptionParser()
|
||||||
|
|
||||||
|
parser.usage = 'wavinfo (--adm | --ixml) [FILES]'
|
||||||
|
|
||||||
|
# parser.add_option('-f', dest='output_format', help='Set the output format',
|
||||||
|
# default='json',
|
||||||
|
# metavar='FORMAT')
|
||||||
|
|
||||||
|
parser.add_option('--adm', dest='adm', help='Output ADM XML',
|
||||||
|
default=False, action='store_true')
|
||||||
|
|
||||||
|
parser.add_option('--ixml', dest='ixml', help='Output iXML',
|
||||||
|
default=False, action='store_true')
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args(sys.argv)
|
||||||
|
for arg in args[1:]:
|
||||||
|
try:
|
||||||
|
this_file = WavInfoReader(path=arg)
|
||||||
|
if options.adm:
|
||||||
|
if this_file.adm:
|
||||||
|
sys.stdout.write(this_file.adm.xml_str())
|
||||||
|
else:
|
||||||
|
raise MissingDataError("adm")
|
||||||
|
elif options.ixml:
|
||||||
|
if this_file.ixml:
|
||||||
|
sys.stdout.write(this_file.ixml.xml_bytes())
|
||||||
|
else:
|
||||||
|
raise MissingDataError("ixml")
|
||||||
|
else:
|
||||||
|
ret_dict = {
|
||||||
|
'filename': arg,
|
||||||
|
'run_date': datetime.datetime.now().isoformat() ,
|
||||||
|
'application': "wavinfo " + __version__,
|
||||||
|
'scopes': {}
|
||||||
|
}
|
||||||
|
for scope, name, value in this_file.walk():
|
||||||
|
if scope not in ret_dict['scopes'].keys():
|
||||||
|
ret_dict['scopes'][scope] = {}
|
||||||
|
|
||||||
|
ret_dict['scopes'][scope][name] = value
|
||||||
|
|
||||||
|
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
|
||||||
|
except MissingDataError as e:
|
||||||
|
print("MissingDataError: Missing metadata (%s) in file %s" % (e, arg), file=sys.stderr)
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
40
wavinfo/rf64_parser.py
Normal file
40
wavinfo/rf64_parser.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import struct
|
||||||
|
from collections import namedtuple
|
||||||
|
from . import riff_parser
|
||||||
|
|
||||||
|
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rf64(stream, signature = b'RF64'):
|
||||||
|
# print("starting parse_rf64")
|
||||||
|
start = stream.tell()
|
||||||
|
assert( stream.read(4) == b'WAVE' )
|
||||||
|
|
||||||
|
ds64_chunk = riff_parser.parse_chunk(stream)
|
||||||
|
|
||||||
|
ds64_field_spec = "<QQQI"
|
||||||
|
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
||||||
|
assert(ds64_chunk.ident == b'ds64')
|
||||||
|
|
||||||
|
ds64_data = ds64_chunk.read_data(stream)
|
||||||
|
assert(len(ds64_data) >= ds64_fields_size )
|
||||||
|
|
||||||
|
# print("Read ds64 chunk: len()",len(ds64_data))
|
||||||
|
riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec , ds64_data[0:ds64_fields_size] )
|
||||||
|
|
||||||
|
bigchunk_table = {}
|
||||||
|
chunksize64format = "<4sL"
|
||||||
|
chunksize64size = struct.calcsize(chunksize64format)
|
||||||
|
# print("Found chunks64s:", length_lookup_table)
|
||||||
|
|
||||||
|
for n in range(length_lookup_table):
|
||||||
|
bigname, bigsize = struct.unpack_from( chunksize64format , ds64_data, offset= ds64_fields_size )
|
||||||
|
bigchunk_table[bigname] = bigsize
|
||||||
|
|
||||||
|
bigchunk_table[b'data'] = data_size
|
||||||
|
bigchunk_table[signature] = riff_size
|
||||||
|
|
||||||
|
stream.seek(start, 0)
|
||||||
|
# print("returning from parse_rf64, context: ", RF64Context(sample_count=sample_count, bigchunk_table=bigchunk_table))
|
||||||
|
return RF64Context( sample_count=sample_count, bigchunk_table=bigchunk_table )
|
||||||
|
|
||||||
73
wavinfo/riff_parser.py
Normal file
73
wavinfo/riff_parser.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
import struct
|
||||||
|
from collections import namedtuple
|
||||||
|
from .rf64_parser import parse_rf64
|
||||||
|
|
||||||
|
|
||||||
|
class WavInfoEOFError(EOFError):
|
||||||
|
def __init__(self, identifier, chunk_start):
|
||||||
|
self.identifier = identifier
|
||||||
|
self.chunk_start = chunk_start
|
||||||
|
|
||||||
|
|
||||||
|
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
|
||||||
|
pass
|
||||||
|
# def find(self, chunk_path):
|
||||||
|
# if len(chunk_path) > 1:
|
||||||
|
# for chunk in self.children:
|
||||||
|
# if type(chunk) is ListChunkDescriptor and \
|
||||||
|
# chunk.signature is chunk_path[0]:
|
||||||
|
# return chunk.find(chunk_path[1:])
|
||||||
|
# else:
|
||||||
|
# for chunk in self.children:
|
||||||
|
# if type(chunk) is ChunkDescriptor and \
|
||||||
|
# chunk.ident is chunk_path[0]:
|
||||||
|
# return chunk
|
||||||
|
|
||||||
|
|
||||||
|
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
|
||||||
|
def read_data(self, from_stream):
|
||||||
|
from_stream.seek(self.start)
|
||||||
|
return from_stream.read(self.length)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_list_chunk(stream, length, rf64_context=None):
|
||||||
|
start = stream.tell()
|
||||||
|
signature = stream.read(4)
|
||||||
|
|
||||||
|
children = []
|
||||||
|
while stream.tell() - start + 8 < length:
|
||||||
|
child_chunk = parse_chunk(stream, rf64_context=rf64_context)
|
||||||
|
children.append(child_chunk)
|
||||||
|
|
||||||
|
stream.seek(start + length)
|
||||||
|
|
||||||
|
return ListChunkDescriptor(signature=signature, children=children)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_chunk(stream, rf64_context=None):
|
||||||
|
header_start = stream.tell()
|
||||||
|
ident = stream.read(4)
|
||||||
|
size_bytes = stream.read(4)
|
||||||
|
|
||||||
|
if len(ident) != 4 or len(size_bytes) != 4:
|
||||||
|
raise WavInfoEOFError(identifier=ident, chunk_start=header_start)
|
||||||
|
|
||||||
|
data_size = struct.unpack('<I', size_bytes)[0]
|
||||||
|
|
||||||
|
if data_size == 0xFFFFFFFF:
|
||||||
|
if rf64_context is None and ident in {b'RF64', b'BW64'}:
|
||||||
|
rf64_context = parse_rf64(stream=stream, signature=ident)
|
||||||
|
|
||||||
|
data_size = rf64_context.bigchunk_table[ident]
|
||||||
|
|
||||||
|
displacement = data_size
|
||||||
|
if displacement % 2:
|
||||||
|
displacement += 1
|
||||||
|
|
||||||
|
if ident in {b'RIFF', b'LIST', b'RF64', b'BW64'}:
|
||||||
|
return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
|
||||||
|
else:
|
||||||
|
data_start = stream.tell()
|
||||||
|
stream.seek(displacement, 1)
|
||||||
|
return ChunkDescriptor(ident=ident, start=data_start, length=data_size, rf64_context=rf64_context)
|
||||||
121
wavinfo/umid_parser.py
Normal file
121
wavinfo/umid_parser.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
|
def binary_to_string(binary_value):
|
||||||
|
return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
|
||||||
|
|
||||||
|
|
||||||
|
class UMIDParser:
|
||||||
|
"""
|
||||||
|
Parse a raw binary SMPTE 330M Universal Materials Identifier
|
||||||
|
|
||||||
|
This implementation is based on SMPTE ST 330:2011
|
||||||
|
"""
|
||||||
|
def __init__(self, raw_umid: bytes):
|
||||||
|
self.raw_umid = raw_umid
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def universal_label(self) -> bytearray:
|
||||||
|
# return self.raw_umid[0:12]
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def basic_umid(self):
|
||||||
|
# return self.raw_umid[0:32]
|
||||||
|
|
||||||
|
def basic_umid_to_str(self):
|
||||||
|
return binary_to_string(self.raw_umid[0:32])
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def universal_label_is_valid(self) -> bool:
|
||||||
|
# valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
|
||||||
|
# return self.universal_label[0:len(valid_preamble)] == valid_preamble
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def material_type(self) -> str:
|
||||||
|
# material_byte = self.raw_umid[10]
|
||||||
|
# if material_byte == 0x1:
|
||||||
|
# return 'picture'
|
||||||
|
# elif material_byte == 0x2:
|
||||||
|
# return 'audio'
|
||||||
|
# elif material_byte == 0x3:
|
||||||
|
# return 'data'
|
||||||
|
# elif material_byte == 0x4:
|
||||||
|
# return 'other'
|
||||||
|
# elif material_byte == 0x5:
|
||||||
|
# return 'picture_single_component'
|
||||||
|
# elif material_byte == 0x6:
|
||||||
|
# return 'picture_multiple_component'
|
||||||
|
# elif material_byte == 0x7:
|
||||||
|
# return 'audio_single_component'
|
||||||
|
# elif material_byte == 0x9:
|
||||||
|
# return 'audio_multiple_component'
|
||||||
|
# elif material_byte == 0xb:
|
||||||
|
# return 'auxiliary_single_component'
|
||||||
|
# elif material_byte == 0xc:
|
||||||
|
# return 'auxiliary_multiple_component'
|
||||||
|
# elif material_byte == 0xd:
|
||||||
|
# return 'mixed_components'
|
||||||
|
# elif material_byte == 0xf:
|
||||||
|
# return 'not_identified'
|
||||||
|
# else:
|
||||||
|
# return 'not_recognized'
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def material_number_creation_method(self) -> str:
|
||||||
|
# method_byte = self.raw_umid[11]
|
||||||
|
# method_byte = (method_byte << 4) & 0xf
|
||||||
|
# if method_byte == 0x0:
|
||||||
|
# return 'undefined'
|
||||||
|
# elif method_byte == 0x1:
|
||||||
|
# return 'smpte'
|
||||||
|
# elif method_byte == 0x2:
|
||||||
|
# return 'uuid'
|
||||||
|
# elif method_byte == 0x3:
|
||||||
|
# return 'masked'
|
||||||
|
# elif method_byte == 0x4:
|
||||||
|
# return 'ieee1394'
|
||||||
|
# elif 0x5 <= method_byte <= 0x7:
|
||||||
|
# return 'reserved_undefined'
|
||||||
|
# else:
|
||||||
|
# return 'unrecognized'
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def instance_number_creation_method(self) -> str:
|
||||||
|
# method_byte = self.raw_umid[11]
|
||||||
|
# method_byte = method_byte & 0xf
|
||||||
|
# if method_byte == 0x0:
|
||||||
|
# return 'undefined'
|
||||||
|
# elif method_byte == 0x01:
|
||||||
|
# return 'local_registration'
|
||||||
|
# elif method_byte == 0x02:
|
||||||
|
# return '24_bit_prs'
|
||||||
|
# elif method_byte == 0x03:
|
||||||
|
# return 'copy_number_and_16_bit_prs'
|
||||||
|
# elif 0x04 <= method_byte <= 0x0e:
|
||||||
|
# return 'reserved_undefined'
|
||||||
|
# elif method_byte == 0x0f:
|
||||||
|
# return 'live_stream'
|
||||||
|
# else:
|
||||||
|
# return 'unrecognized'
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def indicated_length(self) -> str:
|
||||||
|
# if self.raw_umid[12] == 0x13:
|
||||||
|
# return 'basic'
|
||||||
|
# elif self.raw_umid[12] == 0x33:
|
||||||
|
# return 'extended'
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def instance_number(self) -> bytearray:
|
||||||
|
# return self.raw_umid[13:3]
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def material_number(self) -> bytearray:
|
||||||
|
# return self.raw_umid[16:16]
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def source_pack(self) -> Union[bytearray, None]:
|
||||||
|
# if self.indicated_length == 'extended':
|
||||||
|
# return self.raw_umid[32:32]
|
||||||
|
# else:
|
||||||
|
# return None
|
||||||
165
wavinfo/wave_adm_reader.py
Normal file
165
wavinfo/wave_adm_reader.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
"""
|
||||||
|
ADM Reader
|
||||||
|
"""
|
||||||
|
|
||||||
|
from struct import unpack, unpack_from, calcsize
|
||||||
|
from io import BytesIO
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import Iterable, Tuple
|
||||||
|
|
||||||
|
from lxml import etree as ET
|
||||||
|
|
||||||
|
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
|
||||||
|
|
||||||
|
class WavADMReader:
|
||||||
|
"""
|
||||||
|
Reads XML data from an EBU ADM (Audio Definiton Model) WAV File.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, axml_data: bytes, chna_data: bytes):
|
||||||
|
header_fmt = "<HH"
|
||||||
|
uid_fmt = "<H12s14s11sx"
|
||||||
|
|
||||||
|
#: An :mod:`lxml.etree` of the ADM XML document
|
||||||
|
self.axml = ET.parse(BytesIO(axml_data))
|
||||||
|
|
||||||
|
_, 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 = []
|
||||||
|
|
||||||
|
offset = calcsize(header_fmt)
|
||||||
|
for _ in range(uid_count):
|
||||||
|
|
||||||
|
track_index, uid, track_ref, pack_ref = unpack_from(uid_fmt, chna_data, offset)
|
||||||
|
|
||||||
|
# these values are either ascii or all null
|
||||||
|
|
||||||
|
self.channel_uids.append(ChannelEntry(track_index - 1,
|
||||||
|
uid.decode('ascii') , track_ref.decode('ascii'), pack_ref.decode('ascii')))
|
||||||
|
|
||||||
|
offset += calcsize(uid_fmt)
|
||||||
|
|
||||||
|
def xml_str(self) -> str:
|
||||||
|
"""ADM XML as a string"""
|
||||||
|
return ET.tostring(self.axml).decode("utf-8")
|
||||||
|
|
||||||
|
def programme(self) -> dict:
|
||||||
|
"""
|
||||||
|
Extract the ADM audioProgramme data structure and some of its reference properties
|
||||||
|
"""
|
||||||
|
ret_dict = dict()
|
||||||
|
|
||||||
|
nsmap = self.axml.getroot().nsmap
|
||||||
|
|
||||||
|
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
||||||
|
|
||||||
|
program = afext.find("audioProgramme", namespaces=nsmap)
|
||||||
|
ret_dict['programme_id'] = program.get("audioProgrammeID")
|
||||||
|
ret_dict['programme_name'] = program.get("audioProgrammeName")
|
||||||
|
ret_dict['programme_start'] = program.get("start")
|
||||||
|
ret_dict['programme_end'] = program.get("end")
|
||||||
|
ret_dict['contents'] = []
|
||||||
|
|
||||||
|
for content_ref in program.findall("audioContentIDRef", namespaces=nsmap):
|
||||||
|
content_dict = dict()
|
||||||
|
content_dict['content_id'] = cid = content_ref.text
|
||||||
|
content = afext.find("audioContent[@audioContentID='%s']" % cid, namespaces=nsmap)
|
||||||
|
content_dict['content_name'] = content.get("audioContentName")
|
||||||
|
content_dict['objects'] = []
|
||||||
|
|
||||||
|
for object_ref in content.findall("audioObjectIDRef", namespaces=nsmap):
|
||||||
|
object_dict = dict()
|
||||||
|
object_dict['object_id'] = oid = object_ref.text
|
||||||
|
object = afext.find("audioObject[@audioObjectID='%s']" % oid, namespaces=nsmap)
|
||||||
|
pack = object.find("audioPackFormatIDRef", namespaces=nsmap)
|
||||||
|
object_dict['object_name'] = object.get("audioObjectName")
|
||||||
|
object_dict['object_start'] = object.get("start")
|
||||||
|
object_dict['object_duration'] = object.get("duration")
|
||||||
|
object_dict['pack_id'] = pack.text
|
||||||
|
track_uid_list = []
|
||||||
|
for t in object.findall("audioTrackUIDRef", namespaces=nsmap):
|
||||||
|
track_uid_list.append(t.text)
|
||||||
|
|
||||||
|
object_dict['track_uids'] = track_uid_list
|
||||||
|
content_dict['objects'].append(object_dict)
|
||||||
|
|
||||||
|
ret_dict['contents'].append(content_dict)
|
||||||
|
|
||||||
|
return ret_dict
|
||||||
|
|
||||||
|
def track_info(self, index):
|
||||||
|
"""
|
||||||
|
Information about a track in the WAV file.
|
||||||
|
|
||||||
|
:param index: index of audio track (indexed from zero)
|
||||||
|
:returns: a dictionary with *content_name*, *content_id*, *object_name*, *object_id*,
|
||||||
|
*pack_format_name*, *pack_type*, *channel_format_name*
|
||||||
|
"""
|
||||||
|
channel_info = next((x for x in self.channel_uids if x.track_index == index), None)
|
||||||
|
|
||||||
|
if channel_info is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret_dict = {}
|
||||||
|
|
||||||
|
nsmap = self.axml.getroot().nsmap
|
||||||
|
|
||||||
|
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
||||||
|
|
||||||
|
trackformat_elem = afext.find("audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref,
|
||||||
|
namespaces=nsmap)
|
||||||
|
|
||||||
|
stream_id = trackformat_elem[0].text
|
||||||
|
|
||||||
|
channelformatref_elem = afext.find("audioStreamFormat[@audioStreamFormatID='%s']/audioChannelFormatIDRef" % stream_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
channelformat_id = channelformatref_elem.text
|
||||||
|
|
||||||
|
packformatref_elem = afext.find("audioStreamFormat[@audioStreamFormatID='%s']/audioPackFormatIDRef" % stream_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
packformat_id = packformatref_elem.text
|
||||||
|
|
||||||
|
channelformat_elem = afext.find("audioChannelFormat[@audioChannelFormatID='%s']" % channelformat_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
ret_dict['channel_format_name'] = channelformat_elem.get("audioChannelFormatName")
|
||||||
|
|
||||||
|
packformat_elem = afext.find("audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
ret_dict['pack_type'] = packformat_elem.get("typeDefinition")
|
||||||
|
ret_dict['pack_format_name'] = packformat_elem.get("audioPackFormatName")
|
||||||
|
|
||||||
|
object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']" % packformat_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
|
||||||
|
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
|
||||||
|
object_id = object_elem.get("audioObjectID")
|
||||||
|
ret_dict['object_id'] = object_id
|
||||||
|
|
||||||
|
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']" % object_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
|
||||||
|
ret_dict['content_name'] = content_elem.get("audioContentName")
|
||||||
|
ret_dict['content_id'] = content_elem.get("audioContentID")
|
||||||
|
|
||||||
|
return ret_dict
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
Get ADM metadata as a dictionary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def make_entry(channel_uid_rec):
|
||||||
|
rd = channel_uid_rec._asdict()
|
||||||
|
rd.update(self.track_info(channel_uid_rec.track_index))
|
||||||
|
return rd
|
||||||
|
|
||||||
|
return dict(channel_entries=list(map(lambda z: make_entry(z), self.channel_uids)),
|
||||||
|
programme=self.programme())
|
||||||
90
wavinfo/wave_bext_reader.py
Normal file
90
wavinfo/wave_bext_reader.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import struct
|
||||||
|
from .umid_parser import UMIDParser
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class WavBextReader:
|
||||||
|
def __init__(self, bext_data, encoding):
|
||||||
|
"""
|
||||||
|
Read Broadcast-WAV extended metadata.
|
||||||
|
:param bext_data: The bytes-like data.
|
||||||
|
:param encoding: The encoding to use when decoding the text fields of the
|
||||||
|
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
|
||||||
|
"""
|
||||||
|
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
||||||
|
|
||||||
|
rest_starts = struct.calcsize(packstring)
|
||||||
|
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||||
|
|
||||||
|
def sanitize_bytes(b : bytes) -> str:
|
||||||
|
# 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)
|
||||||
|
trimmed = b if first_null is None else b[:first_null]
|
||||||
|
decoded = trimmed.decode(encoding)
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
#: Description. A free-text field up to 256 characters long.
|
||||||
|
self.description : str = sanitize_bytes(unpacked[0])
|
||||||
|
#: Originator. Usually the name of the encoding application, sometimes
|
||||||
|
#: a artist name.
|
||||||
|
self.originator : str = sanitize_bytes(unpacked[1])
|
||||||
|
#: A unique identifier for the file, a serial number.
|
||||||
|
self.originator_ref : str = sanitize_bytes(unpacked[2])
|
||||||
|
#: Date of the recording, in the format YYYY-MM-DD in the local calendar
|
||||||
|
self.originator_date : str = sanitize_bytes(unpacked[3])
|
||||||
|
#: Time of the recording, in the format HH:MM:SS on the local clock
|
||||||
|
self.originator_time : str = sanitize_bytes(unpacked[4])
|
||||||
|
#: The sample offset of the start of the file relative to an
|
||||||
|
#: epoch, usually midnight the day of the recording.
|
||||||
|
self.time_reference : int = unpacked[5]
|
||||||
|
#: A variable-length text field containing a list of processes and
|
||||||
|
#: and conversions performed on the file.
|
||||||
|
self.coding_history : str = sanitize_bytes(bext_data[rest_starts:])
|
||||||
|
#: BEXT version.
|
||||||
|
self.version : int = unpacked[6]
|
||||||
|
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
|
||||||
|
#: may only be 32 bytes long.
|
||||||
|
self.umid : Optional[bytes] = None
|
||||||
|
#: EBU R128 Integrated loudness, in LUFS.
|
||||||
|
self.loudness_value : Optional[float] = None
|
||||||
|
#: EBU R128 Loudness rante, in LUFS.
|
||||||
|
self.loudness_range : Optional[float] = None
|
||||||
|
#: True peak level, in dBFS TP
|
||||||
|
self.max_true_peak : Optional[float] = None
|
||||||
|
#: EBU R128 Maximum momentary loudness, in LUFS
|
||||||
|
self.max_momentary_loudness : Optional[float] = None
|
||||||
|
#: EBU R128 Maximum short-term loudness, in LUFS.
|
||||||
|
self.max_shortterm_loudness : Optional[float] = None
|
||||||
|
|
||||||
|
if self.version > 0:
|
||||||
|
self.umid = unpacked[7]
|
||||||
|
|
||||||
|
if self.version > 1:
|
||||||
|
self.loudness_value = unpacked[8] / 100.0
|
||||||
|
self.loudness_range = unpacked[9] / 100.0
|
||||||
|
self.max_true_peak = unpacked[10] / 100.0
|
||||||
|
self.max_momentary_loudness = unpacked[11] / 100.0
|
||||||
|
self.max_shortterm_loudness = unpacked[12] / 100.0
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
if self.umid is not None:
|
||||||
|
umid_parsed = UMIDParser(self.umid)
|
||||||
|
umid_str = umid_parsed.basic_umid_to_str()
|
||||||
|
else:
|
||||||
|
umid_str = None
|
||||||
|
|
||||||
|
return {'description': self.description,
|
||||||
|
'originator': self.originator,
|
||||||
|
'originator_ref': self.originator_ref,
|
||||||
|
'originator_date': self.originator_date,
|
||||||
|
'originator_time': self.originator_time,
|
||||||
|
'time_reference': self.time_reference,
|
||||||
|
'version': self.version,
|
||||||
|
'umid': umid_str,
|
||||||
|
'coding_history': self.coding_history,
|
||||||
|
'loudness_value': self.loudness_value,
|
||||||
|
'loudness_range': self.loudness_range,
|
||||||
|
'max_true_peak': self.max_true_peak,
|
||||||
|
'max_momentary_loudness': self.max_momentary_loudness,
|
||||||
|
'max_shortterm_loudness': self.max_shortterm_loudness
|
||||||
|
}
|
||||||
661
wavinfo/wave_dbmd_reader.py
Normal file
661
wavinfo/wave_dbmd_reader.py
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
"""
|
||||||
|
Reading Dolby Bitstream Metadata
|
||||||
|
|
||||||
|
Unless otherwise stated, all § references here are to
|
||||||
|
`EBU Tech 3285 Supplement 6`_.
|
||||||
|
|
||||||
|
.. _EBU Tech 3285 Supplement 6: https://tech.ebu.ch/docs/tech/tech3285s6.pdf
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import IntEnum, Enum
|
||||||
|
from struct import unpack
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import List, Optional, Tuple, Any, Union
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
class SegmentType(IntEnum):
|
||||||
|
"""
|
||||||
|
Metadata segment type.
|
||||||
|
"""
|
||||||
|
EndMarker = 0x0
|
||||||
|
DolbyE = 0x1
|
||||||
|
# Reserved2 = 0x2
|
||||||
|
DolbyDigital = 0x3
|
||||||
|
# Reserved4 = 0x4
|
||||||
|
# Reserved5 = 0x5
|
||||||
|
# Reserved6 = 0x6
|
||||||
|
DolbyDigitalPlus = 0x7
|
||||||
|
AudioInfo = 0x8
|
||||||
|
DolbyAtmos = 0x9
|
||||||
|
DolbyAtmosSupplemental = 0xa
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _missing_(cls,val):
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DolbyDigitalPlusMetadata:
|
||||||
|
"""
|
||||||
|
*Dolby Digital Plus* is Dolby's brand for multichannel surround
|
||||||
|
on discrete formats that aren't AC-3 (Dolby Digital) or Dolby E. This
|
||||||
|
metadata segment is present in ADM wave files created with a Dolby Atmos
|
||||||
|
Production Suite.
|
||||||
|
|
||||||
|
Where an AC-3 bitstream can contain multiple programs, a Dolby Digital
|
||||||
|
Plus bitstream will only contain one program.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class DownMixLevelToken(Enum):
|
||||||
|
"""
|
||||||
|
A gain coefficient used in several metadata fields for downmix
|
||||||
|
scenarios.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUS_3DB = 0b000
|
||||||
|
"+3 dB"
|
||||||
|
|
||||||
|
PLUS_1_5DB = 0b001
|
||||||
|
"+1.5 dB"
|
||||||
|
|
||||||
|
UNITY = 0b010
|
||||||
|
"0dB"
|
||||||
|
|
||||||
|
MINUS_1_5DB = 0b011
|
||||||
|
"-1.5 dB"
|
||||||
|
|
||||||
|
MINUS_3DB = 0b100
|
||||||
|
"-3 dB"
|
||||||
|
|
||||||
|
MINUS_4_5DB = 0b101
|
||||||
|
"-4.5 dB"
|
||||||
|
|
||||||
|
MINUS_6DB = 0b110
|
||||||
|
"-6 dB"
|
||||||
|
|
||||||
|
MUTE = 0b111
|
||||||
|
"-∞ dB"
|
||||||
|
|
||||||
|
|
||||||
|
class DolbySurroundEncodingMode(Enum):
|
||||||
|
"""
|
||||||
|
Dolby surround endcoding mode.
|
||||||
|
"""
|
||||||
|
RESERVED = 0b11
|
||||||
|
IN_USE = 0b10
|
||||||
|
NOT_IN_USE = 0b01
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
|
||||||
|
|
||||||
|
class BitStreamMode(Enum):
|
||||||
|
"""
|
||||||
|
Dolby Digital Plus `bsmod` field
|
||||||
|
§ 4.3.2.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPLETE_MAIN = 0b000
|
||||||
|
"main audio service: complete main"
|
||||||
|
|
||||||
|
MUSIC_AND_EFFECTS = 0b001
|
||||||
|
"main audio service: music and effects"
|
||||||
|
|
||||||
|
VISUALLY_IMPAIRED = 0b010
|
||||||
|
"associated service: visually impaired"
|
||||||
|
|
||||||
|
HEARING_IMPAIRED = 0b011
|
||||||
|
"associated service: hearing impaired"
|
||||||
|
|
||||||
|
DIALOGUE_ONLY = 0b100
|
||||||
|
"associated service: dialogue"
|
||||||
|
|
||||||
|
COMMENTARY = 0b101
|
||||||
|
"associated service: commentary"
|
||||||
|
|
||||||
|
EMERGENCY = 0b110
|
||||||
|
"associated service: emergency"
|
||||||
|
|
||||||
|
VOICEOVER_KARAOKE = 0b111
|
||||||
|
"""
|
||||||
|
associated service: voice over *OR* main audio service: karaoke.
|
||||||
|
If `acmod` is `0b001` (mono 1/0), this is voice-over, otherwise it
|
||||||
|
should be interpreted as karaoke.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class AudioCodingMode(Enum):
|
||||||
|
"""
|
||||||
|
Dolby Digital Plus `acmod` field
|
||||||
|
§ 4.3.2.3
|
||||||
|
"""
|
||||||
|
RESERVED = 0b000
|
||||||
|
CH_ORD_1_0 = 0b001
|
||||||
|
"Mono"
|
||||||
|
CH_ORD_2_0 = 0b010
|
||||||
|
"L/R stereo"
|
||||||
|
CH_ORD_3_0 = 0b011
|
||||||
|
"LCR stereo"
|
||||||
|
CH_ORD_2_1 = 0b100
|
||||||
|
"LR + mono surround"
|
||||||
|
CH_ORD_3_1 = 0b101
|
||||||
|
"LCR + mono surround"
|
||||||
|
CH_ORD_2_2 = 0b110
|
||||||
|
"LR + LR surround"
|
||||||
|
CH_ORD_3_2 = 0b111
|
||||||
|
"LCR + LR surround"
|
||||||
|
|
||||||
|
|
||||||
|
class CenterDownMixLevel(Enum):
|
||||||
|
"""
|
||||||
|
§ 4.3.3.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
DOWN_3DB = 0b00
|
||||||
|
"Attenuate 3 dB"
|
||||||
|
|
||||||
|
DOWN_45DB = 0b01
|
||||||
|
"Attenuate 4.5 dB"
|
||||||
|
|
||||||
|
DOWN_6DB = 0b10
|
||||||
|
"Attenuate 6 dB"
|
||||||
|
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class SurroundDownMixLevel(Enum):
|
||||||
|
"""
|
||||||
|
Dolby Digital Plus `surmixlev` field
|
||||||
|
§ 4.3.3.2
|
||||||
|
"""
|
||||||
|
DOWN_3DB = 0b00
|
||||||
|
DOWN_6DB = 0b01
|
||||||
|
MUTE = 0b10
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class LanguageCode(int):
|
||||||
|
"""
|
||||||
|
§ 4.3.4.1
|
||||||
|
|
||||||
|
Per ATSC/A52 § 5.4.2.12, this is not in use and always 0xFF.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MixLevel(int):
|
||||||
|
"""
|
||||||
|
§ 4.3.6.2
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DialnormLevel(int):
|
||||||
|
"""
|
||||||
|
§ 4.3.4.4
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RoomType(Enum):
|
||||||
|
"""
|
||||||
|
`roomtyp` 4.3.6.3
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
LARGE_ROOM_X_CURVE = 0b01
|
||||||
|
SMALL_ROOM_FLAT_CURVE = 0b10
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class PreferredDownMixMode(Enum):
|
||||||
|
"""
|
||||||
|
Indicates the creating engineer's preference of what the receiver should
|
||||||
|
downmix.
|
||||||
|
§ 4.3.8.1
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
PRO_LOGIC = 0b01
|
||||||
|
STEREO = 0b10
|
||||||
|
PRO_LOGIC_2 = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class SurroundEXMode(IntEnum):
|
||||||
|
"""
|
||||||
|
Dolby Surround-EX mode.
|
||||||
|
`dsurexmod` § 4.3.9.1
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
NOT_SEX = 0b01
|
||||||
|
SEX = 0b10
|
||||||
|
PRO_LOGIC_2 = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class HeadphoneMode(IntEnum):
|
||||||
|
"""
|
||||||
|
`dheadphonmod` § 4.3.9.2
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
NOT_DOLBY_HEADPHONE = 0b01
|
||||||
|
DOLBY_HEADPHONE = 0b10
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class ADConverterType(Enum):
|
||||||
|
STANDARD = 0
|
||||||
|
HDCD = 1
|
||||||
|
|
||||||
|
|
||||||
|
class StreamDependency(Enum):
|
||||||
|
"""
|
||||||
|
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
INDEPENDENT = 0
|
||||||
|
DEPENDENT = 1
|
||||||
|
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
|
||||||
|
RESERVED = 3
|
||||||
|
|
||||||
|
|
||||||
|
class RFCompressionProfile(Enum):
|
||||||
|
"""
|
||||||
|
`compr1` RF compression profile
|
||||||
|
§ 4.3.10 (fig 42)
|
||||||
|
"""
|
||||||
|
NONE = 0
|
||||||
|
FILM_STANDARD = 1
|
||||||
|
FILM_LIGHT = 2
|
||||||
|
MUSIC_STANDARD = 3
|
||||||
|
MUSIC_LIGHT = 4
|
||||||
|
SPEECH = 5
|
||||||
|
|
||||||
|
#: Program ID number, this identifies the program in a multi-program
|
||||||
|
#: element. § 4.3.1
|
||||||
|
program_id: int
|
||||||
|
|
||||||
|
#: `True` if LFE is enabled. § 4.3.2.1
|
||||||
|
lfe_on: bool
|
||||||
|
|
||||||
|
#: The kind of service of this stream. `bsmod` § 4.3.2.2
|
||||||
|
bitstream_mode: BitStreamMode
|
||||||
|
|
||||||
|
#: Indicates which channels are in use. `acmod` § 4.3.2.3
|
||||||
|
audio_coding_mode: AudioCodingMode
|
||||||
|
|
||||||
|
#: When the front three channels are in use, gives the center
|
||||||
|
#: downmix level. ``
|
||||||
|
center_downmix_level: CenterDownMixLevel
|
||||||
|
|
||||||
|
#: When the surround channels are in use, gives the surround
|
||||||
|
#: downmix level.
|
||||||
|
surround_downmix_level: SurroundDownMixLevel
|
||||||
|
|
||||||
|
#: If the `acmod` is LR, this indicates if the channels
|
||||||
|
#: are encoded in Dolby Surround.
|
||||||
|
dolby_surround_encoded: DolbySurroundEncodingMode
|
||||||
|
|
||||||
|
#: `True` if there is a langcode present in the metadata.
|
||||||
|
langcode_present: bool
|
||||||
|
|
||||||
|
#: `True` if this bitstream is copyrighted.
|
||||||
|
copyright_bitstream: bool
|
||||||
|
|
||||||
|
#: `True` if this bitstream is original.
|
||||||
|
original_bitstream: bool
|
||||||
|
|
||||||
|
dialnorm: DialnormLevel
|
||||||
|
|
||||||
|
#: Language code
|
||||||
|
langcode: int
|
||||||
|
|
||||||
|
#: `True` if `mixlevel` and `roomtype` are valid
|
||||||
|
prod_info_exists: bool
|
||||||
|
|
||||||
|
#: Mix level
|
||||||
|
mixlevel: MixLevel
|
||||||
|
|
||||||
|
#: Room Type
|
||||||
|
roomtype: RoomType
|
||||||
|
|
||||||
|
#: LoRo preferred center downmix level
|
||||||
|
loro_center_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: LoRo preferred surround downmix level
|
||||||
|
loro_surround_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: Preferred downmix mode
|
||||||
|
downmix_mode: PreferredDownMixMode
|
||||||
|
|
||||||
|
#: LtRt preferred center downmix level
|
||||||
|
ltrt_center_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: LtRt preferred surround downmix level
|
||||||
|
ltrt_surround_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: Surround-EX mode
|
||||||
|
surround_ex_mode: SurroundEXMode
|
||||||
|
|
||||||
|
#: Dolby Headphone mode
|
||||||
|
dolby_headphone_encoded: HeadphoneMode
|
||||||
|
|
||||||
|
ad_converter_type: ADConverterType
|
||||||
|
compression_profile: RFCompressionProfile
|
||||||
|
dynamic_range: RFCompressionProfile
|
||||||
|
|
||||||
|
#: Indicates if this stream can be decoded independently or not
|
||||||
|
stream_dependency: StreamDependency
|
||||||
|
|
||||||
|
#: Data rate of this bitstream in kilobits per second
|
||||||
|
datarate_kbps: int
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(buffer: bytes):
|
||||||
|
assert len(buffer) == 96, "Dolby Digital Plus segment incorrect size, "
|
||||||
|
"expected 96 got %i" % len(buffer)
|
||||||
|
|
||||||
|
def program_id(b) -> int:
|
||||||
|
return b
|
||||||
|
|
||||||
|
def program_info(b):
|
||||||
|
return (b & 0x40) > 0, \
|
||||||
|
DolbyDigitalPlusMetadata.BitStreamMode(b & 0x38 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.AudioCodingMode(b & 0x7)
|
||||||
|
|
||||||
|
def ddplus_reserved1(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def surround_config(b):
|
||||||
|
return DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4), \
|
||||||
|
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2), \
|
||||||
|
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3)
|
||||||
|
|
||||||
|
def dialnorm_info(b):
|
||||||
|
return (b & 0x80) > 0 , b & 0x40 > 0, b & 0x20 > 0, \
|
||||||
|
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
|
||||||
|
|
||||||
|
def langcod(b) -> int:
|
||||||
|
return b
|
||||||
|
|
||||||
|
def audio_prod_info(b):
|
||||||
|
return (b & 0x80) > 0, \
|
||||||
|
DolbyDigitalPlusMetadata.MixLevel(b & 0x7c >> 2), \
|
||||||
|
DolbyDigitalPlusMetadata.RoomType(b & 0x3)
|
||||||
|
|
||||||
|
# loro_center_downmix_level, loro_surround_downmix_level
|
||||||
|
def ext_bsi1_word1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
||||||
|
|
||||||
|
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
|
||||||
|
def ext_bsi1_word2(b):
|
||||||
|
return DolbyDigitalPlusMetadata.PreferredDownMixMode(b & 0xC0 >> 6), \
|
||||||
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
||||||
|
|
||||||
|
#surround_ex_mode, dolby_headphone_encoded, ad_converter_type
|
||||||
|
def ext_bsi2_word1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \
|
||||||
|
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.ADConverterType( b & 0x4 >> 2)
|
||||||
|
|
||||||
|
def ddplus_reserved2(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def compr1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.RFCompressionProfile(b)
|
||||||
|
|
||||||
|
def dynrng1(b):
|
||||||
|
DolbyDigitalPlusMetadata.RFCompressionProfile(b)
|
||||||
|
|
||||||
|
def ddplus_reserved3(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ddplus_info1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.StreamDependency(b & 0xc >> 2)
|
||||||
|
|
||||||
|
def ddplus_reserved4(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def datarate(b) -> int:
|
||||||
|
return unpack("<H", b)[0]
|
||||||
|
|
||||||
|
def reserved(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
pid = program_id(buffer[0])
|
||||||
|
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
|
||||||
|
ddplus_reserved1(buffer[2:2])
|
||||||
|
center_downmix_level, surround_downmix_level, dolby_surround_encoded = surround_config(buffer[4])
|
||||||
|
langcode_present, copyright_bitstream, original_bitstream, dialnorm = dialnorm_info(buffer[5])
|
||||||
|
langcode = langcod(buffer[6])
|
||||||
|
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
|
||||||
|
|
||||||
|
loro_center_downmix_level, loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
|
||||||
|
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])
|
||||||
|
compression = compr1(buffer[14])
|
||||||
|
dynamic_range = dynrng1(buffer[15])
|
||||||
|
ddplus_reserved3(buffer[16:19])
|
||||||
|
stream_info = ddplus_info1(buffer[19])
|
||||||
|
ddplus_reserved4(buffer[20:25])
|
||||||
|
data_rate = datarate(buffer[25:27])
|
||||||
|
reserved(buffer[27:69])
|
||||||
|
|
||||||
|
return DolbyDigitalPlusMetadata(program_id=pid,
|
||||||
|
lfe_on=lfe_on,
|
||||||
|
bitstream_mode=bitstream_mode,
|
||||||
|
audio_coding_mode=audio_coding_mode,
|
||||||
|
center_downmix_level=center_downmix_level,
|
||||||
|
surround_downmix_level=surround_downmix_level,
|
||||||
|
dolby_surround_encoded=dolby_surround_encoded,
|
||||||
|
langcode_present=langcode_present,
|
||||||
|
copyright_bitstream=copyright_bitstream,
|
||||||
|
original_bitstream=original_bitstream,
|
||||||
|
dialnorm=dialnorm,
|
||||||
|
langcode=langcode,
|
||||||
|
prod_info_exists=prod_info_exists,
|
||||||
|
mixlevel=mixlevel,
|
||||||
|
roomtype=roomtype,
|
||||||
|
loro_center_downmix_level=loro_center_downmix_level,
|
||||||
|
loro_surround_downmix_level=loro_surround_downmix_level,
|
||||||
|
downmix_mode=downmix_mode,
|
||||||
|
ltrt_center_downmix_level=ltrt_center_downmix_level,
|
||||||
|
ltrt_surround_downmix_level=ltrt_surround_downmix_level,
|
||||||
|
surround_ex_mode=surround_ex_mode,
|
||||||
|
dolby_headphone_encoded=dolby_headphone_encoded,
|
||||||
|
ad_converter_type=ad_converter_type,
|
||||||
|
compression_profile=compression,
|
||||||
|
dynamic_range=dynamic_range,
|
||||||
|
stream_dependency=stream_info,
|
||||||
|
datarate_kbps=data_rate)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DolbyAtmosMetadata:
|
||||||
|
"""
|
||||||
|
Dolby Atmos Metadata Segment
|
||||||
|
|
||||||
|
https://github.com/DolbyLaboratories/dbmd-atmos-parser/
|
||||||
|
"""
|
||||||
|
|
||||||
|
class WarpMode(Enum):
|
||||||
|
NORMAL = 0x00
|
||||||
|
WARPING = 0x01
|
||||||
|
DOWNMIX_PLIIX = 0x02
|
||||||
|
DOWNMIX_LORO = 0x03
|
||||||
|
NOT_INDICATED = 0x04
|
||||||
|
|
||||||
|
tool_name: str
|
||||||
|
tool_version: Tuple[int,int,int]
|
||||||
|
warp_mode: WarpMode
|
||||||
|
|
||||||
|
SEGMENT_LENGTH = 248
|
||||||
|
TOOL_NAME_LENGTH = 64
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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))
|
||||||
|
|
||||||
|
h = BytesIO(data)
|
||||||
|
|
||||||
|
h.seek(32, 1)
|
||||||
|
toolname = h.read(cls.TOOL_NAME_LENGTH)
|
||||||
|
toolname = unpack("%is" % cls.TOOL_NAME_LENGTH, toolname)[0]
|
||||||
|
toolname = toolname.decode('utf-8').strip('\0')
|
||||||
|
|
||||||
|
vers = h.read(3)
|
||||||
|
major, minor, fix = unpack("BBB", vers)
|
||||||
|
|
||||||
|
h.seek(53, 1)
|
||||||
|
|
||||||
|
a_val = unpack("B", h.read(1))[0]
|
||||||
|
warp_mode = a_val & 0x7
|
||||||
|
|
||||||
|
return DolbyAtmosMetadata(tool_name=toolname,
|
||||||
|
tool_version=(major, minor, fix), warp_mode=DolbyAtmosMetadata.WarpMode(warp_mode))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DolbyAtmosSupplementalMetadata:
|
||||||
|
"""
|
||||||
|
Dolby Atmos supplemental metadata segment.
|
||||||
|
|
||||||
|
https://github.com/DolbyLaboratories/dbmd-atmos-parser/blob/master/dbmd_atmos_parse/src/dbmd_atmos_parse.c
|
||||||
|
"""
|
||||||
|
|
||||||
|
class BinauralRenderMode(Enum):
|
||||||
|
BYPASS = 0x00
|
||||||
|
NEAR = 0x01
|
||||||
|
FAR = 0x02
|
||||||
|
MID = 0x03
|
||||||
|
NOT_INDICATED = 0x04
|
||||||
|
|
||||||
|
|
||||||
|
object_count: int
|
||||||
|
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
|
||||||
|
trim_modes: List[int]
|
||||||
|
|
||||||
|
|
||||||
|
MAGIC = 0xf8726fbd
|
||||||
|
TRIM_CONFIG_COUNT = 9
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, data: bytes):
|
||||||
|
|
||||||
|
trim_modes = []
|
||||||
|
render_modes = []
|
||||||
|
|
||||||
|
h = BytesIO(data)
|
||||||
|
magic = unpack("<I", h.read(4))
|
||||||
|
assert magic == cls.MAGIC, "Magic value was not found"
|
||||||
|
|
||||||
|
object_count = unpack("<H", h.read(2))
|
||||||
|
|
||||||
|
h.read(1) #skip 1
|
||||||
|
|
||||||
|
for _ in range(cls.TRIM_CONFIG_COUNT):
|
||||||
|
auto_trim = unpack("B", h.read(1))
|
||||||
|
trim_modes.append(auto_trim)
|
||||||
|
|
||||||
|
h.read(14) #skip 14
|
||||||
|
|
||||||
|
h.read(object_count) # skip object_count bytes
|
||||||
|
|
||||||
|
for _ in range(object_count):
|
||||||
|
binaural_mode = unpack("B", h.read(1))
|
||||||
|
binaural_mode &= 0x7
|
||||||
|
render_modes.append(binaural_mode)
|
||||||
|
|
||||||
|
return DolbyAtmosSupplementalMetadata(object_count=object_count,
|
||||||
|
render_modes=render_modes,trim_modes=trim_modes)
|
||||||
|
|
||||||
|
|
||||||
|
class WavDolbyMetadataReader:
|
||||||
|
"""
|
||||||
|
Reads Dolby bitstream metadata.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: List of the Dolby Metadata Segments.
|
||||||
|
#:
|
||||||
|
#: Each list entry is a tuple of `SegmentType`, a `bool`
|
||||||
|
#: indicating if the segment's checksum was valid, and the
|
||||||
|
#: segment's parsed dataclass (or a `bytes` array if it was
|
||||||
|
#: not recognized).
|
||||||
|
segment_list: Tuple[Union[SegmentType, int], bool, Any]
|
||||||
|
|
||||||
|
version: Tuple[int,int,int,int]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def segment_checksum(bs: bytes, size: int):
|
||||||
|
retval = size
|
||||||
|
for b in bs:
|
||||||
|
retval += int(b)
|
||||||
|
retval &= 0xff
|
||||||
|
|
||||||
|
retval = ((~retval) + 1) & 0xff
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, dbmd_data) -> None:
|
||||||
|
self.segment_list = []
|
||||||
|
|
||||||
|
h = BytesIO(dbmd_data)
|
||||||
|
|
||||||
|
v_vec = []
|
||||||
|
for _ in range(4):
|
||||||
|
b = h.read(1)
|
||||||
|
v_vec.insert(0, unpack("B",b)[0])
|
||||||
|
|
||||||
|
self.version = tuple(v_vec)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
stype= SegmentType(unpack("B", h.read(1))[0])
|
||||||
|
if stype == SegmentType.EndMarker:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
seg_size = unpack("<H", h.read(2))[0]
|
||||||
|
seg_payload = h.read(seg_size)
|
||||||
|
expected_checksum = WavDolbyMetadataReader.segment_checksum(seg_payload, seg_size)
|
||||||
|
checksum = unpack("B", h.read(1))[0]
|
||||||
|
|
||||||
|
segment = seg_payload
|
||||||
|
if stype == SegmentType.DolbyDigitalPlus:
|
||||||
|
segment = DolbyDigitalPlusMetadata.load(segment)
|
||||||
|
elif stype == SegmentType.DolbyAtmos:
|
||||||
|
segment = DolbyAtmosMetadata.load(segment)
|
||||||
|
# elif stype == SegmentType.DolbyAtmosSupplemental:
|
||||||
|
# segment = DolbyAtmosSupplementalMetadata.load(segment)
|
||||||
|
|
||||||
|
self.segment_list.append( (stype, checksum == expected_checksum, segment) )
|
||||||
|
|
||||||
|
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
|
||||||
|
"""
|
||||||
|
Every valid Dolby Digital Plus metadata segment in the file.
|
||||||
|
"""
|
||||||
|
return [x[2] for x in self.segment_list \
|
||||||
|
if x[0] == SegmentType.DolbyDigitalPlus and x[1]]
|
||||||
|
|
||||||
|
def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
|
||||||
|
"""
|
||||||
|
Every valid Dolby Atmos metadata segment in the file.
|
||||||
|
"""
|
||||||
|
return [x[2] for x in self.segment_list \
|
||||||
|
if x[0] == SegmentType.DolbyAtmos and x[1]]
|
||||||
|
|
||||||
|
# def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
||||||
|
# """
|
||||||
|
# Every valid Dolby Atmos Supplemental metadata segment in the file.
|
||||||
|
# """
|
||||||
|
# return [x[2] for x in self.segment_list \
|
||||||
|
# if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
|
||||||
|
ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
|
||||||
|
atmos = map(lambda x: asdict(x), self.dolby_atmos())
|
||||||
|
#atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
|
||||||
|
|
||||||
|
return dict(dolby_digital_plus=list(ddp),
|
||||||
|
dolby_atmos=list(atmos))
|
||||||
88
wavinfo/wave_info_reader.py
Normal file
88
wavinfo/wave_info_reader.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from .riff_parser import parse_chunk, ListChunkDescriptor
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class WavInfoChunkReader:
|
||||||
|
|
||||||
|
def __init__(self, f, encoding):
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
f.seek(0)
|
||||||
|
parsed_chunks = parse_chunk(f)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
#: 'ICOP' Copyright
|
||||||
|
self.copyright : Optional[str] = self._get_field(f, b'ICOP')
|
||||||
|
#: 'IPRD' Product
|
||||||
|
self.product : Optional[str]= self._get_field(f, b'IPRD')
|
||||||
|
self.album : Optional[str] = self.product
|
||||||
|
#: 'IGNR' Genre
|
||||||
|
self.genre : Optional[str] = self._get_field(f, b'IGNR')
|
||||||
|
#: 'ISBJ' Supject
|
||||||
|
self.subject : Optional[str] = self._get_field(f, b'ISBJ')
|
||||||
|
#: 'IART' Artist, composer, author
|
||||||
|
self.artist : Optional[str] = self._get_field(f, b'IART')
|
||||||
|
#: 'ICMT' Comment
|
||||||
|
self.comment : Optional[str] = self._get_field(f, b'ICMT')
|
||||||
|
#: 'ISFT' Software, encoding application
|
||||||
|
self.software : Optional[str] = self._get_field(f, b'ISFT')
|
||||||
|
#: 'ICRD' Created date
|
||||||
|
self.created_date : Optional[str] = self._get_field(f, b'ICRD')
|
||||||
|
#: 'IENG' Engineer
|
||||||
|
self.engineer : Optional[str] = self._get_field(f, b'IENG')
|
||||||
|
#: 'ITCH' Technician
|
||||||
|
self.technician : Optional[str] = self._get_field(f, b'ITCH')
|
||||||
|
#: 'IKEY' Keywords, keyword list
|
||||||
|
self.keywords : Optional[str] = self._get_field(f, b'IKEY')
|
||||||
|
#: 'INAM' Name, title
|
||||||
|
self.title : Optional[str] = self._get_field(f, b'INAM')
|
||||||
|
#: 'ISRC' Source
|
||||||
|
self.source : Optional[str] = self._get_field(f, b'ISRC')
|
||||||
|
#: 'TAPE' Tape
|
||||||
|
self.tape : Optional[str] = self._get_field(f, b'TAPE')
|
||||||
|
#: 'IARL' Archival Location
|
||||||
|
self.archival_location : Optional[str] = self._get_field(f, b'IARL')
|
||||||
|
#: 'ICSM' Commissioned
|
||||||
|
self.commissioned : Optional[str] = self._get_field(f, b'ICMS')
|
||||||
|
|
||||||
|
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),
|
||||||
|
None)
|
||||||
|
|
||||||
|
if search is not None:
|
||||||
|
f.seek(search[0])
|
||||||
|
data = f.read(search[1])
|
||||||
|
return data.decode(self.encoding).rstrip('\0')
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
A dictionary with all of the key/values read from the INFO scope.
|
||||||
|
"""
|
||||||
|
return {'copyright': self.copyright,
|
||||||
|
'product': self.product,
|
||||||
|
'album': self.album,
|
||||||
|
'genre': self.genre,
|
||||||
|
'artist': self.artist,
|
||||||
|
'comment': self.comment,
|
||||||
|
'software': self.software,
|
||||||
|
'created_date': self.created_date,
|
||||||
|
'engineer': self.engineer,
|
||||||
|
'keywords': self.keywords,
|
||||||
|
'title': self.title,
|
||||||
|
'source': self.source,
|
||||||
|
'tape': self.tape,
|
||||||
|
'commissioned': self.commissioned,
|
||||||
|
'archival_location': self.archival_location,
|
||||||
|
'subject': self.subject,
|
||||||
|
'technician': self.technician
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return_val = self.to_dict()
|
||||||
|
return_val.update({'encoding': self.encoding})
|
||||||
|
return str(return_val)
|
||||||
@@ -1,40 +1,104 @@
|
|||||||
import xml.etree.ElementTree as ET
|
from lxml import etree as ET
|
||||||
|
import io
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
|
||||||
|
|
||||||
|
|
||||||
class WavIXMLFormat:
|
class WavIXMLFormat:
|
||||||
"""
|
"""
|
||||||
iXML recorder metadata, as defined by iXML 2.0
|
iXML recorder metadata.
|
||||||
"""
|
"""
|
||||||
def __init__(self, xml):
|
def __init__(self, xml):
|
||||||
|
"""
|
||||||
|
Parse iXML.
|
||||||
|
:param xml: A bytes-like object containing the iXML payload.
|
||||||
|
"""
|
||||||
self.source = xml
|
self.source = xml
|
||||||
self.parsed = ET.fromstring(xml)
|
xml_bytes = io.BytesIO(xml)
|
||||||
|
parser = ET.XMLParser(recover=True)
|
||||||
|
self.parsed = ET.parse(xml_bytes, parser=parser)
|
||||||
|
|
||||||
def _get_text_value(self, xpath):
|
def _get_text_value(self, xpath) -> Optional[str]:
|
||||||
e = self.parsed.find("./" + xpath)
|
e = self.parsed.find("./" + xpath)
|
||||||
if e is not None:
|
if e is not None:
|
||||||
return e.text
|
return e.text
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def xml_bytes(self):
|
||||||
|
return ET.tostring(self.parsed).decode("utf-8")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project(self):
|
def raw_xml(self):
|
||||||
|
"""
|
||||||
|
The root entity of the iXML document.
|
||||||
|
"""
|
||||||
|
return self.parsed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def track_list(self):
|
||||||
|
"""
|
||||||
|
A description of each track.
|
||||||
|
:returns: An Iterator
|
||||||
|
"""
|
||||||
|
for track in self.parsed.find("./TRACK_LIST").iter():
|
||||||
|
if track.tag == 'TRACK':
|
||||||
|
yield IXMLTrack(channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
|
||||||
|
interleave_index=track.xpath('string(INTERLEAVE_INDEX/text())'),
|
||||||
|
name=track.xpath('string(NAME/text())'),
|
||||||
|
function=track.xpath('string(FUNCTION/text())'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The project/film name entered for the recording.
|
||||||
|
"""
|
||||||
return self._get_text_value("PROJECT")
|
return self._get_text_value("PROJECT")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scene(self):
|
def scene(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Scene/slate.
|
||||||
|
"""
|
||||||
return self._get_text_value("SCENE")
|
return self._get_text_value("SCENE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def take(self):
|
def take(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Take number.
|
||||||
|
"""
|
||||||
return self._get_text_value("TAKE")
|
return self._get_text_value("TAKE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tape(self):
|
def tape(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Tape name.
|
||||||
|
"""
|
||||||
return self._get_text_value("TAPE")
|
return self._get_text_value("TAPE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family_uid(self):
|
def family_uid(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
return self._get_text_value("FILE_SET/FAMILY_UID")
|
return self._get_text_value("FILE_SET/FAMILY_UID")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family_name(self):
|
def family_name(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The name of this file's file family.
|
||||||
|
"""
|
||||||
return self._get_text_value("FILE_SET/FAMILY_NAME")
|
return self._get_text_value("FILE_SET/FAMILY_NAME")
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(track_list=list(map(lambda x: x._asdict(), self.track_list)),
|
||||||
|
project=self.project,
|
||||||
|
scene=self.scene,
|
||||||
|
take=self.take,
|
||||||
|
tape=self.tape,
|
||||||
|
family_uid=self.family_uid,
|
||||||
|
family_name=self.family_name
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
ListChunkDescriptor = namedtuple('ListChunkDescriptor' , 'signature children')
|
|
||||||
|
|
||||||
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length') ):
|
|
||||||
def read_data(self, from_stream):
|
|
||||||
from_stream.seek(self.start)
|
|
||||||
return from_stream.read(self.length)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_list_chunk(stream, length):
|
|
||||||
children = []
|
|
||||||
|
|
||||||
start = stream.tell()
|
|
||||||
|
|
||||||
signature = stream.read(4)
|
|
||||||
|
|
||||||
while (stream.tell() - start) < length:
|
|
||||||
children.append(parse_chunk(stream))
|
|
||||||
|
|
||||||
return ListChunkDescriptor(signature=signature, children=children)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_chunk(stream):
|
|
||||||
ident = stream.read(4)
|
|
||||||
if len(ident) != 4:
|
|
||||||
return
|
|
||||||
|
|
||||||
sizeb = stream.read(4)
|
|
||||||
size = struct.unpack('<I',sizeb)[0]
|
|
||||||
|
|
||||||
displacement = size
|
|
||||||
if displacement % 2 is not 0:
|
|
||||||
displacement = displacement + 1
|
|
||||||
|
|
||||||
if ident in [b'RIFF',b'LIST']:
|
|
||||||
return parse_list_chunk(stream=stream, length=size)
|
|
||||||
else:
|
|
||||||
start = stream.tell()
|
|
||||||
stream.seek(displacement,1)
|
|
||||||
return ChunkDescriptor(ident=ident, start=start, length=size)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,65 +1,120 @@
|
|||||||
|
#-*- coding: utf-8 -*-
|
||||||
import struct
|
import struct
|
||||||
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from .wave_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
|
from typing import Optional, Generator, Any
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
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_info_reader import WavInfoChunkReader
|
||||||
|
from .wave_adm_reader import WavADMReader
|
||||||
|
from .wave_dbmd_reader import WavDolbyMetadataReader
|
||||||
|
|
||||||
WavDataDescriptor = namedtuple('WavDataDescriptor','byte_count frame_count')
|
#: Calculated statistics about the audio data.
|
||||||
|
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
|
||||||
|
|
||||||
WavInfoFormat = namedtuple("WavInfoFormat",'audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
|
#: The format of the audio samples.
|
||||||
|
WavAudioFormat = namedtuple('WavAudioFormat',
|
||||||
WavBextFormat = namedtuple("WavBextFormat",'description originator originator_ref ' +
|
'audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
|
||||||
'originator_date originator_time time_reference version umid ' +
|
|
||||||
'loudness_value loudness_range max_true_peak max_momentary_loudness max_shortterm_loudness ' +
|
|
||||||
'coding_history')
|
|
||||||
|
|
||||||
|
|
||||||
class WavInfoReader():
|
class WavInfoReader:
|
||||||
"""
|
"""
|
||||||
format : WAV format
|
Parse a WAV audio file for metadata.
|
||||||
bext : The Broadcast-WAV extension as definied by EBU Tech 3285 v2 (2011)
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
|
||||||
with open(path, 'rb') as f:
|
"""
|
||||||
chunks = parse_chunk(f)
|
Create a new reader object.
|
||||||
|
|
||||||
self.main_list = chunks.children
|
:param path:
|
||||||
f.seek(0)
|
A filesystem path to the wav file you wish to probe or a
|
||||||
|
file handle to an open file.
|
||||||
|
|
||||||
self.fmt = self._get_format(f)
|
:param info_encoding:
|
||||||
self.bext = self._get_bext(f)
|
The text encoding of the INFO metadata fields.
|
||||||
self.ixml = self._get_ixml(f)
|
latin_1/Win CP1252 has always been a pretty good guess for this.
|
||||||
|
|
||||||
self.data = self._describe_data(f)
|
:param bext_encoding:
|
||||||
|
The text encoding to use when decoding the string
|
||||||
|
fields of the Broadcast-WAV extension. Per EBU 3285 this is ASCII
|
||||||
|
but this parameter is available to you if you encounter a weirdo.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.info_encoding = info_encoding
|
||||||
|
self.bext_encoding = bext_encoding
|
||||||
|
|
||||||
|
if hasattr(path, 'read'):
|
||||||
|
self.get_wav_info(path)
|
||||||
|
self.url = 'about:blank'
|
||||||
|
self.path = repr(path)
|
||||||
|
else:
|
||||||
|
absolute_path = os.path.abspath(path)
|
||||||
|
|
||||||
|
#: `file://` url for the file.
|
||||||
|
self.url: pathlib.Path = pathlib.Path(absolute_path).as_uri()
|
||||||
|
|
||||||
|
self.path = absolute_path
|
||||||
|
|
||||||
|
#: Wave audio data format.
|
||||||
|
self.fmt :Optional[WavAudioFormat] = None
|
||||||
|
|
||||||
|
#: Statistics of the `data` section.
|
||||||
|
self.data :Optional[WavDataDescriptor] = None
|
||||||
|
|
||||||
|
#: Broadcast-Wave metadata.
|
||||||
|
self.bext :Optional[WavBextReader] = None
|
||||||
|
|
||||||
|
#: iXML metadata.
|
||||||
|
self.ixml :Optional[WavIXMLFormat] = None
|
||||||
|
|
||||||
|
#: ADM Audio Definiton Model metadata.
|
||||||
|
self.adm :Optional[WavADMReader]= None
|
||||||
|
|
||||||
|
#: Dolby bitstream metadata.
|
||||||
|
self.dolby :Optional[WavDolbyMetadataReader] = None
|
||||||
|
|
||||||
|
#: RIFF INFO metadata.
|
||||||
|
self.info :Optional[WavInfoChunkReader]= None
|
||||||
|
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
self.get_wav_info(f)
|
||||||
|
|
||||||
|
def get_wav_info(self, wavfile):
|
||||||
|
chunks = parse_chunk(wavfile)
|
||||||
|
|
||||||
|
self.main_list = chunks.children
|
||||||
|
wavfile.seek(0)
|
||||||
|
|
||||||
|
self.fmt = self._get_format(wavfile)
|
||||||
|
self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
|
||||||
|
self.ixml = self._get_ixml(wavfile)
|
||||||
|
self.adm = self._get_adm(wavfile)
|
||||||
|
self.info = self._get_info(wavfile, encoding=self.info_encoding)
|
||||||
|
self.dolby = self._get_dbmd(wavfile)
|
||||||
|
self.data = self._describe_data()
|
||||||
|
|
||||||
def _find_chunk_data(self, ident, from_stream, default_none=False):
|
def _find_chunk_data(self, ident, from_stream, default_none=False):
|
||||||
chunk_descriptor = None
|
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor and chunk.ident == ident)
|
||||||
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor)
|
chunk_descriptor = next(top_chunks, None) if default_none else next(top_chunks)
|
||||||
|
return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None
|
||||||
|
|
||||||
if default_none:
|
def _describe_data(self):
|
||||||
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident),None)
|
data_chunk = next(c for c in self.main_list if type(c) is ChunkDescriptor and c.ident == b'data')
|
||||||
else:
|
|
||||||
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident))
|
|
||||||
|
|
||||||
if chunk_descriptor:
|
return WavDataDescriptor(byte_count=data_chunk.length,
|
||||||
return chunk_descriptor.read_data(from_stream)
|
frame_count=int(data_chunk.length / self.fmt.block_align))
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
def _get_format(self, f):
|
||||||
def _describe_data(self,f):
|
fmt_data = self._find_chunk_data(b'fmt ', f)
|
||||||
data_chunk = next(c for c in self.main_list if c.ident == b'data')
|
|
||||||
|
|
||||||
return WavDataDescriptor(byte_count= data_chunk.length,
|
|
||||||
frame_count= int(data_chunk.length / self.fmt.block_align))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_format(self,f):
|
|
||||||
fmt_data = self._find_chunk_data(b'fmt ',f)
|
|
||||||
|
|
||||||
# The format chunk is
|
# The format chunk is
|
||||||
# audio_format U16
|
# audio_format U16
|
||||||
@@ -73,76 +128,65 @@ class WavInfoReader():
|
|||||||
|
|
||||||
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
|
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
|
||||||
|
|
||||||
#0x0001 WAVE_FORMAT_PCM PCM
|
# 0x0001 WAVE_FORMAT_PCM PCM
|
||||||
#0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
|
# 0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
|
||||||
#0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
|
# 0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
|
||||||
#0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
|
# 0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
|
||||||
#0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
|
# 0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
|
||||||
if unpacked[0] == 0x0001:
|
|
||||||
return WavInfoFormat(audio_format = unpacked[0],
|
|
||||||
channel_count = unpacked[1],
|
|
||||||
sample_rate = unpacked[2],
|
|
||||||
byte_rate = unpacked[3],
|
|
||||||
block_align = unpacked[4],
|
|
||||||
bits_per_sample = unpacked[5]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_bext(self,f):
|
# https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html
|
||||||
|
return WavAudioFormat(audio_format=unpacked[0],
|
||||||
|
channel_count=unpacked[1],
|
||||||
|
sample_rate=unpacked[2],
|
||||||
|
byte_rate=unpacked[3],
|
||||||
|
block_align=unpacked[4],
|
||||||
|
bits_per_sample=unpacked[5]
|
||||||
|
)
|
||||||
|
|
||||||
bext_data = self._find_chunk_data(b'bext',f,default_none=True)
|
def _get_info(self, f, encoding):
|
||||||
|
finder = (chunk.signature for chunk in self.main_list if type(chunk) is ListChunkDescriptor)
|
||||||
|
|
||||||
# description[256]
|
if b'INFO' in finder:
|
||||||
# originator[32]
|
return WavInfoChunkReader(f, encoding)
|
||||||
# originatorref[32]
|
|
||||||
# originatordate[10] "YYYY:MM:DD"
|
|
||||||
# originatortime[8] "HH:MM:SS"
|
|
||||||
# lowtimeref U32
|
|
||||||
# hightimeref U32
|
|
||||||
# version U16
|
|
||||||
# umid[64]
|
|
||||||
#
|
|
||||||
# EBU 3285 fields
|
|
||||||
# loudnessvalue S16 (in LUFS*100)
|
|
||||||
# loudnessrange S16 (in LUFS*100)
|
|
||||||
# maxtruepeak S16 (in dbTB*100)
|
|
||||||
# maxmomentaryloudness S16 (LUFS*100)
|
|
||||||
# maxshorttermloudness S16 (LUFS*100)
|
|
||||||
# reserved[180]
|
|
||||||
# codinghistory []
|
|
||||||
if bext_data is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
packstring = "<256s"+ "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
def _get_bext(self, f, encoding):
|
||||||
|
bext_data = self._find_chunk_data(b'bext', f, default_none=True)
|
||||||
|
return WavBextReader(bext_data, encoding) if bext_data else None
|
||||||
|
|
||||||
rest_starts = struct.calcsize(packstring)
|
def _get_adm(self, f):
|
||||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
axml = self._find_chunk_data(b'axml', f, default_none=True)
|
||||||
|
chna = self._find_chunk_data(b'chna', f, default_none=True)
|
||||||
|
return WavADMReader(axml_data=axml, chna_data=chna) if axml and chna else None
|
||||||
|
|
||||||
return WavBextFormat(description=unpacked[0].decode('ascii').rstrip('\0'),
|
def _get_dbmd(self, f):
|
||||||
originator = unpacked[1].decode('ascii').rstrip('\0'),
|
dbmd_data = self._find_chunk_data(b'dbmd', f, default_none=True)
|
||||||
originator_ref = unpacked[2].decode('ascii').rstrip('\0'),
|
return WavDolbyMetadataReader(dbmd_data=dbmd_data) if dbmd_data else None
|
||||||
originator_date = unpacked[3].decode('ascii'),
|
|
||||||
originator_time = unpacked[4].decode('ascii'),
|
|
||||||
time_reference = unpacked[5],
|
|
||||||
version = unpacked[6],
|
|
||||||
umid = unpacked[7],
|
|
||||||
loudness_value = unpacked[8] / 100.0,
|
|
||||||
loudness_range = unpacked[9] / 100.0,
|
|
||||||
max_true_peak = unpacked[10] / 100.0,
|
|
||||||
max_momentary_loudness = unpacked[11] / 100.0,
|
|
||||||
max_shortterm_loudness = unpacked[12] / 100.0,
|
|
||||||
coding_history = bext_data[rest_starts:].decode('ascii').rstrip('\0')
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_ixml(self,f):
|
|
||||||
|
|
||||||
ixml_data = self._find_chunk_data(b'iXML',f,default_none=True)
|
|
||||||
if ixml_data is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ixml_string = ixml_data.decode('utf-8')
|
|
||||||
return WavIXMLFormat(ixml_string)
|
|
||||||
|
|
||||||
|
def _get_ixml(self, f):
|
||||||
|
ixml_data = self._find_chunk_data(b'iXML', f, default_none=True)
|
||||||
|
return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
|
||||||
|
|
||||||
|
def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()"
|
||||||
|
"""
|
||||||
|
Walk all of the available metadata fields.
|
||||||
|
|
||||||
|
:yields: tuples of the *scope*, *key*, and *value* of
|
||||||
|
each metadatum. The *scope* value will be one of
|
||||||
|
"fmt", "data", "ixml", "bext", "info", "dolby", or "adm".
|
||||||
|
"""
|
||||||
|
|
||||||
|
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'dolby')
|
||||||
|
|
||||||
|
for scope in scopes:
|
||||||
|
if scope in ['fmt', 'data']:
|
||||||
|
attr = self.__getattribute__(scope)
|
||||||
|
for field in attr._fields:
|
||||||
|
yield scope, field, attr.__getattribute__(field)
|
||||||
|
|
||||||
|
else:
|
||||||
|
dict = self.__getattribute__(scope).to_dict() if self.__getattribute__(scope) else {}
|
||||||
|
for key in dict.keys():
|
||||||
|
yield scope, key, dict[key]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)
|
||||||
|
|||||||
Reference in New Issue
Block a user