120 Commits
v1.6 ... v2.0.2

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
41 changed files with 686 additions and 291 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: on:
release: release:
types: [created] types: [published]
jobs: jobs:
deploy: deploy:
@@ -19,8 +19,18 @@ jobs:
pip install setuptools wheel twine lxml pip install setuptools wheel twine lxml
- name: Build and publish - name: Build and publish
env: env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }}
run: | run: |
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
twine upload dist/* 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/
env.bak/ env.bak/
venv.bak/ venv.bak/
docs_venv/
venv_docs/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@@ -106,3 +108,5 @@ venv.bak/
# vim swap # vim swap
*.swp *.swp
.DS_Store .DS_Store
.vscode/

3
.idea/.gitignore generated vendored
View File

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

View File

@@ -1,9 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="jamiehardt">
<words>
<w>bext</w>
<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.8 (wavinfo)" 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="" vcs="Git" />
</component>
</project>

8
.idea/wavinfo.iml generated
View File

@@ -1,8 +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.8 (wavinfo)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</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,26 +0,0 @@
dist: xenial
language: python
python:
# - "2.7"
- "3.5"
- "3.6"
- "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 lxml"
- "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 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 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

View File

@@ -1,37 +1,39 @@
[![Build Status](https://travis-ci.com/iluvcapra/wavinfo.svg?branch=master)](https://travis-ci.com/iluvcapra/wavinfo)
[![codecov](https://codecov.io/gh/iluvcapra/wavinfo/branch/master/graph/badge.svg)](https://codecov.io/gh/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) [![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)
<!-- ![Test](https://github.com/iluvcapra/wavinfo/workflows/Upload%20Python%20Package/badge.svg) -->
# 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 and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
## Metadata Support
`wavinfo` reads: `wavinfo` reads:
* __Broadcast-WAVE__ metadata<sup>[1][ebu]</sup>, including embedded program * [__Broadcast-WAVE__][ebu] metadata, including embedded program
loudness and coding history, if extant. This also includes the SMPTE UMID<sup>[2][smpte_330m2011]</sup>. loudness and coding history and [__SMPTE UMID__][smpte_330m2011].
* __iXML__ production recorder metadata<sup>[3][ixml]</sup>, including project, scene, and take tags, recorder notes * [__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. and file family information.
* Most of the common __RIFF INFO__<sup>[4][info-tags]</sup> metadata fields. * 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 * The __wav format__ is also parsed, so you can access the basic sample rate and channel count
information. information.
In progress: In progress:
* ADM metadata consilient with the output of the __Dolby RMU__, perhaps later fully complaint with [ITU BS.2076-2][adm]. * [__Dolby RMU__][dolby] metadata and [EBU Tech 3285 Supplement 6][ebu3285s6].
* iXML `STEINBERG` sound library attributes.
* __NetMix__ library attributes.
* Pro Tools __embedded regions__. * 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 [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 [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.info [ixml]:http://www.ixml.info
[eburf64]:https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf [eburf64]:https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
[info-tags]:https://exiftool.org/TagNames/RIFF.html#Info [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.
@@ -41,6 +43,9 @@ 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
``` ```
The package also installs a shell command: The package also installs a shell command:
@@ -49,21 +54,6 @@ The package also installs a shell command:
$ wavinfo test_files/A101_1.WAV $ wavinfo test_files/A101_1.WAV
``` ```
### 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)
(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)
>>> (48000, 2, 6, 24)
```
## Platform Lifecycle Stuff
Python 3.5 support is deprecated.
## Other Resources ## Other Resources
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata). * 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. # You can set these variables from the command line.
SPHINXOPTS = SPHINXOPTS =
SPHINXBUILD = sphinx-build SPHINXBUILD = sphinx-build
SOURCEDIR = . SOURCEDIR = source
BUILDDIR = _build BUILDDIR = _build
# Put it first so that "make" without argument is like "make help". # 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 # Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile %: 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 os
import sys import sys
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath("../../.."))
print(sys.path)
import wavinfo
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = u'wavinfo' project = u'wavinfo'
copyright = u'2019, Jamie Hardt' copyright = u'2022, Jamie Hardt'
author = u'Jamie Hardt' author = u'Jamie Hardt'
# The short X.Y version # The short X.Y version
version = u'' version = wavinfo.__version__
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = u'v1.1' release = wavinfo.__version__
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
@@ -61,7 +64,7 @@ master_doc = 'index'
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # 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 # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.

View File

@@ -6,30 +6,17 @@
Welcome to wavinfo's documentation! 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:: .. toctree::
:maxdepth: 2 :maxdepth: 2
:caption: Notes: :caption: Notes
quickstart
metadata_scopes/adm.rst
metadata_scopes/bext.rst metadata_scopes/bext.rst
metadata_scopes/ixml.rst
metadata_scopes/info.rst metadata_scopes/info.rst
metadata_scopes/ixml.rst
classes
Indices and tables 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 Broadcast WAV Extension
======================= =======================
.. module:: wavinfo
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
:members:
Notes Notes
----- -----
@@ -63,3 +58,12 @@ Result:
Originator Time: 12:40:00 Originator Time: 12:40:00
Time Reference: 2190940753 Time Reference: 2190940753
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch 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 INFO Metadata
============= =============
.. module:: wavinfo
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
:members:
Notes Notes
----- -----
@@ -28,5 +20,12 @@ music library software.
print("INFO Comment:", bullet.info.comment) 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 iXML Production Recorder Metadata
================================= =================================
.. module:: wavinfo
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
:members:
Notes Notes
----- -----
iXML allows an XML document to be embedded in a WAV file. iXML allows an XML document to be embedded in a WAV file.
@@ -41,4 +34,10 @@ Result:
iXML File Family UID: USSDVGR1112089007124001008206300 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)

1
requirements.txt Normal file
View File

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

View File

@@ -26,10 +26,10 @@ setup(name='wavinfo',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Topic :: Multimedia', 'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.8"], "Programming Language :: Python :: 3.11"],
keywords='waveform metadata audio ebu smpte avi library film tv editing editorial', keywords='waveform metadata audio ebu smpte avi library film tv editing editorial',
install_requires=['lxml'], install_requires=['lxml'],
entry_points={ entry_points={

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.

View File

@@ -1,5 +1,4 @@
import os.path import os.path
import sys
from unittest import TestCase from unittest import TestCase
@@ -12,7 +11,7 @@ class TestWaveInfo(TestCase):
def test_sanity(self): def test_sanity(self):
for wav_file in all_files(): for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file) info = wavinfo.WavInfoReader(wav_file)
self.assertEqual(info.__repr__(), 'WavInfoReader(%s, %s, %s)'.format(wav_file, 'latin_1', 'ascii')) self.assertEqual(info.__repr__(), 'WavInfoReader({}, latin_1, ascii)'.format(os.path.abspath(wav_file)))
self.assertIsNotNone(info) self.assertIsNotNone(info)
def test_fmt_against_ffprobe(self): def test_fmt_against_ffprobe(self):
@@ -22,12 +21,12 @@ class TestWaveInfo(TestCase):
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'))
streams = ffprobe_info['streams'][0] streams = ffprobe_info['streams'][0]
byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_raw_sample']) / 8 byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_sample']) / 8
self.assertEqual(info.fmt.byte_rate, byte_rate) self.assertEqual(info.fmt.byte_rate, byte_rate)
def test_data_against_ffprobe(self): def test_data_against_ffprobe(self):

View File

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

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,5 +1,3 @@
from typing import Union
import binascii
from functools import reduce from functools import reduce
@@ -13,7 +11,7 @@ class UMIDParser:
This implementation is based on SMPTE ST 330:2011 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 self.raw_umid = raw_umid
# #
# @property # @property

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,7 +1,7 @@
import struct import struct
import binascii
from .umid_parser import UMIDParser from .umid_parser import UMIDParser
from typing import Optional
class WavBextReader: class WavBextReader:
def __init__(self, bext_data, encoding): def __init__(self, bext_data, encoding):
@@ -16,44 +16,44 @@ class WavBextReader:
rest_starts = struct.calcsize(packstring) rest_starts = struct.calcsize(packstring)
unpacked = struct.unpack(packstring, bext_data[:rest_starts]) unpacked = struct.unpack(packstring, bext_data[:rest_starts])
def sanitize_bytes(b): def sanitize_bytes(b : bytes) -> str:
first_null = next((index for index, byte in enumerate(b) if byte == 0), None) first_null = next((index for index, byte in enumerate(b) if byte == 0), None)
trimmed = b if first_null is None else b[:first_null] trimmed = b if first_null is None else b[:first_null]
decoded = trimmed.decode(encoding) decoded = trimmed.decode(encoding)
return decoded return decoded
#: Description. A free-text field up to 256 characters long. #: Description. A free-text field up to 256 characters long.
self.description = sanitize_bytes(unpacked[0]) self.description : str = sanitize_bytes(unpacked[0])
#: Originator. Usually the name of the encoding application, sometimes #: Originator. Usually the name of the encoding application, sometimes
#: a artist name. #: a artist name.
self.originator = sanitize_bytes(unpacked[1]) self.originator : str = sanitize_bytes(unpacked[1])
#: A unique identifier for the file, a serial number. #: A unique identifier for the file, a serial number.
self.originator_ref = sanitize_bytes(unpacked[2]) self.originator_ref : str = sanitize_bytes(unpacked[2])
#: Date of the recording, in the format YYY-MM-DD #: Date of the recording, in the format YYY-MM-DD
self.originator_date = sanitize_bytes(unpacked[3]) self.originator_date : str = sanitize_bytes(unpacked[3])
#: Time of the recording, in the format HH:MM:SS. #: Time of the recording, in the format HH:MM:SS.
self.originator_time = sanitize_bytes(unpacked[4]) self.originator_time : str = sanitize_bytes(unpacked[4])
#: The sample offset of the start of the file relative to an #: The sample offset of the start of the file relative to an
#: epoch, usually midnight the day of the recording. #: 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 #: A variable-length text field containing a list of processes and
#: and conversions performed on the file. #: and conversions performed on the file.
self.coding_history = sanitize_bytes(bext_data[rest_starts:]) self.coding_history : str = sanitize_bytes(bext_data[rest_starts:])
#: BEXT version. #: 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 #: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
#: may only be 32 bytes long. #: may only be 32 bytes long.
self.umid = None self.umid : Optional[bytes] = None
#: EBU R128 Integrated loudness, in LUFS. #: EBU R128 Integrated loudness, in LUFS.
self.loudness_value = None self.loudness_value : Optional[float] = None
#: EBU R128 Loudness rante, in LUFS. #: EBU R128 Loudness rante, in LUFS.
self.loudness_range = None self.loudness_range : Optional[float] = None
#: True peak level, in dBFS TP #: 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 #: 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. #: EBU R128 Maximum short-term loudness, in LUFS.
self.max_shortterm_loudness = None self.max_shortterm_loudness : Optional[float] = None
if self.version > 0: if self.version > 0:
self.umid = unpacked[7] self.umid = unpacked[7]

View File

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

View File

@@ -1,8 +1,7 @@
# import xml.etree.ElementTree as ET
from lxml import etree as ET from lxml import etree as ET
import io import io
from collections import namedtuple from collections import namedtuple
from typing import Optional
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function']) IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
@@ -21,10 +20,12 @@ class WavIXMLFormat:
parser = ET.XMLParser(recover=True) parser = ET.XMLParser(recover=True)
self.parsed = ET.parse(xml_bytes, parser=parser) 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
@property @property
def raw_xml(self): def raw_xml(self):
@@ -47,35 +48,35 @@ class WavIXMLFormat:
function=track.xpath('string(FUNCTION/text())')) function=track.xpath('string(FUNCTION/text())'))
@property @property
def project(self): def project(self) -> Optional[str]:
""" """
The project/film name entered for the recording. 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. 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. 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. 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 The globally-unique ID for this file family. This may be in the format
of a GUID, or an EBU Rec 9 source identifier, or some other dumb number. of a GUID, or an EBU Rec 9 source identifier, or some other dumb number.
@@ -83,8 +84,18 @@ class WavIXMLFormat:
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. 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
)

View File

@@ -1,7 +1,6 @@
#-*- coding: utf-8 -*- #-*- coding: utf-8 -*-
import struct import struct
import os import os
import sys
from collections import namedtuple from collections import namedtuple
import pathlib import pathlib
@@ -10,6 +9,7 @@ from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
from .wave_ixml_reader import WavIXMLFormat from .wave_ixml_reader import WavIXMLFormat
from .wave_bext_reader import WavBextReader from .wave_bext_reader import WavBextReader
from .wave_info_reader import WavInfoChunkReader from .wave_info_reader import WavInfoChunkReader
from .wave_adm_reader import WavADMReader
#: Calculated statistics about the audio data. #: Calculated statistics about the audio data.
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count') WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
@@ -28,43 +28,62 @@ class WavInfoReader:
""" """
Create a new reader object. 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 weirdo.
""" """
absolute_path = os.path.abspath(path)
#: `file://` url for the file.
self.url = pathlib.Path(absolute_path).as_uri()
# for __repr__()
self.path = absolute_path
self.info_encoding = info_encoding self.info_encoding = info_encoding
self.bext_encoding = bext_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)
with open(path, 'rb') as f: #: `file://` url for the file.
chunks = parse_chunk(f) self.url = pathlib.Path(absolute_path).as_uri()
self.main_list = chunks.children # for __repr__()
f.seek(0) 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)
#: :class:`wavinfo.wave_reader.WavAudioFormat` self.main_list = chunks.children
self.fmt = self._get_format(f) wavfile.seek(0)
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata #: :class:`wavinfo.wave_reader.WavAudioFormat`
self.bext = self._get_bext(f, encoding=bext_encoding) self.fmt = self._get_format(wavfile)
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata #: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
self.ixml = self._get_ixml(f) self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata #: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
self.info = self._get_info(f, encoding=info_encoding) self.ixml = self._get_ixml(wavfile)
self.data = self._describe_data()
#: :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): def _find_chunk_data(self, ident, from_stream, default_none=False):
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 and chunk.ident == ident)
@@ -72,7 +91,7 @@ class WavInfoReader:
return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None
def _describe_data(self): def _describe_data(self):
data_chunk = next(c for c in self.main_list if c.ident == b'data') data_chunk = next(c for c in self.main_list if type(c) is ChunkDescriptor and c.ident == b'data')
return WavDataDescriptor(byte_count=data_chunk.length, return WavDataDescriptor(byte_count=data_chunk.length,
frame_count=int(data_chunk.length / self.fmt.block_align)) frame_count=int(data_chunk.length / self.fmt.block_align))
@@ -117,6 +136,11 @@ class WavInfoReader:
bext_data = self._find_chunk_data(b'bext', f, default_none=True) bext_data = self._find_chunk_data(b'bext', f, default_none=True)
return WavBextReader(bext_data, encoding) if bext_data else None return WavBextReader(bext_data, encoding) if bext_data else 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
def _get_ixml(self, f): def _get_ixml(self, f):
ixml_data = self._find_chunk_data(b'iXML', f, default_none=True) 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')) return None if ixml_data is None else WavIXMLFormat(ixml_data.rstrip(b'\0'))
@@ -127,24 +151,23 @@ class WavInfoReader:
:yields: a string, the :scope: of the metadatum, the string :name: of the :yields: a string, the :scope: of the metadatum, the string :name: of the
metadata field, and the value. metadata field, and the value.
""" """
scopes = ('fmt', 'data') # 'bext', 'ixml', 'info') scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm')
for scope in scopes: for scope in scopes:
attr = self.__getattribute__(scope) if scope in ['fmt', 'data']:
for field in attr._fields: attr = self.__getattribute__(scope)
yield scope, field, attr.__getattribute__(field) for field in attr._fields:
yield scope, field, attr.__getattribute__(field)
if self.bext is not None: else:
bext_dict = (self.bext or {}).to_dict() dict = self.__getattribute__(scope).to_dict() if self.__getattribute__(scope) else {}
for key in bext_dict.keys(): for key in dict.keys():
yield 'bext', key, bext_dict[key] yield scope, key, dict[key]
if self.info is not None:
info_dict = self.info.to_dict()
for key in info_dict.keys():
yield 'info', key, info_dict[key]
def __repr__(self): def __repr__(self):
return 'WavInfoReader(%s, %s, %s)'.format(self.path, self.info_encoding, self.bext_encoding) return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)