159 Commits

Author SHA1 Message Date
Jamie Hardt
8fcc9787f6 Fixed typo in include 2022-11-23 19:11:57 -08:00
Jamie Hardt
52ea6fdb60 Delete metadata.py 2022-11-23 19:10:48 -08:00
Jamie Hardt
c26942db04 Cleaned up some wavereader code 2022-11-23 18:57:35 -08:00
Jamie Hardt
12eff79e5f Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-11-23 18:50:52 -08:00
Jamie Hardt
d9e3e8deee Fixed a big in INFO parsing 2022-11-23 18:50:46 -08:00
Jamie Hardt
c17fb242e3 Delete _build/html/_static directory 2022-11-23 18:32:09 -08:00
Jamie Hardt
64f3a640e3 Docs 2022-11-23 18:30:56 -08:00
Jamie Hardt
5d4f97f6cc Documentation 2022-11-23 18:30:50 -08:00
Jamie Hardt
f9e5f28f7d Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-11-23 18:15:17 -08:00
Jamie Hardt
3e6c485eb9 Re-factored package metadata 2022-11-23 18:14:34 -08:00
Jamie Hardt
436bbe1686 Rename _README.md to README.md 2022-11-23 18:04:45 -08:00
Jamie Hardt
ddb4d5cdca Delete README.rst 2022-11-23 18:04:28 -08:00
Jamie Hardt
cec8165919 Rename README.md to _README.md 2022-11-23 18:03:59 -08:00
Jamie Hardt
73a5034e02 Documentation 2022-11-23 18:02:46 -08:00
Jamie Hardt
9a46db4ae5 Update README.md 2022-11-23 14:56:22 -08:00
Jamie Hardt
ccca30e234 Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-11-23 14:55:24 -08:00
Jamie Hardt
c367acc185 Docs v2.0. 2022-11-23 14:55:21 -08:00
Jamie Hardt
2266cc5032 Update metadata.py
Version 2.0.0
2022-11-23 14:46:19 -08:00
Jamie Hardt
ec5b796181 Fixed readme 2022-11-23 14:44:40 -08:00
Jamie Hardt
97bdb23441 Base documentation for ADM 2022-11-23 14:42:31 -08:00
Jamie Hardt
8f2fd69b00 ADM support 2022-11-23 14:23:42 -08:00
Jamie Hardt
a063fffb41 Fixing walk
And adding to_dict method to
ADM
2022-11-23 11:43:47 -08:00
Jamie Hardt
5b9d326e94 Removed some dead code 2022-11-23 11:04:00 -08:00
Jamie Hardt
85775055a9 removed settings 2022-11-23 10:52:41 -08:00
Jamie Hardt
59509e4399 Added .vscode to gitignore 2022-11-23 10:51:05 -08:00
Jamie Hardt
3a63ce9c8c Fixed mistake in call 2022-11-23 10:48:48 -08:00
Jamie Hardt
5bfe0bd95b Moved Test ADM file into folder 2022-11-23 09:03:26 -08:00
Jamie Hardt
992de72cc9 Add files via upload
Added an ADM WAV from Pro Tools
2022-11-23 08:43:48 -08:00
Jamie Hardt
ee305cebf4 Update setup.py 2022-11-22 22:42:40 -08:00
Jamie Hardt
ea4f484488 Update python-package.yml
Adding 3.11 to matrix
2022-11-22 22:41:27 -08:00
Jamie Hardt
d00e07be36 Update README.md 2022-11-22 22:38:45 -08:00
Jamie Hardt
68931348a6 Update README.md 2022-11-22 22:37:55 -08:00
Jamie Hardt
68c75fc43f ADM impl 2022-11-23 06:29:07 +00:00
Jamie Hardt
1eca249ba4 Axml implementation 2022-11-23 06:23:31 +00:00
Jamie Hardt
2052fa385a Some AXML impl 2022-11-22 19:51:08 +00:00
Jamie Hardt
3096f02971 README.md
Added dolby ADM profile link
2022-11-22 19:50:51 +00:00
Jamie Hardt
be47786439 Update devcontainer.json 2022-11-21 23:15:33 -08:00
Jamie Hardt
ecde5359f1 Update devcontainer.json 2022-11-21 22:53:25 -08:00
Jamie Hardt
8ae73213bc Dev container 2022-11-22 06:52:44 +00:00
Jamie Hardt
53217ce293 Devcontainer 2022-11-22 06:48:57 +00:00
Jamie Hardt
f9969d32cc Setting up devcontainer 2022-11-22 06:45:53 +00:00
Jamie Hardt
04c402680b Create devcontainer.json 2022-11-21 22:22:07 -08:00
Jamie Hardt
f10a546fe9 Update pythonpublish.yml 2022-11-21 20:27:58 -08:00
Jamie Hardt
ec42ee1d3d Update pythonpublish.yml
Added a Report to mastadon action
2022-11-20 12:22:39 -08:00
Jamie Hardt
bba4d67641 Streamlined requirements
Separated reqs into project and docs venvs
2022-11-20 12:12:24 -08:00
Jamie Hardt
4bc7f94198 Twiddle 2022-11-18 22:44:20 -08:00
Jamie Hardt
14eb8df496 Delete .idea directory 2022-11-16 21:15:53 -08:00
Jamie Hardt
a3aee8e785 Update LICENSE 2022-11-16 21:14:10 -08:00
Jamie Hardt
9e9b6b512b Update README.md 2022-11-16 20:57:03 -08:00
Jamie Hardt
a3365c113d Update metadata.py 2022-11-16 20:44:52 -08:00
Jamie Hardt
fbf4d72915 Added a punch of type annotations
For documentation
2022-11-16 20:40:07 -08:00
Jamie Hardt
90f273cf99 Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-11-16 20:37:39 -08:00
Jamie Hardt
bec3d98ee7 Added some type annotations for doc help 2022-11-16 20:23:35 -08:00
Jamie Hardt
a87bc71755 Update .readthedocs.yaml 2022-11-16 20:05:26 -08:00
Jamie Hardt
4817e7eb49 Update .readthedocs.yaml 2022-11-16 20:04:35 -08:00
Jamie Hardt
07832f7133 Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-11-16 20:02:45 -08:00
Jamie Hardt
f3d03296d5 source 2022-11-16 20:02:24 -08:00
Jamie Hardt
9496912b15 Update .readthedocs.yaml 2022-11-16 19:57:49 -08:00
Jamie Hardt
1077b49ce0 Update .readthedocs.yaml 2022-11-16 19:54:50 -08:00
Jamie Hardt
90042d57b2 Reorg doc files 2022-11-16 19:49:59 -08:00
Jamie Hardt
f12d7dfea0 Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-11-16 19:49:44 -08:00
Jamie Hardt
c2327568a8 Tweaked docs/conf 2022-11-16 19:42:23 -08:00
Jamie Hardt
722df48f9b Added docs/source dir 2022-11-16 19:41:31 -08:00
Jamie Hardt
17c0357364 Moved sources to new folder 2022-11-16 19:41:10 -08:00
Jamie Hardt
7e88c46d54 Update conf.py 2022-11-16 19:39:09 -08:00
Jamie Hardt
df90c67a73 Update conf.py 2022-11-16 19:33:01 -08:00
Jamie Hardt
cc107bf65d Update .readthedocs.yaml 2022-11-16 19:31:36 -08:00
Jamie Hardt
f9c68e0995 Update .readthedocs.yaml 2022-11-16 19:31:03 -08:00
Jamie Hardt
a98dd2668a Update conf.py 2022-11-16 19:29:47 -08:00
Jamie Hardt
d8ff4ed63b Update .readthedocs.yaml 2022-11-16 19:28:36 -08:00
Jamie Hardt
084c3d7ae5 Create .readthedocs.yaml 2022-11-16 19:26:47 -08:00
Jamie Hardt
e69b71e989 Merge branch 'release' 2022-11-16 19:17:25 -08:00
Jamie Hardt
6fe6126f3a Updated docs 2022-11-16 19:15:51 -08:00
Jamie Hardt
e57c76a722 Update README.md 2022-11-15 11:35:59 -08:00
Jamie Hardt
e40a2c5471 Update pythonpublish.yml 2022-11-15 11:29:47 -08:00
Jamie Hardt
dec6180744 Update pythonpublish.yml 2022-11-15 11:27:34 -08:00
Jamie Hardt
edbe748718 Update metadata.py 2022-11-15 11:17:02 -08:00
Jamie Hardt
2019a4ec63 Update pythonpublish.yml
Switched to pypi token authentication
2022-11-15 11:12:00 -08:00
Jamie Hardt
fb43838c7d Update README.md 2022-11-15 11:06:28 -08:00
Jamie Hardt
4cd58b8ddd Delete .travis.yml 2022-11-15 11:04:30 -08:00
Jamie Hardt
38dab7723f Update python-package.yml
Changed action name
2022-11-15 11:02:14 -08:00
Jamie Hardt
354d88a5b2 Update python-package.yml
Removed 3.6 and 3.7 from test grid
2022-11-15 10:58:51 -08:00
Jamie Hardt
d8a405b6d2 Update setup.py
Dropping 3.6 and 3.7, there seems to be a lxml conflict with its dependencies on numpy.
2022-11-15 10:58:12 -08:00
Jamie Hardt
5f7e467fbd Update python-package.yml 2022-11-15 10:56:11 -08:00
Jamie Hardt
3377ddb4b9 Updated ear to latest version 2022-11-15 10:53:12 -08:00
Jamie Hardt
9cd6cf7f12 Relieved requirements for attrs package 2022-11-15 10:51:02 -08:00
Jamie Hardt
1f8ebe253b Updated requirements for pytest 2022-11-15 10:49:18 -08:00
Jamie Hardt
fe46d1b242 Update python-package.yml 2022-11-15 10:37:04 -08:00
Jamie Hardt
b213933ad8 Update README.md 2022-11-15 10:35:59 -08:00
Jamie Hardt
7e314f7475 Update README.md 2022-11-15 10:34:41 -08:00
Jamie Hardt
b2d6fd3c92 Update README.md 2022-11-15 10:34:07 -08:00
Jamie Hardt
c4d8608c8f Create python-package.yml 2022-11-15 10:33:36 -08:00
Jamie Hardt
5605b05f9f Update README.md 2022-11-15 10:32:11 -08:00
Jamie Hardt
5d71cabda7 Delete .coveragerc 2022-11-15 10:30:31 -08:00
Jamie Hardt
c2f87b1fef Update README.md 2022-11-15 10:29:58 -08:00
Jamie Hardt
3db40d4f12 Added dosctring 2022-11-13 22:43:22 +00:00
Jamie Hardt
40b30f5bd8 Merge pull request #11 from soundappraisal/master
Make wavinfo work with filehandles.
2022-11-03 18:03:51 -07:00
Ronald van Elburg
048f20c64c Move version and author information to separate file. The current location leads to problems with dependencies which are only resolved after running pip. 2022-10-17 15:18:24 +02:00
Ronald van Elburg
6a69df2ee8 Move version and author information to separate file. The current location leads to problems with dependencies which are only resolved after running pip. 2022-10-17 13:28:28 +02:00
Ronald van Elburg
ec327ee76f Move version and author information to separate file. The current location leads to problems with dependencies which are only resolved after running pip. 2022-10-17 13:25:41 +02:00
Ronald van Elburg
62a34cfee8 Make it possible to pass file handles or in memory wav data to wavinfo. Some of the fields for the __repr__ where not available for files. For these the url member is set to "about:blank", and the self.path to the representation of the incoming object. The formating in __repr__ turned out to be broken, with fixing that we also had to fix the test on __repr__. 2022-08-30 11:42:47 +02:00
Jamie Hardt
c966097e7d Nudged version to 1.6.3 2022-01-06 11:45:39 -08:00
Jamie Hardt
35311e394d Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-01-06 11:13:43 -08:00
Jamie Hardt
0633a8685c Deleted .idea dir 2022-01-06 11:11:33 -08:00
Jamie Hardt
f65665a06c Delete python-package.yml 2022-01-06 11:08:48 -08:00
Jamie Hardt
261572cff3 Delete python-package-conda.yml 2022-01-06 11:06:47 -08:00
Jamie Hardt
623a8569fd Create python-package.yml 2022-01-06 11:06:31 -08:00
Jamie Hardt
31493f7cf4 Create python-package-conda.yml 2022-01-06 11:05:17 -08:00
Jamie Hardt
4809bb4844 Bumped lxml requirements re CVE-2021-43818 2022-01-06 11:01:28 -08:00
Jamie Hardt
a76f3b1518 Fixed idea interpreter again 2020-12-09 20:16:35 -08:00
Jamie Hardt
b8cb585d50 idea target stuff 2020-12-09 19:47:23 -08:00
Jamie Hardt
84a76f9c74 Update __init__.py
Version 1.6.2
2020-10-19 21:29:40 -07:00
Jamie Hardt
5a1a12e21e Update __init__.py
Version 1.6.1
2020-10-19 21:28:58 -07:00
Jamie Hardt
06835ffe11 Update setup.py
Added 3.9
2020-10-19 21:26:22 -07:00
Jamie Hardt
1b25b8214d Update .travis.yml
Added 3.9
2020-10-19 21:25:44 -07:00
Jamie Hardt
cfc1a451bc Update .travis.yml 2020-10-08 21:37:50 -07:00
Jamie Hardt
0788613ea3 Update setup.py 2020-10-08 21:37:01 -07:00
Jamie Hardt
e9e4b4bcbb Made quick fixes of these to get tests working 2020-10-08 21:31:32 -07:00
Jamie Hardt
c114eb7cf3 Create dolby_parser.py 2020-08-27 13:37:49 -07:00
Jamie Hardt
4576d65da6 Update wave_reader.py
Removed needless sys import
2020-08-21 22:39:52 -07:00
Jamie Hardt
65994db36d Added 3.8 to idea 2020-08-17 11:27:30 -07:00
Jamie Hardt
1a3417d3e5 Merge pull request #8 from elibroftw/master
Updated Code
2020-08-17 11:15:50 -07:00
Elijah Lopez
7589d5fb82 Add metadata tests 2020-08-14 15:19:28 -04:00
Elijah Lopez
6014d1d48b Update utils.py 2020-08-14 15:05:32 -04:00
Elijah Lopez
f8bf6cb4a0 Add tests 2020-08-14 15:03:06 -04:00
Elijah Lopez
6d8e717f42 Update wave_info_reader.py 2020-08-14 14:51:31 -04:00
Elijah Lopez
ba232605db fix bugs 2020-08-14 14:48:49 -04:00
Elijah Lopez
9a90a0c310 Update wave_reader.py 2020-08-14 14:34:17 -04:00
Elijah Lopez
add390c0a0 Formatting, refactoring, __repr__ 2020-08-14 09:07:56 -04:00
Jamie Hardt
7351623e3a Update riff_parser.py
Pass file signature to parse_rf64()
2020-08-07 22:56:31 -07:00
Jamie Hardt
c23ca4bded Update rf64_parser.py
Parameterize the file magic number.
2020-08-07 22:55:37 -07:00
Jamie Hardt
8fe799b211 Update riff_parser.py
Added `BW64` identifier, which is apparently what certain ITU/EBU big WAV files use.
2020-08-07 22:52:28 -07:00
Jamie Hardt
18ebd22ec1 Update wave_info_reader.py
Removed redundant "ISFT" fourcc
2020-06-24 18:30:14 -07:00
Jamie Hardt
cf8aa36fc3 Update README.md 2020-01-06 09:16:24 -08:00
Jamie Hardt
12d16a472f Update README.md 2020-01-06 09:12:24 -08:00
Jamie Hardt
39210738e3 Update umid_parser.py
Can't figure out how these are formatted as string yet so will just output raw hex
2020-01-06 08:58:32 -08:00
Jamie Hardt
966da7c4a2 Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2020-01-06 08:27:38 -08:00
Jamie Hardt
5a30ce3afc UMID Implementation 2020-01-06 08:27:34 -08:00
Jamie Hardt
1b9547e8c2 Style tweaks
Style stuff
2020-01-06 08:27:26 -08:00
Jamie Hardt
1507898b9e Update jamiehardt.xml 2020-01-06 08:23:56 -08:00
Jamie Hardt
4f2a6689f5 Update README.md 2020-01-05 19:41:52 -08:00
Jamie Hardt
597acb2122 Update README.md 2020-01-05 19:28:54 -08:00
Jamie Hardt
9d9592e9e1 Update wave_reader.py
output INFO and test for if these aren't present
2020-01-05 19:20:12 -08:00
Jamie Hardt
b0c5a7de72 Update wave_bext_reader.py
Print UMID in to_dict
2020-01-05 19:19:56 -08:00
Jamie Hardt
c36b53c5c5 Update wave_ixml_reader.py
Removed dead code from iXML parser
2020-01-05 17:20:05 -08:00
Jamie Hardt
25485d9601 Update riff_parser.py
Removed code that isn't being used.
2020-01-05 17:14:09 -08:00
Jamie Hardt
ffa51eaff4 Tests for walking metadata 2020-01-05 17:10:33 -08:00
Jamie Hardt
93a9ca0fd3 Update README.md 2020-01-05 16:45:45 -08:00
Jamie Hardt
b930fc6d6e Update README.md 2020-01-05 16:43:07 -08:00
Jamie Hardt
5f7803fd00 Update .travis.yml 2020-01-05 15:00:02 -08:00
Jamie Hardt
e37a37221b Update README.md 2020-01-05 14:56:23 -08:00
Jamie Hardt
d0b0b06ecb Update README.md 2020-01-05 14:38:31 -08:00
Jamie Hardt
5824406ae6 Update umid_parser.py 2020-01-05 14:29:46 -08:00
Jamie Hardt
dbbc0683f5 Update .travis.yml 2020-01-05 14:28:36 -08:00
Jamie Hardt
24c6871bbc Update pythonpublish.yml 2020-01-05 14:23:55 -08:00
Jamie Hardt
c62dfb7a8a Update .travis.yml 2020-01-05 13:54:49 -08:00
Jamie Hardt
6c8cc47788 Update README.md
Added note on command-line entry point
2020-01-05 10:10:37 -08:00
Jamie Hardt
a90d3f4b38 Version 1.5
Added command-line entrypoint, more UMID implementation
2020-01-05 10:05:54 -08:00
Jamie Hardt
3ede4de06a Delete pypi_upload.sh 2020-01-04 22:30:15 -08:00
49 changed files with 1064 additions and 593 deletions

