109 Commits

Author SHA1 Message Date
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
42 changed files with 502 additions and 306 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>

10
.idea/wavinfo.iml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8" 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,27 +0,0 @@
dist: xenial
language: python
python:
# - "2.7"
# - "3.5"
- "3.6"
- "3.7"
- "3.8"
- "3.9"
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)

3
metadata.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Wavinfo
"""

View File

@@ -1,3 +1 @@
lxml~=4.5.2 lxml==4.9.1
setuptools~=49.6.0
ear~=2.0.0

View File

@@ -1,5 +1,5 @@
from setuptools import setup from setuptools import setup
from wavinfo import __author__, __license__, __version__ from .wavinfo import __author__, __license__, __version__
with open("README.md", "r") as fh: with open("README.md", "r") as fh:
long_description = fh.read() long_description = fh.read()
@@ -26,13 +26,12 @@ 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.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.9",
"Programming Language :: Python :: 3.10",
"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', 'ear'], install_requires=['lxml'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'wavinfo = wavinfo.__main__:main' '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.

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.2' __version__ = '2.0.1'
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>' __author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
__license__ = "MIT" __license__ = "MIT"

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,9 +0,0 @@
from lxml import etree as ET
from collections import namedtuple
from ear.fileio.bw64 import Bw64Reader

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,7 +1,7 @@
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'])
@@ -20,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):
@@ -46,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.
@@ -82,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

@@ -9,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')
@@ -27,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)
@@ -71,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))
@@ -116,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'))
@@ -126,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)