View File

@@ -1,13 +0,0 @@
[run]
branch = True
source = wavinfo
[report]
exclude_lines =
if self.debug:
pragma: no cover
raise NotImplementedError
if __name__ == .__main__.:
ignore_errors = True
omit =
tests/*

View 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
View 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

View File

@@ -2,7 +2,7 @@ name: Upload Python Package
on:
release:
types: [created]
types: [published]
jobs:
deploy:
@@ -16,11 +16,21 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
pip install setuptools wheel twine lxml
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
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 }}

4
.gitignore vendored
View File

@@ -89,6 +89,8 @@ venv/
ENV/
env.bak/
venv.bak/
docs_venv/
venv_docs/
# Spyder project settings
.spyderproject
@@ -106,3 +108,5 @@ venv.bak/
# vim swap
*.swp
.DS_Store
.vscode/

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/workspace.xml

View File

@@ -1,8 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="jamiehardt">
<words>
<w>ident</w>
<w>umid</w>
</words>
</dictionary>
</component>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated
View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/wavinfo.iml" filepath="$PROJECT_DIR$/.idea/wavinfo.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

11
.idea/wavinfo.iml generated
View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.7" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

29
.readthedocs.yaml Normal file
View 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

View File

@@ -1,25 +0,0 @@
dist: xenial
language: python
python:
# - "2.7"
- "3.6"
- "3.5"
- "3.7"
- "3.8"
script:
- "gunzip tests/test_files/rf64/*.gz"
- "python setup.py test"
# - "python -m pytest tests/ -v --cov wavinfo --cov-report term-missing"
before_install:
- "sudo apt-get update"
- "sudo add-apt-repository universe"
- "sudo apt-get install -y ffmpeg"
- "pip install pytest"
# - "pip install coverage"
# - "pip install codecov"
# - "pip install pytest-cov==2.5.0"
# - "pip install coverage==4.4"
install:
- "pip install setuptools"
# after_success:
# - "codecov"

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Jamie Hardt
Copyright (c) 2022 Jamie Hardt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,35 +1,39 @@
[![Build Status](https://travis-ci.com/iluvcapra/wavinfo.svg?branch=master)](https://travis-ci.com/iluvcapra/wavinfo)
[![Documentation Status](https://readthedocs.org/projects/wavinfo/badge/?version=latest)](https://wavinfo.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/wavinfo.svg) ![](https://img.shields.io/pypi/pyversions/wavinfo.svg) [![](https://img.shields.io/pypi/v/wavinfo.svg)](https://pypi.org/project/wavinfo/) ![](https://img.shields.io/pypi/wheel/wavinfo.svg)
[![Lint and Test](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml/badge.svg)](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
# 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 and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
## Metadata Support
`wavinfo` reads:
* __Broadcast-WAVE__ metadata, compliant with [EBU Tech 3285v2 (2011)][ebu], including embedded program
loudness and coding history, if extant. This also includes the [SMPTE 330M __UMID__][smpte_330m2011]
Unique Materials Identifier.
* [__iXML__ production recorder metadata][ixml], including project, scene, and take tags, recorder notes
* [__Broadcast-WAVE__][ebu] metadata, including embedded program
loudness and coding history and [__SMPTE UMID__][smpte_330m2011].
* [__ADM__][adm] track metadata, including channel, pack formats, object and content names.
* [__iXML__][ixml] production recorder metadata, including project, scene, and take tags, recorder notes
and file family information.
* Most of the common __RIFF INFO__ metadata fields.
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count
* 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:
* iXML `STEINBERG` sound library attributes.
* [__Dolby RMU__][dolby] metadata and [EBU Tech 3285 Supplement 6][ebu3285s6].
* Pro Tools __embedded regions__.
* iXML `STEINBERG` sound library attributes.
[dolby]:https://developer.dolby.com/globalassets/documentation/technology/dolby_atmos_master_adm_profile_v1.0.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
[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.
@@ -39,23 +43,17 @@ from wavinfo import WavInfoReader
path = '../tests/test_files/A101_1.WAV'
info = WavInfoReader(path)
adm_metadata = info.adm
ixml_metadata = info.ixml
```
### Basic WAV Data
The package also installs a shell command:
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)
(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)
>>> (48000, 2, 6, 24)
```sh
$ wavinfo test_files/A101_1.WAV
```
## Other Resources
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).

View File

@@ -4,7 +4,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = .
SOURCEDIR = source
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
@@ -16,4 +16,4 @@ help:
# 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)
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

25
docs/requirements.txt Normal file
View 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

18
docs/source/classes.rst Normal file
View File

@@ -0,0 +1,18 @@
Other wavinfo Classes
===============
.. autoclass:: wavinfo.wave_reader.WavInfoReader
:members:
.. automethod:: __init__
.. autoclass:: wavinfo.wave_reader.WavAudioFormat
:members:
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
:members:

View File

@@ -14,19 +14,22 @@
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
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'2019, Jamie Hardt'
copyright = u'2022, Jamie Hardt'
author = u'Jamie Hardt'
# The short X.Y version
version = u''
version = wavinfo.__version__
# The full version, including alpha/beta/rc tags
release = u'v1.1'
release = wavinfo.__version__
# -- General configuration ---------------------------------------------------
@@ -61,7 +64,7 @@ master_doc = 'index'
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.

View File

@@ -6,30 +6,17 @@
Welcome to wavinfo's documentation!
===================================
.. module:: wavinfo
.. autoclass:: WavInfoReader
:members:
.. automethod:: __init__
.. autoclass:: wavinfo.wave_reader.WavAudioFormat
:members:
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
:members:
.. toctree::
:maxdepth: 2
:caption: Notes:
:caption: Notes
quickstart
metadata_scopes/adm.rst
metadata_scopes/bext.rst
metadata_scopes/ixml.rst
metadata_scopes/info.rst
metadata_scopes/ixml.rst
classes
Indices and tables

View File

@@ -0,0 +1,15 @@
ADM (Audio Definition Model) Metadata
=====================================
Notes
-----
Class Reference
---------------
.. module:: wavinfo
.. autoclass:: wavinfo.wave_adm_reader.WavADMReader
:members:

View File

@@ -1,11 +1,6 @@
Broadcast WAV Extension
=======================
.. module:: wavinfo
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
:members:
Notes
-----
@@ -63,3 +58,12 @@ Result:
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:

View File

@@ -1,14 +1,6 @@
INFO Metadata
=============
.. module:: wavinfo
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
:members:
Notes
-----
@@ -28,5 +20,12 @@ music library software.
print("INFO Comment:", bullet.info.comment)
Class Reference
---------------
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
:members:

View File

@@ -1,13 +1,6 @@
iXML Production Recorder Metadata
=================================
.. module:: wavinfo
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
:members:
Notes
-----
iXML allows an XML document to be embedded in a WAV file.
@@ -41,4 +34,10 @@ Result:
iXML File Family UID: USSDVGR1112089007124001008206300
Class Reference
---------------
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
:members:

View File

@@ -0,0 +1,12 @@
ptulsconv Quickstart
====================
.. code-block:: python
:caption: Using wavinfo
import wavinfo
path = 'path/to/your/wave/audio.wav'
info = wavinfo.WavInfoReader(path)

View File

@@ -1,2 +0,0 @@
#!/bin/bash
python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/*

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
lxml==4.9.1

View File

@@ -1,15 +1,17 @@
from setuptools import setup
from wavinfo import __author__, __license__, __version__
with open("README.md", "r") as fh:
long_description = fh.read()
setup(name='wavinfo',
version='1.4.1',
author='Jamie Hardt',
version=__version__,
author=__author__,
author_email='jamiehardt@me.com',
description='Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.',
long_description_content_type="text/markdown",
long_description=long_description,
license=__license__,
url='https://github.com/iluvcapra/wavinfo',
project_urls={
'Source':
@@ -24,10 +26,15 @@ setup(name='wavinfo',
'License :: OSI Approved :: MIT License',
'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8"],
"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']
install_requires=['lxml'],
entry_points={
'console_scripts': [
'wavinfo = wavinfo.__main__:main'
]
}
)

40
tests/test_adm.py Normal file
View File

@@ -0,0 +1,40 @@
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_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"])

Binary file not shown.

Binary file not shown.

25
tests/test_walk.py Normal file
View 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()

View File

@@ -1,39 +1,39 @@
import os.path
import sys
from unittest import TestCase
from .utils import all_files, ffprobe
import wavinfo
class TestWaveInfo(TestCase):
def test_sanity(self):
for wav_file in all_files():
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):
for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file)
ffprobe_info = ffprobe(wav_file)
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.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_raw_sample']) )
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.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_sample']))
if info.fmt.audio_format == 1:
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm') )
byte_rate = int(ffprobe_info['streams'][0]['sample_rate']) \
* ffprobe_info['streams'][0]['channels'] \
* int(ffprobe_info['streams'][0]['bits_per_raw_sample']) / 8
self.assertEqual( info.fmt.byte_rate , byte_rate )
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm'))
streams = ffprobe_info['streams'][0]
byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_sample']) / 8
self.assertEqual(info.fmt.byte_rate, byte_rate)
def test_data_against_ffprobe(self):
for wav_file in all_files():
info = wavinfo.WavInfoReader(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):
for wav_file in all_files():
@@ -41,56 +41,73 @@ class TestWaveInfo(TestCase):
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'])
else:
self.assertEqual(info.bext.description, '')
if 'encoded_by' in ffprobe_info['format']['tags']:
self.assertEqual( info.bext.originator, ffprobe_info['format']['tags']['encoded_by'] )
self.assertEqual(info.bext.originator, ffprobe_info['format']['tags']['encoded_by'])
else:
self.assertEqual( info.bext.originator, '')
self.assertEqual(info.bext.originator, '')
if 'originator_reference' in ffprobe_info['format']['tags']:
self.assertEqual( info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'] )
self.assertEqual(info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'])
else:
self.assertEqual( info.bext.originator_ref, '')
self.assertEqual(info.bext.originator_ref, '')
# 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']) )
# 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'] )
self.assertEqual(info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'])
else:
self.assertEqual( info.bext.coding_history, '' )
self.assertEqual(info.bext.coding_history, '')
def test_ixml(self):
expected = {'A101_4.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '4',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
'A101_3.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '3',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
'A101_2.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '2',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
'A101_1.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '1',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
}
expected = {'A101_4.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '4',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
'A101_3.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '3',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
'A101_2.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '2',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
'A101_1.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '1',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
}
for wav_file in all_files():
basename = os.path.basename(wav_file)
basename = os.path.basename(wav_file)
if basename in expected:
info = wavinfo.WavInfoReader(wav_file)
e = expected[basename]
self.assertEqual( e['project'], info.ixml.project )
self.assertEqual( e['scene'], info.ixml.scene )
self.assertEqual( e['take'], info.ixml.take )
self.assertEqual( e['tape'], info.ixml.tape )
self.assertEqual( e['family_uid'], info.ixml.family_uid )
self.assertEqual(e['project'], info.ixml.project)
self.assertEqual(e['scene'], info.ixml.scene)
self.assertEqual(e['take'], info.ixml.take)
self.assertEqual(e['tape'], info.ixml.tape)
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.assertTrue(track.name == 'MKH516 A')
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)

View File

@@ -8,5 +8,6 @@ from unittest import TestCase
import wavinfo
class TestZoomF8(TestCase):
pass

View File

@@ -4,10 +4,11 @@ import subprocess
from subprocess import PIPE
import json
FFPROBE='ffprobe'
FFPROBE = 'ffprobe'
def ffprobe(path):
arguments = [ FFPROBE , "-of", "json" , "-show_format", "-show_streams", path ]
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
if int(sys.version[0]) < 3:
process = subprocess.Popen(arguments, stdout=PIPE)
process.wait()
@@ -27,13 +28,9 @@ def ffprobe(path):
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']:
if ext in ['.wav', '.WAV']:
yield os.path.join(dirpath, filename)

View File

@@ -7,6 +7,6 @@ Go to the documentation for wavinfo.WavInfoReader for more information.
from .wave_reader import WavInfoReader
from .riff_parser import WavInfoEOFError
__version__ = '1.4.1'
__version__ = '2.0.1'
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
__license__ = "MIT"
__license__ = "MIT"

34
wavinfo/__main__.py Normal file
View File

@@ -0,0 +1,34 @@
from optparse import OptionParser, OptionGroup
import datetime
from . import WavInfoReader
import sys
import json
def main():
parser = OptionParser()
parser.usage = 'wavinfo [FILE.wav]*'
# parser.add_option('-f', dest='output_format', help='Set the output format',
# default='json',
# metavar='FORMAT')
(options, args) = parser.parse_args(sys.argv)
for arg in args[1:]:
try:
this_file = WavInfoReader(path=arg)
ret_dict = {'file_argument': arg, 'run_date': datetime.datetime.now().isoformat() , '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, fp=sys.stdout, indent=2)
except Exception as e:
print(e)
if __name__ == "__main__":
main()

188
wavinfo/dolby_parser.py Normal file
View File

@@ -0,0 +1,188 @@
# Dolby RMU Metadata per EBU Tech 3285 Supp 6
#
# https://tech.ebu.ch/docs/tech/tech3285s6.pdf
#
from struct import unpack, calcsize
from enum import (Enum, IntEnum)
CHUNK_IDENT = "dbmd"
DOLBY_VERSION = "1.0.0.6"
class _DPPGenericDownMixLevel(Enum):
PLUS_3DB = 0b000
PLUS_1_5DB = 0b001
UNITY = 0b010
MINUS_1_5DB = 0b011
MINUS_3DB = 0b100
MINUS_4_5DB = 0b101
MINUS_6DB = 0b110
MUTE = 0b111
class DPPDolbySurroundEncodingMode(Enum):
RESERVED = 0b11
IN_USE = 0b10
NOT_IN_USE = 0b01
NOT_INDICATED = 0b00
# DPPLoRoDownMixCenterLevel
# DPPLtRtCenterMixLevel
# DPPLtRtSurroundMixLevel
class DolbyMetadataSegmentTypes(IntEnum):
END_MARKER = 0
DOLBY_E_METADATA = 1
DOLBY_DIGITAL_METADATA = 3
DOLBY_DIGITAL_PLUS_METADATA = 7
AUDIO_INFO = 8
class DDPBitStreamMode(Enum):
"""
Dolby Digital Plus `bsmod` field
§ 4.3.2.2
"""
COMPLETE_MAIN = 0b000
MUSIC_AND_EFFECTS = 0b001
VISUALLY_IMPAIRED = 0b010
HEARING_IMPAIRED = 0b011
DIALOGUE_ONLY = 0b100
COMMENTARY = 0b101
EMERGENCY = 0b110
VOICEOVER = 0b111 # if audioconfigmode is 1_0
KARAOKE = 0b1000 # if audioconfigmode is not 1_0
class DDPAudioCodingMode(Enum):
"""
Dolby Digital Plus `acmod` field
§ 4.3.2.3
"""
RESERVED = 0b000
CH_ORD_1_0 = 0b001
CH_ORD_2_0 = 0b010
CH_ORD_3_0 = 0b011
CH_ORD_2_1 = 0b100
CH_ORD_3_1 = 0b101
CH_ORD_2_2 = 0b110
CH_ORD_3_2 = 0b111
class DPPCenterDownMixLevel(Enum):
"""
§ 4.3.3.1
"""
DOWN_3DB = 0b00
DOWN_45DB = 0b01
DOWN_6DB = 0b10
RESERVED = 0b11
class DPPSurroundDownMixLevel(Enum):
"""
Dolby Digital Plus `surmixlev` field
§ 4.3.3.2
"""
DOWN_3DB = 0b00
DOWN_6DB = 0b01
MUTE = 0b10
RESERVED = 0b11
class DPPLanguageCode(Enum):
"""
§ 4.3.4.1 , 4.3.5 (always 0xFF)
"""
# this is removed in https://www.atsc.org/wp-content/uploads/2015/03/A52-201212-17.pdf § 5.4.2.12
# It should just be 0xff
pass
class DPPMixLevel(int):
pass
class DPPDialnormLevel(int):
pass
class DPPRoomTime(Enum):
"""
`roomtyp` 4.3.6.3
"""
NOT_INDICATED = 0b00
LARGE_ROOM_X_CURVE = 0b01
SMALL_ROOM_FLAT_CURVE = 0b10
RESERVED = 0b11
class DPPPreferredDownMixMode(Enum):
"""
§ 4.3.8.1
"""
NOT_INDICATED = 0b00
PRO_LOGIC = 0b01
STEREO = 0b10
PRO_LOGIC_2 = 0b11
# class DPPLtRtCenterMixLevel(_DPPGenericDownMixLevel):
# pass
#
#
# class DPPLtRtSurroundMixLevel(_DPPGenericDownMixLevel):
# pass
#
#
# class DPPSurroundEXMode(_DPPGenericInUseIndicator):
# pass
#
#
# class DPPHeadphoneMode(_DPPGenericInUseIndicator):
# pass
class DPPADConverterType(Enum):
STANDARD = 0
HDCD = 1
class DDPStreamDependency(Enum):
"""
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
"""
INDEPENDENT = 0
DEPENDENT = 1
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
RESERVED = 3
class DDPDataRate(int):
pass
class DPPRFCompressionProfile(Enum):
NONE = 0
FILM_STANDARD = 1
FILM_LIGHT = 2
MUSIC_STANDARD = 3
MUSIC_LIGHT = 4
SPEECH = 5
class DolbyDigitalPlusMetadata:
@classmethod
def parse(cls, binary_data):
binary_format = "<BBxxBBBBBBBBBBBBBxxxBxxxxxH"
assert len(binary_data >= calcsize(binary_format))
fields = unpack(binary_format, binary_data)
class WavDolbyReader:
def __init__(self, dolby_data):
version, remainder = unpack("<U", dolby_data[0]), dolby_data[1:]
## FIXME continues...

View File

@@ -1,40 +1,40 @@
import struct
from collections import namedtuple
from . import riff_parser
from . import riff_parser
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
def parse_rf64(stream):
#print("starting parse_rf64")
def parse_rf64(stream, signature = b'RF64'):
# print("starting parse_rf64")
start = stream.tell()
assert( stream.read(4) == b'WAVE' )
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))
# 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)
# print("Found chunks64s:", length_lookup_table)
for n in range(length_lookup_table):
bigname, bigsize = struct.unpack_from( chunksize64format , ds64data, offset= ds64_fields_size )
bigname, bigsize = struct.unpack_from( chunksize64format , ds64_data, offset= ds64_fields_size )
bigchunk_table[bigname] = bigsize
bigchunk_table[b'data'] = data_size
bigchunk_table[b'RF64'] = riff_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 ) )
# 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 )

View File

@@ -11,17 +11,18 @@ class WavInfoEOFError(EOFError):
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
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
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')):
@@ -35,7 +36,7 @@ def parse_list_chunk(stream, length, rf64_context=None):
signature = stream.read(4)
children = []
while (stream.tell() - start + 8) < length:
while stream.tell() - start + 8 < length:
child_chunk = parse_chunk(stream, rf64_context=rf64_context)
children.append(child_chunk)
@@ -55,16 +56,16 @@ def parse_chunk(stream, rf64_context=None):
data_size = struct.unpack('<I', size_bytes)[0]
if data_size == 0xFFFFFFFF:
if rf64_context is None and ident == b'RF64':
rf64_context = parse_rf64(stream=stream)
data_size = rf64_context.bigchunk_table[ident]
displacement = data_size
if displacement % 2 is not 0:
displacement = displacement + 1
if rf64_context is None and ident in {b'RF64', b'BW64'}:
rf64_context = parse_rf64(stream=stream, signature=ident)
if ident in [b'RIFF', b'LIST', b'RF64']:
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()

View File

@@ -1,5 +1,9 @@
import struct
from typing import Union
from functools import reduce
def binary_to_string(binary_value):
return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
class UMIDParser:
"""
@@ -7,113 +11,111 @@ class UMIDParser:
This implementation is based on SMPTE ST 330:2011
"""
def __init__(self, raw_umid: bytearray):
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]
@property
def universal_label(self) -> bytearray:
return self.raw_umid[0:12]
@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) -> int:
return struct.unpack('<I', self.raw_umid[13:4])[0] & 0x00ffffff
@property
def material_number(self) -> bytearray:
return self.raw_umid[14:16]
@property
def material_number_hex(self) -> str:
result_str = ''
for n in range(16):
result_str = '{:x}'.format(self.material_number[n]) + result_str
return result_str
@property
def source_pack(self) -> Union[bytearray, None]:
if self.indicated_length == 'extended':
return self.raw_umid[32:32]
else:
return None
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

View File

@@ -0,0 +1,89 @@
"""
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.
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,
uid.decode('ascii') , track_ref.decode('ascii'), pack_ref.decode('ascii')))
offset += calcsize(uid_fmt)
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, object_name, pack_format_name, pack_type,
channel_format_name
"""
channel_info = next((x for x in self.channel_uids if x.track_index == index + 1), None)
if channel_info is None:
return None
ret_dict = {}
nsmap = self.axml.getroot().nsmap
trackformat_elem = self.axml.find(".//audioFormatExtended/audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref, namespaces=nsmap)
stream_id = trackformat_elem[0].text
channelformatref_elem = self.axml.find(".//audioFormatExtended/audioStreamFormat[@audioStreamFormatID='%s']/audioChannelFormatIDRef" % stream_id, namespaces=nsmap)
channelformat_id = channelformatref_elem.text
packformatref_elem = self.axml.find(".//audioFormatExtended/audioStreamFormat[@audioStreamFormatID='%s']/audioPackFormatIDRef" % stream_id, namespaces=nsmap)
packformat_id = packformatref_elem.text
channelformat_elem = self.axml.find(".//audioFormatExtended/audioChannelFormat[@audioChannelFormatID='%s']" % channelformat_id, namespaces=nsmap)
ret_dict['channel_format_name'] = channelformat_elem.get("audioChannelFormatName")
packformat_elem = self.axml.find(".//audioFormatExtended/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 = self.axml.find(".//audioFormatExtended/audioObject[audioPackFormatIDRef = '%s']" % packformat_id, namespaces=nsmap)
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
object_id = object_elem.get("audioObjectID")
content_elem = self.axml.find(".//audioFormatExtended/audioContent/[audioObjectIDRef = '%s']" % object_id, namespaces=nsmap)
ret_dict['content_name'] = content_elem.get("audioContentName")
return ret_dict
def to_dict(self):
return dict(channel_entries=list(map(lambda z: z._asdict(), self.channel_uids)))

View File

@@ -1,85 +1,89 @@
import struct
from .umid_parser import UMIDParser
from typing import Optional
class WavBextReader:
def __init__(self,bext_data,encoding):
def __init__(self, bext_data, encoding):
"""
Read Broadcast-WAV extended metadata.
:param best_data: The bytes-like data.
"param encoding: The encoding to use when decoding the text fields of the
: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"
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
rest_starts = struct.calcsize(packstring)
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
def sanatize_bytes(bytes):
first_null = next( (index for index, byte in enumerate(bytes) if byte == 0 ), None )
if first_null is not None:
trimmed = bytes[:first_null]
else:
trimmed = bytes
def sanitize_bytes(b : bytes) -> str:
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 = sanatize_bytes(unpacked[0])
self.description : str = sanitize_bytes(unpacked[0])
#: Originator. Usually the name of the encoding application, sometimes
#: a artist name.
self.originator = sanatize_bytes(unpacked[1])
#: A unique identifer for the file, a serial number.
self.originator_ref = sanatize_bytes(unpacked[2])
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 YYY-MM-DD
self.originator_date = sanatize_bytes(unpacked[3])
self.originator_date : str = sanitize_bytes(unpacked[3])
#: Time of the recording, in the format HH:MM:SS.
self.originator_time = sanatize_bytes(unpacked[4])
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 = unpacked[5]
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 = sanatize_bytes(bext_data[rest_starts:])
self.coding_history : str = sanitize_bytes(bext_data[rest_starts:])
#: BEXT version.
self.version = unpacked[6]
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 = None
self.umid : Optional[bytes] = None
#: EBU R128 Integrated loudness, in LUFS.
self.loudness_value = None
self.loudness_value : Optional[float] = None
#: EBU R128 Loudness rante, in LUFS.
self.loudness_range = None
self.loudness_range : Optional[float] = None
#: True peak level, in dBFS TP
self.max_true_peak = None
self.max_true_peak : Optional[float] = None
#: EBU R128 Maximum momentary loudness, in LUFS
self.max_momentary_loudness = None
self.max_momentary_loudness : Optional[float] = None
#: EBU R128 Maximum short-term loudness, in LUFS.
self.max_shortterm_loudness = None
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
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):
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,
'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
}
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
}

View File

@@ -1,6 +1,7 @@
from .riff_parser import parse_chunk, ListChunkDescriptor
from typing import Optional
class WavInfoChunkReader:
def __init__(self, f, encoding):
@@ -9,53 +10,47 @@ class WavInfoChunkReader:
f.seek(0)
parsed_chunks = parse_chunk(f)
list_chunks = [chunk for chunk in parsed_chunks.children \
if type(chunk) is ListChunkDescriptor]
list_chunks = [chunk for chunk in parsed_chunks.children if type(chunk) is ListChunkDescriptor]
self.info_chunk = next((chunk for chunk in list_chunks if chunk.signature == b'INFO'), None)
self.info_chunk = next((chunk for chunk in list_chunks \
if chunk.signature == b'INFO'), None)
#: 'ICOP' Copyright
self.copyright = self._get_field(f,b'ICOP')
self.copyright : Optional[str] = self._get_field(f, b'ICOP')
#: 'IPRD' Product
self.product = self._get_field(f,b'IPRD')
self.product : Optional[str]= self._get_field(f, b'IPRD')
self.album : Optional[str] = self.product
#: 'IGNR' Genre
self.genre = self._get_field(f,b'IGNR')
self.genre : Optional[str] = self._get_field(f, b'IGNR')
#: 'ISBJ' Supject
self.subject = self._get_field(f,b'ISBJ')
self.subject : Optional[str] = self._get_field(f, b'ISBJ')
#: 'IART' Artist, composer, author
self.artist = self._get_field(f,b'IART')
self.artist : Optional[str] = self._get_field(f, b'IART')
#: 'ICMT' Comment
self.comment = self._get_field(f,b'ICMT')
self.comment : Optional[str] = self._get_field(f, b'ICMT')
#: 'ISFT' Software, encoding application
self.software = self._get_field(f,b'ISFT')
self.software : Optional[str] = self._get_field(f, b'ISFT')
#: 'ICRD' Created date
self.created_date = self._get_field(f,b'ICRD')
self.created_date : Optional[str] = self._get_field(f, b'ICRD')
#: 'IENG' Engineer
self.engineer = self._get_field(f,b'IENG')
self.engineer : Optional[str] = self._get_field(f, b'IENG')
#: 'ITCH' Technician
self.technician = self._get_field(f,b'ITCH')
self.technician : Optional[str] = self._get_field(f, b'ITCH')
#: 'IKEY' Keywords, keyword list
self.keywords = self._get_field(f,b'IKEY')
self.keywords : Optional[str] = self._get_field(f, b'IKEY')
#: 'INAM' Name, title
self.title = self._get_field(f,b'INAM')
self.title : Optional[str] = self._get_field(f, b'INAM')
#: 'ISRC' Source
self.source = self._get_field(f,b'ISRC')
self.source : Optional[str] = self._get_field(f, b'ISRC')
#: 'TAPE' Tape
self.tape = self._get_field(f,b'TAPE')
self.tape : Optional[str] = self._get_field(f, b'TAPE')
#: 'IARL' Archival Location
self.archival_location = self._get_field(f,b'IARL')
#: 'ISFT' Software
self.software = self._get_field(f,b'ISFT')
self.archival_location : Optional[str] = self._get_field(f, b'IARL')
#: 'ICSM' Commissioned
self.commissioned = self._get_field(f,b'ICMS')
self.commissioned : Optional[str] = self._get_field(f, b'ICMS')
def _get_field(self, f, field_ident):
search = next( ( (chunk.start, chunk.length) for chunk in self.info_chunk.children \
if chunk.ident == field_ident ), None)
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])
@@ -64,32 +59,30 @@ class WavInfoChunkReader:
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,
'genre': self.genre,
'artist': self.artist,
'comment': self.comment,
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,
'title': self.title,
'source': self.source,
'tape': self.tape,
'commissioned': self.commissioned,
'software': self.software,
'archival_location':self.archival_location,
'subject': self.subject,
'technician':self.technician
'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)

View File

@@ -1,11 +1,11 @@
#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:
"""
iXML recorder metadata.
@@ -16,20 +16,16 @@ class WavIXMLFormat:
:param xml: A bytes-like object containing the iXML payload.
"""
self.source = xml
xmlBytes = io.BytesIO(xml)
try:
parser = ET.XMLParser(recover=True)
self.parsed = ET.parse(xmlBytes, parser=parser)
except ET.ParseError as err:
print("Error parsing iXML: " + str(err))
decoded = xml.decode(encoding='utf_8_sig')
print(decoded)
self.parsed = ET.parse(io.StringIO(decoded))
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)
if e is not None:
return e.text
else:
return None
@property
def raw_xml(self):
@@ -52,35 +48,35 @@ class WavIXMLFormat:
function=track.xpath('string(FUNCTION/text())'))
@property
def project(self):
def project(self) -> Optional[str]:
"""
The project/film name entered for the recording.
"""
return self._get_text_value("PROJECT")
@property
def scene(self):
def scene(self) -> Optional[str]:
"""
Scene/slate.
"""
return self._get_text_value("SCENE")
@property
def take(self):
def take(self) -> Optional[str]:
"""
Take number.
"""
return self._get_text_value("TAKE")
@property
def tape(self):
def tape(self) -> Optional[str]:
"""
Tape name.
"""
return self._get_text_value("TAPE")
@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.
@@ -88,10 +84,18 @@ class WavIXMLFormat:
return self._get_text_value("FILE_SET/FAMILY_UID")
@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")
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
)

View File

@@ -1,26 +1,25 @@
#-*- coding: utf-8 -*-
import struct
import os
import sys
from collections import namedtuple
if sys.version[0] == '3':
import pathlib
else:
import urlparse, urllib
import pathlib
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
from .wave_ixml_reader import WavIXMLFormat
from .wave_bext_reader import WavBextReader
from .wave_info_reader import WavInfoChunkReader
from .wave_adm_reader import WavADMReader
#: Calculated statistics about the audio data.
WavDataDescriptor = namedtuple('WavDataDescriptor','byte_count frame_count')
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
#: The format of the audio samples.
WavAudioFormat = namedtuple('WavAudioFormat','audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
WavAudioFormat = namedtuple('WavAudioFormat',
'audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
class WavInfoReader():
class WavInfoReader:
"""
Parse a WAV audio file for metadata.
"""
@@ -29,66 +28,76 @@ class WavInfoReader():
"""
Create a new reader object.
:param path: A filesystem path to the wav file you wish to probe.
:param path:
A filesystem path to the wav file you wish to probe or a
file handle to an open file.
:param info_encoding:
The text encoding of the INFO metadata fields.
latin_1/Win CP1252 has always been a pretty good guess for this.
: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.
:param info_encoding: The text encoding of the INFO metadata fields.
latin_1/Win CP1252 has always been a pretty good guess for this.
: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 werido.
"""
absolute_path = os.path.abspath(path)
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)
if sys.version[0] == '3':
#: `file://` url for the file.
self.url = pathlib.Path(absolute_path).as_uri()
else:
self.url = urlparse.urljoin('file:', urllib.pathname2url(absolute_path))
with open(path, 'rb') as f:
chunks = parse_chunk(f)
# for __repr__()
self.path = absolute_path
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
f.seek(0)
self.main_list = chunks.children
wavfile.seek(0)
#: :class:`wavinfo.wave_reader.WavAudioFormat`
self.fmt = self._get_format(f)
#: :class:`wavinfo.wave_reader.WavAudioFormat`
self.fmt = self._get_format(wavfile)
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
self.bext = self._get_bext(f, encoding=bext_encoding)
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
self.ixml = self._get_ixml(f)
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
self.ixml = self._get_ixml(wavfile)
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
self.info = self._get_info(f, encoding=info_encoding)
self.data = self._describe_data(f)
#: :class:`wavinfo.wave_axml_reader.WavAxmlReader` with ADM metadata
self.adm = self._get_adm(wavfile)
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
self.info = self._get_info(wavfile, encoding=self.info_encoding)
self.data = self._describe_data()
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)
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor and chunk.ident == ident)
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:
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident),None)
else:
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident))
def _describe_data(self):
data_chunk = next(c for c in self.main_list if type(c) is ChunkDescriptor and c.ident == b'data')
if chunk_descriptor:
return chunk_descriptor.read_data(from_stream)
else:
return None
return WavDataDescriptor(byte_count=data_chunk.length,
frame_count=int(data_chunk.length / self.fmt.block_align))
def _describe_data(self,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)
def _get_format(self, f):
fmt_data = self._find_chunk_data(b'fmt ', f)
# The format chunk is
# audio_format U16
@@ -102,42 +111,39 @@ class WavInfoReader():
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
#0x0001 WAVE_FORMAT_PCM PCM
#0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
#0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
#0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
#0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
# 0x0001 WAVE_FORMAT_PCM PCM
# 0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
# 0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
# 0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
# 0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
#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]
)
# 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]
)
def _get_info(self, f, encoding):
finder = (chunk.signature for chunk in self.main_list \
if type(chunk) is ListChunkDescriptor)
finder = (chunk.signature for chunk in self.main_list if type(chunk) is ListChunkDescriptor)
if b'INFO' in finder:
return WavInfoChunkReader(f, encoding)
def _get_bext(self, f, encoding):
bext_data = self._find_chunk_data(b'bext',f,default_none=True)
if bext_data:
return WavBextReader(bext_data, encoding)
else:
return None
bext_data = self._find_chunk_data(b'bext', f, default_none=True)
return WavBextReader(bext_data, encoding) if bext_data else None
def _get_ixml(self,f):
ixml_data = self._find_chunk_data(b'iXML',f,default_none=True)
if ixml_data is None:
return None
def _get_adm(self, f):
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
ixml_string = ixml_data.rstrip(b'\0')
return WavIXMLFormat(ixml_string)
def _get_ixml(self, f):
ixml_data = self._find_chunk_data(b'iXML', f, default_none=True)
return None if ixml_data is None else WavIXMLFormat(ixml_data.rstrip(b'\0'))
def walk(self):
"""
@@ -145,11 +151,23 @@ class WavInfoReader():
:yields: a string, the :scope: of the metadatum, the string :name: of the
metadata field, and the value.
"""
scopes = ('fmt','data')#,'bext','ixml','info')
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm')
for scope in scopes:
attr = self.__getattribute__(scope)
for field in attr._fields:
yield scope, field, attr.__getattribute__(field)
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)