mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec42ee1d3d | ||
|
|
bba4d67641 | ||
|
|
4bc7f94198 | ||
|
|
14eb8df496 | ||
|
|
a3aee8e785 | ||
|
|
9e9b6b512b | ||
|
|
a3365c113d | ||
|
|
fbf4d72915 | ||
|
|
90f273cf99 | ||
|
|
bec3d98ee7 | ||
|
|
a87bc71755 | ||
|
|
4817e7eb49 | ||
|
|
07832f7133 | ||
|
|
f3d03296d5 | ||
|
|
9496912b15 | ||
|
|
1077b49ce0 | ||
|
|
90042d57b2 | ||
|
|
f12d7dfea0 | ||
|
|
c2327568a8 | ||
|
|
722df48f9b | ||
|
|
17c0357364 | ||
|
|
7e88c46d54 | ||
|
|
df90c67a73 | ||
|
|
cc107bf65d | ||
|
|
f9c68e0995 | ||
|
|
a98dd2668a | ||
|
|
d8ff4ed63b | ||
|
|
084c3d7ae5 | ||
|
|
e69b71e989 | ||
|
|
6fe6126f3a | ||
|
|
e57c76a722 | ||
|
|
e40a2c5471 | ||
|
|
dec6180744 | ||
|
|
edbe748718 | ||
|
|
2019a4ec63 | ||
|
|
fb43838c7d | ||
|
|
4cd58b8ddd | ||
|
|
38dab7723f | ||
|
|
354d88a5b2 | ||
|
|
d8a405b6d2 | ||
|
|
5f7e467fbd | ||
|
|
3377ddb4b9 | ||
|
|
9cd6cf7f12 | ||
|
|
1f8ebe253b | ||
|
|
fe46d1b242 | ||
|
|
b213933ad8 | ||
|
|
7e314f7475 | ||
|
|
b2d6fd3c92 | ||
|
|
c4d8608c8f | ||
|
|
5605b05f9f | ||
|
|
5d71cabda7 | ||
|
|
c2f87b1fef | ||
|
|
3db40d4f12 | ||
|
|
40b30f5bd8 | ||
|
|
048f20c64c | ||
|
|
6a69df2ee8 | ||
|
|
ec327ee76f | ||
|
|
62a34cfee8 | ||
|
|
c966097e7d | ||
|
|
35311e394d | ||
|
|
0633a8685c | ||
|
|
f65665a06c | ||
|
|
261572cff3 | ||
|
|
623a8569fd | ||
|
|
31493f7cf4 | ||
|
|
4809bb4844 | ||
|
|
a76f3b1518 | ||
|
|
b8cb585d50 | ||
|
|
84a76f9c74 | ||
|
|
5a1a12e21e | ||
|
|
06835ffe11 | ||
|
|
1b25b8214d | ||
|
|
cfc1a451bc | ||
|
|
0788613ea3 | ||
|
|
e9e4b4bcbb | ||
|
|
c114eb7cf3 | ||
|
|
4576d65da6 | ||
|
|
65994db36d | ||
|
|
1a3417d3e5 | ||
|
|
7589d5fb82 | ||
|
|
6014d1d48b | ||
|
|
f8bf6cb4a0 | ||
|
|
6d8e717f42 | ||
|
|
ba232605db | ||
|
|
9a90a0c310 | ||
|
|
add390c0a0 | ||
|
|
7351623e3a | ||
|
|
c23ca4bded | ||
|
|
8fe799b211 | ||
|
|
18ebd22ec1 | ||
|
|
cf8aa36fc3 | ||
|
|
12d16a472f | ||
|
|
39210738e3 | ||
|
|
966da7c4a2 | ||
|
|
5a30ce3afc | ||
|
|
1b9547e8c2 | ||
|
|
1507898b9e | ||
|
|
4f2a6689f5 | ||
|
|
597acb2122 | ||
|
|
9d9592e9e1 | ||
|
|
b0c5a7de72 | ||
|
|
c36b53c5c5 | ||
|
|
25485d9601 | ||
|
|
ffa51eaff4 | ||
|
|
93a9ca0fd3 | ||
|
|
b930fc6d6e | ||
|
|
5f7803fd00 | ||
|
|
e37a37221b | ||
|
|
d0b0b06ecb | ||
|
|
5824406ae6 | ||
|
|
dbbc0683f5 |
13
.coveragerc
13
.coveragerc
@@ -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/*
|
|
||||||
42
.github/workflows/python-package.yml
vendored
Normal file
42
.github/workflows/python-package.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||||
|
|
||||||
|
name: Python Lint and Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
|
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
|
||||||
16
.github/workflows/pythonpublish.yml
vendored
16
.github/workflows/pythonpublish.yml
vendored
@@ -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!
|
||||||
|
|
||||||
|
${{ github.server_url }}/${{ github.repository }}
|
||||||
|
env:
|
||||||
|
MASTODON_URL: ${{ secrets.MASTODON_URL }}
|
||||||
|
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -89,6 +89,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
docs_venv/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
|
|||||||
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
# Default ignored files
|
|
||||||
/workspace.xml
|
|
||||||
8
.idea/dictionaries/jamiehardt.xml
generated
8
.idea/dictionaries/jamiehardt.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<component name="ProjectDictionaryState">
|
|
||||||
<dictionary name="jamiehardt">
|
|
||||||
<words>
|
|
||||||
<w>ident</w>
|
|
||||||
<w>umid</w>
|
|
||||||
</words>
|
|
||||||
</dictionary>
|
|
||||||
</component>
|
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -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
4
.idea/misc.xml
generated
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -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
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
11
.idea/wavinfo.iml
generated
11
.idea/wavinfo.iml
generated
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="jdk" jdkName="Python 3.7" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
<component name="TestRunnerService">
|
|
||||||
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
29
.readthedocs.yaml
Normal file
29
.readthedocs.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-20.04
|
||||||
|
tools:
|
||||||
|
python: "3.10"
|
||||||
|
# You can also specify other tool versions:
|
||||||
|
# nodejs: "16"
|
||||||
|
# rust: "1.55"
|
||||||
|
# golang: "1.17"
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/source/conf.py
|
||||||
|
|
||||||
|
#If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||||
|
formats:
|
||||||
|
- pdf
|
||||||
|
|
||||||
|
#Optionally declare the Python requirements required to build your docs
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: requirements.txt
|
||||||
26
.travis.yml
26
.travis.yml
@@ -1,26 +0,0 @@
|
|||||||
dist: xenial
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
# - "2.7"
|
|
||||||
- "3.6"
|
|
||||||
- "3.5"
|
|
||||||
- "3.7"
|
|
||||||
- "3.8"
|
|
||||||
script:
|
|
||||||
- "gunzip tests/test_files/rf64/*.gz"
|
|
||||||
- "python setup.py test"
|
|
||||||
# - "python -m pytest tests/ -v --cov wavinfo --cov-report term-missing"
|
|
||||||
before_install:
|
|
||||||
- "sudo apt-get update"
|
|
||||||
- "sudo add-apt-repository universe"
|
|
||||||
- "sudo apt-get install -y ffmpeg"
|
|
||||||
- "pip install pytest"
|
|
||||||
- "pip install 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"
|
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"esbonio.sphinx.confDir": ""
|
||||||
|
}
|
||||||
2
LICENSE
2
LICENSE
@@ -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
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -1,33 +1,32 @@
|
|||||||
[](https://travis-ci.com/iluvcapra/wavinfo)
|
|
||||||
[](https://wavinfo.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/wavinfo/) 
|
[](https://wavinfo.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/wavinfo/) 
|
||||||
|
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
|
||||||
|
|
||||||
# wavinfo
|
# wavinfo
|
||||||
|
|
||||||
|
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
|
||||||
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
|
|
||||||
|
|
||||||
`wavinfo` reads:
|
`wavinfo` reads:
|
||||||
|
|
||||||
* __Broadcast-WAVE__ metadata, compliant with [EBU Tech 3285v2 (2011)][ebu], including embedded program
|
* __Broadcast-WAVE__ metadata<sup>[1][ebu]</sup>, including embedded program
|
||||||
loudness and coding history, if extant. This also includes the [SMPTE 330M __UMID__][smpte_330m2011]
|
loudness and coding history, if extant. This also includes the SMPTE UMID<sup>[2][smpte_330m2011]</sup>.
|
||||||
Unique Materials Identifier.
|
* __iXML__ production recorder metadata<sup>[3][ixml]</sup>, including project, scene, and take tags, recorder notes
|
||||||
* [__iXML__ production recorder metadata][ixml], including project, scene, and take tags, recorder notes
|
|
||||||
and file family information.
|
and file family information.
|
||||||
* Most of the common __RIFF INFO__ metadata fields.
|
* Most of the common __RIFF INFO__<sup>[4][info-tags]</sup> 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].
|
||||||
* iXML `STEINBERG` sound library attributes.
|
* iXML `STEINBERG` sound library attributes.
|
||||||
|
* __NetMix__ library attributes.
|
||||||
* Pro Tools __embedded regions__.
|
* Pro Tools __embedded regions__.
|
||||||
|
|
||||||
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
|
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.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
|
||||||
|
|
||||||
|
|
||||||
## Demonstration
|
## Demonstration
|
||||||
|
|
||||||
@@ -61,7 +60,3 @@ The length of the file in frames (interleaved samples) and bytes is available, a
|
|||||||
## 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).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
25
docs/requirements.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
alabaster==0.7.12
|
||||||
|
Babel==2.11.0
|
||||||
|
certifi==2022.9.24
|
||||||
|
charset-normalizer==2.1.1
|
||||||
|
docutils==0.17.1
|
||||||
|
idna==3.4
|
||||||
|
imagesize==1.4.1
|
||||||
|
Jinja2==3.1.2
|
||||||
|
lxml==4.9.1
|
||||||
|
MarkupSafe==2.1.1
|
||||||
|
packaging==21.3
|
||||||
|
Pygments==2.13.0
|
||||||
|
pyparsing==3.0.9
|
||||||
|
pytz==2022.6
|
||||||
|
requests==2.28.1
|
||||||
|
snowballstemmer==2.2.0
|
||||||
|
Sphinx==5.3.0
|
||||||
|
sphinx-rtd-theme==1.1.1
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
|
urllib3==1.26.12
|
||||||
18
docs/source/classes.rst
Normal file
18
docs/source/classes.rst
Normal 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:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -14,19 +14,21 @@
|
|||||||
#
|
#
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
# -- 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 = u''
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = u'v1.1'
|
release = u'v1.7.1'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
@@ -61,7 +63,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.
|
||||||
@@ -6,30 +6,15 @@
|
|||||||
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
|
||||||
|
|
||||||
metadata_scopes/bext.rst
|
metadata_scopes/bext.rst
|
||||||
metadata_scopes/ixml.rst
|
metadata_scopes/ixml.rst
|
||||||
metadata_scopes/info.rst
|
metadata_scopes/info.rst
|
||||||
|
|
||||||
|
classes
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
Broadcast WAV Extension
|
Broadcast WAV Extension
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
.. module:: wavinfo
|
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
@@ -63,3 +58,14 @@ 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
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
INFO Metadata
|
INFO Metadata
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
||||||
.. module:: wavinfo
|
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -28,5 +20,14 @@ music library software.
|
|||||||
print("INFO Comment:", bullet.info.comment)
|
print("INFO Comment:", bullet.info.comment)
|
||||||
|
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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,12 @@ Result:
|
|||||||
iXML File Family UID: USSDVGR1112089007124001008206300
|
iXML File Family UID: USSDVGR1112089007124001008206300
|
||||||
|
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
6
metadata.py
Normal file
6
metadata.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"""
|
||||||
|
Wavinfo
|
||||||
|
"""
|
||||||
|
__version__ = '1.7.2'
|
||||||
|
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
|
||||||
|
__license__ = "MIT"
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lxml==4.9.1
|
||||||
9
setup.py
9
setup.py
@@ -1,5 +1,5 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
from wavinfo import __author__, __license__, __version__
|
from metadata 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,10 +26,9 @@ 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"],
|
|
||||||
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={
|
||||||
|
|||||||
BIN
tests/test_files/protools/umid.wav
Normal file
BIN
tests/test_files/protools/umid.wav
Normal file
Binary file not shown.
25
tests/test_walk.py
Normal file
25
tests/test_walk.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import unittest
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
|
||||||
|
class TestWalk(unittest.TestCase):
|
||||||
|
def test_walk_metadata(self):
|
||||||
|
test_file = 'tests/test_files/protools/PT A101_4.A1.wav'
|
||||||
|
info = wavinfo.WavInfoReader(test_file)
|
||||||
|
|
||||||
|
tested_data , tested_format = False, False
|
||||||
|
for scope, key, value in info.walk():
|
||||||
|
if scope == 'fmt':
|
||||||
|
if key == 'channel_count':
|
||||||
|
tested_format = True
|
||||||
|
self.assertEqual(value, 2)
|
||||||
|
if scope == 'data':
|
||||||
|
if key == 'frame_count':
|
||||||
|
tested_data = True
|
||||||
|
self.assertEqual(value, 144140)
|
||||||
|
|
||||||
|
self.assertTrue(tested_data and tested_format)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -6,34 +6,35 @@ from unittest import TestCase
|
|||||||
from .utils import all_files, ffprobe
|
from .utils import all_files, ffprobe
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
|
|
||||||
class TestWaveInfo(TestCase):
|
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.assertTrue(info is not None)
|
self.assertEqual(info.__repr__(), 'WavInfoReader({}, latin_1, ascii)'.format(os.path.abspath(wav_file)))
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
|
||||||
def test_fmt_against_ffprobe(self):
|
def test_fmt_against_ffprobe(self):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
ffprobe_info = ffprobe(wav_file)
|
ffprobe_info = ffprobe(wav_file)
|
||||||
|
|
||||||
self.assertEqual( info.fmt.channel_count , ffprobe_info['streams'][0]['channels'] )
|
self.assertEqual(info.fmt.channel_count, ffprobe_info['streams'][0]['channels'])
|
||||||
self.assertEqual( info.fmt.sample_rate , int(ffprobe_info['streams'][0]['sample_rate']) )
|
self.assertEqual(info.fmt.sample_rate, int(ffprobe_info['streams'][0]['sample_rate']))
|
||||||
self.assertEqual( info.fmt.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_raw_sample']) )
|
self.assertEqual(info.fmt.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_raw_sample']))
|
||||||
|
|
||||||
if info.fmt.audio_format == 1:
|
if info.fmt.audio_format == 1:
|
||||||
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm') )
|
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm'))
|
||||||
byte_rate = int(ffprobe_info['streams'][0]['sample_rate']) \
|
streams = ffprobe_info['streams'][0]
|
||||||
* ffprobe_info['streams'][0]['channels'] \
|
byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_raw_sample']) / 8
|
||||||
* int(ffprobe_info['streams'][0]['bits_per_raw_sample']) / 8
|
self.assertEqual(info.fmt.byte_rate, byte_rate)
|
||||||
self.assertEqual( info.fmt.byte_rate , byte_rate )
|
|
||||||
|
|
||||||
def test_data_against_ffprobe(self):
|
def test_data_against_ffprobe(self):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
ffprobe_info = ffprobe(wav_file)
|
ffprobe_info = ffprobe(wav_file)
|
||||||
self.assertEqual( info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts'] ))
|
self.assertEqual(info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts']))
|
||||||
|
|
||||||
def test_bext_against_ffprobe(self):
|
def test_bext_against_ffprobe(self):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
@@ -41,56 +42,73 @@ class TestWaveInfo(TestCase):
|
|||||||
ffprobe_info = ffprobe(wav_file)
|
ffprobe_info = ffprobe(wav_file)
|
||||||
if info.bext:
|
if info.bext:
|
||||||
if 'comment' in ffprobe_info['format']['tags']:
|
if 'comment' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual( info.bext.description, ffprobe_info['format']['tags']['comment'] )
|
self.assertEqual(info.bext.description, ffprobe_info['format']['tags']['comment'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual( info.bext.description , '')
|
self.assertEqual(info.bext.description, '')
|
||||||
|
|
||||||
if 'encoded_by' in ffprobe_info['format']['tags']:
|
if 'encoded_by' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual( info.bext.originator, ffprobe_info['format']['tags']['encoded_by'] )
|
self.assertEqual(info.bext.originator, ffprobe_info['format']['tags']['encoded_by'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual( info.bext.originator, '')
|
self.assertEqual(info.bext.originator, '')
|
||||||
|
|
||||||
if 'originator_reference' in ffprobe_info['format']['tags']:
|
if 'originator_reference' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual( info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'] )
|
self.assertEqual(info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual( info.bext.originator_ref, '')
|
self.assertEqual(info.bext.originator_ref, '')
|
||||||
|
|
||||||
# these don't always reflect the bext info
|
# these don't always reflect the bext info
|
||||||
# self.assertEqual( info.bext.originator_date, ffprobe_info['format']['tags']['date'] )
|
# self.assertEqual(info.bext.originator_date, ffprobe_info['format']['tags']['date'])
|
||||||
# self.assertEqual( info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'] )
|
# self.assertEqual(info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'])
|
||||||
self.assertEqual( info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']) )
|
self.assertEqual(info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']))
|
||||||
|
|
||||||
if 'coding_history' in ffprobe_info['format']['tags']:
|
if 'coding_history' in ffprobe_info['format']['tags']:
|
||||||
self.assertEqual( info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'] )
|
self.assertEqual(info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'])
|
||||||
else:
|
else:
|
||||||
self.assertEqual( info.bext.coding_history, '' )
|
self.assertEqual(info.bext.coding_history, '')
|
||||||
|
|
||||||
def test_ixml(self):
|
def test_ixml(self):
|
||||||
expected = {'A101_4.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '4',
|
expected = {'A101_4.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '4',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
|
||||||
'A101_3.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '3',
|
'A101_3.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '3',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
|
||||||
'A101_2.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '2',
|
'A101_2.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '2',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
|
||||||
'A101_1.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '1',
|
'A101_1.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '1',
|
||||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
|
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
|
||||||
}
|
}
|
||||||
|
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
basename = os.path.basename(wav_file)
|
basename = os.path.basename(wav_file)
|
||||||
if basename in expected:
|
if basename in expected:
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
e = expected[basename]
|
e = expected[basename]
|
||||||
|
|
||||||
self.assertEqual( e['project'], info.ixml.project )
|
self.assertEqual(e['project'], info.ixml.project)
|
||||||
self.assertEqual( e['scene'], info.ixml.scene )
|
self.assertEqual(e['scene'], info.ixml.scene)
|
||||||
self.assertEqual( e['take'], info.ixml.take )
|
self.assertEqual(e['take'], info.ixml.take)
|
||||||
self.assertEqual( e['tape'], info.ixml.tape )
|
self.assertEqual(e['tape'], info.ixml.tape)
|
||||||
self.assertEqual( e['family_uid'], info.ixml.family_uid )
|
self.assertEqual(e['family_uid'], info.ixml.family_uid)
|
||||||
|
|
||||||
for track in info.ixml.track_list:
|
for track in info.ixml.track_list:
|
||||||
self.assertIsNotNone(track.channel_index)
|
self.assertIsNotNone(track.channel_index)
|
||||||
if basename == 'A101_4.WAV' and track.channel_index == '1':
|
if basename == 'A101_4.WAV' and track.channel_index == '1':
|
||||||
self.assertTrue(track.name == 'MKH516 A')
|
self.assertEqual(track.name, 'MKH516 A')
|
||||||
|
|
||||||
|
def test_metadata(self):
|
||||||
|
file_with_metadata = 'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
||||||
|
self.assertTrue(os.path.exists(file_with_metadata))
|
||||||
|
info = wavinfo.WavInfoReader(file_with_metadata).info
|
||||||
|
self.assertEqual(info.title, 'camera bumb 1')
|
||||||
|
self.assertEqual(info.artist, 'Jamie Hardt')
|
||||||
|
self.assertEqual(info.copyright, '© 2010 Jamie Hardt')
|
||||||
|
self.assertEqual(info.product, 'Test Sounds') # album
|
||||||
|
self.assertEqual(info.album, info.product)
|
||||||
|
self.assertEqual(info.comment, 'Comments')
|
||||||
|
self.assertEqual(info.software, 'Sound Grinder Pro')
|
||||||
|
self.assertEqual(info.created_date, '2010-12-28')
|
||||||
|
self.assertEqual(info.engineer, 'JPH')
|
||||||
|
self.assertEqual(info.keywords, 'Sound Effect, movement, microphone, bump')
|
||||||
|
self.assertEqual(info.title, 'camera bumb 1')
|
||||||
|
self.assertEqual(type(info.to_dict()), dict)
|
||||||
|
self.assertEqual(type(info.__repr__()), str)
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ from unittest import TestCase
|
|||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
|
|
||||||
class TestZoomF8(TestCase):
|
class TestZoomF8(TestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import subprocess
|
|||||||
from subprocess import PIPE
|
from subprocess import PIPE
|
||||||
import json
|
import json
|
||||||
|
|
||||||
FFPROBE='ffprobe'
|
FFPROBE = 'ffprobe'
|
||||||
|
|
||||||
|
|
||||||
def ffprobe(path):
|
def ffprobe(path):
|
||||||
arguments = [ FFPROBE , "-of", "json" , "-show_format", "-show_streams", path ]
|
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
|
||||||
if int(sys.version[0]) < 3:
|
if int(sys.version[0]) < 3:
|
||||||
process = subprocess.Popen(arguments, stdout=PIPE)
|
process = subprocess.Popen(arguments, stdout=PIPE)
|
||||||
process.wait()
|
process.wait()
|
||||||
@@ -27,13 +28,9 @@ def ffprobe(path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def all_files():
|
def all_files():
|
||||||
for dirpath, _, filenames in os.walk('tests/test_files'):
|
for dirpath, _, filenames in os.walk('tests/test_files'):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
_, ext = os.path.splitext(filename)
|
_, ext = os.path.splitext(filename)
|
||||||
if ext in ['.wav','.WAV']:
|
if ext in ['.wav', '.WAV']:
|
||||||
yield os.path.join(dirpath, filename)
|
yield os.path.join(dirpath, filename)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,3 @@ 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.5'
|
|
||||||
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
|
|
||||||
__license__ = "MIT"
|
|
||||||
@@ -4,6 +4,7 @@ from . import WavInfoReader
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
|
||||||
@@ -28,5 +29,6 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
188
wavinfo/dolby_parser.py
Normal file
188
wavinfo/dolby_parser.py
Normal 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...
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
import struct
|
import struct
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from . import riff_parser
|
from . import riff_parser
|
||||||
|
|
||||||
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
|
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
|
||||||
|
|
||||||
|
|
||||||
def parse_rf64(stream):
|
def parse_rf64(stream, signature = b'RF64'):
|
||||||
#print("starting parse_rf64")
|
# print("starting parse_rf64")
|
||||||
start = stream.tell()
|
start = stream.tell()
|
||||||
assert( stream.read(4) == b'WAVE' )
|
assert( stream.read(4) == b'WAVE' )
|
||||||
|
|
||||||
ds64_chunk = riff_parser.parse_chunk(stream)
|
ds64_chunk = riff_parser.parse_chunk(stream)
|
||||||
|
|
||||||
ds64_field_spec = "<QQQI"
|
ds64_field_spec = "<QQQI"
|
||||||
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
||||||
assert(ds64_chunk.ident == b'ds64')
|
assert(ds64_chunk.ident == b'ds64')
|
||||||
|
|
||||||
ds64_data = ds64_chunk.read_data(stream)
|
ds64_data = ds64_chunk.read_data(stream)
|
||||||
assert(len(ds64_data) >= ds64_fields_size )
|
assert(len(ds64_data) >= ds64_fields_size )
|
||||||
|
|
||||||
#print("Read ds64 chunk: len()",len(ds64_data))
|
# print("Read ds64 chunk: len()",len(ds64_data))
|
||||||
riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec , ds64_data[0:ds64_fields_size] )
|
riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec , ds64_data[0:ds64_fields_size] )
|
||||||
|
|
||||||
bigchunk_table = {}
|
bigchunk_table = {}
|
||||||
chunksize64format = "<4sL"
|
chunksize64format = "<4sL"
|
||||||
chunksize64size = struct.calcsize(chunksize64format)
|
chunksize64size = struct.calcsize(chunksize64format)
|
||||||
#print("Found chunks64s:", length_lookup_table)
|
# print("Found chunks64s:", length_lookup_table)
|
||||||
|
|
||||||
for n in range(length_lookup_table):
|
for n in range(length_lookup_table):
|
||||||
bigname, bigsize = struct.unpack_from( chunksize64format , ds64data, offset= ds64_fields_size )
|
bigname, bigsize = struct.unpack_from( chunksize64format , ds64_data, offset= ds64_fields_size )
|
||||||
bigchunk_table[bigname] = bigsize
|
bigchunk_table[bigname] = bigsize
|
||||||
|
|
||||||
bigchunk_table[b'data'] = data_size
|
bigchunk_table[b'data'] = data_size
|
||||||
bigchunk_table[b'RF64'] = riff_size
|
bigchunk_table[signature] = riff_size
|
||||||
|
|
||||||
stream.seek(start, 0)
|
stream.seek(start, 0)
|
||||||
#print("returning from parse_rf64, context: ",RF64Context( sample_count=sample_count, bigchunk_table=bigchunk_table ) )
|
# print("returning from parse_rf64, context: ", RF64Context(sample_count=sample_count, bigchunk_table=bigchunk_table))
|
||||||
return RF64Context( sample_count=sample_count, bigchunk_table=bigchunk_table )
|
return RF64Context( sample_count=sample_count, bigchunk_table=bigchunk_table )
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ class WavInfoEOFError(EOFError):
|
|||||||
|
|
||||||
|
|
||||||
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
|
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
|
||||||
def find(self, chunk_path):
|
pass
|
||||||
if len(chunk_path) > 1:
|
# def find(self, chunk_path):
|
||||||
for chunk in self.children:
|
# if len(chunk_path) > 1:
|
||||||
if type(chunk) is ListChunkDescriptor and \
|
# for chunk in self.children:
|
||||||
chunk.signature is chunk_path[0]:
|
# if type(chunk) is ListChunkDescriptor and \
|
||||||
return chunk.find(chunk_path[1:])
|
# chunk.signature is chunk_path[0]:
|
||||||
else:
|
# return chunk.find(chunk_path[1:])
|
||||||
for chunk in self.children:
|
# else:
|
||||||
if type(chunk) is ChunkDescriptor and \
|
# for chunk in self.children:
|
||||||
chunk.ident is chunk_path[0]:
|
# if type(chunk) is ChunkDescriptor and \
|
||||||
return chunk
|
# chunk.ident is chunk_path[0]:
|
||||||
|
# return chunk
|
||||||
|
|
||||||
|
|
||||||
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
|
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
|
||||||
@@ -35,7 +36,7 @@ def parse_list_chunk(stream, length, rf64_context=None):
|
|||||||
signature = stream.read(4)
|
signature = stream.read(4)
|
||||||
|
|
||||||
children = []
|
children = []
|
||||||
while (stream.tell() - start + 8) < length:
|
while stream.tell() - start + 8 < length:
|
||||||
child_chunk = parse_chunk(stream, rf64_context=rf64_context)
|
child_chunk = parse_chunk(stream, rf64_context=rf64_context)
|
||||||
children.append(child_chunk)
|
children.append(child_chunk)
|
||||||
|
|
||||||
@@ -55,16 +56,16 @@ def parse_chunk(stream, rf64_context=None):
|
|||||||
data_size = struct.unpack('<I', size_bytes)[0]
|
data_size = struct.unpack('<I', size_bytes)[0]
|
||||||
|
|
||||||
if data_size == 0xFFFFFFFF:
|
if data_size == 0xFFFFFFFF:
|
||||||
if rf64_context is None and ident == b'RF64':
|
if rf64_context is None and ident in {b'RF64', b'BW64'}:
|
||||||
rf64_context = parse_rf64(stream=stream)
|
rf64_context = parse_rf64(stream=stream, signature=ident)
|
||||||
|
|
||||||
data_size = rf64_context.bigchunk_table[ident]
|
|
||||||
|
|
||||||
displacement = data_size
|
|
||||||
if displacement % 2 is not 0:
|
|
||||||
displacement = displacement + 1
|
|
||||||
|
|
||||||
if ident in [b'RIFF', b'LIST', b'RF64']:
|
data_size = rf64_context.bigchunk_table[ident]
|
||||||
|
|
||||||
|
displacement = data_size
|
||||||
|
if displacement % 2:
|
||||||
|
displacement += 1
|
||||||
|
|
||||||
|
if ident in {b'RIFF', b'LIST', b'RF64', b'BW64'}:
|
||||||
return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
|
return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
|
||||||
else:
|
else:
|
||||||
data_start = stream.tell()
|
data_start = stream.tell()
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import struct
|
from functools import reduce
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
|
def binary_to_string(binary_value):
|
||||||
|
return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
|
||||||
|
|
||||||
|
|
||||||
class UMIDParser:
|
class UMIDParser:
|
||||||
"""
|
"""
|
||||||
@@ -7,123 +11,111 @@ 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
|
||||||
|
#
|
||||||
@classmethod
|
# @property
|
||||||
def binary_to_string(cls, binary_value):
|
# def universal_label(self) -> bytearray:
|
||||||
result_str = ''
|
# return self.raw_umid[0:12]
|
||||||
for n in range(len(binary_value)):
|
#
|
||||||
result_str = f'{binary_value[n]:x}' + result_str
|
# @property
|
||||||
|
# def basic_umid(self):
|
||||||
return result_str
|
# return self.raw_umid[0:32]
|
||||||
|
|
||||||
@property
|
|
||||||
def universal_label(self) -> bytearray:
|
|
||||||
return self.raw_umid[0:12]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def basic_umid(self):
|
|
||||||
return self.raw_umid[0:32]
|
|
||||||
|
|
||||||
def basic_umid_to_str(self):
|
def basic_umid_to_str(self):
|
||||||
return "%024x-%06x-%032x" % (self.binary_to_string(self.universal_label),
|
return binary_to_string(self.raw_umid[0:32])
|
||||||
self.binary_to_string(self.instance_number),
|
#
|
||||||
self.binary_to_string(self.material_number))
|
# @property
|
||||||
|
# def universal_label_is_valid(self) -> bool:
|
||||||
@property
|
# valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
|
||||||
def universal_label_is_valid(self) -> bool:
|
# return self.universal_label[0:len(valid_preamble)] == valid_preamble
|
||||||
valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
|
#
|
||||||
return self.universal_label[0:len(valid_preamble)] == valid_preamble
|
# @property
|
||||||
|
# def material_type(self) -> str:
|
||||||
@property
|
# material_byte = self.raw_umid[10]
|
||||||
def material_type(self) -> str:
|
# if material_byte == 0x1:
|
||||||
material_byte = self.raw_umid[10]
|
# return 'picture'
|
||||||
if material_byte == 0x1:
|
# elif material_byte == 0x2:
|
||||||
return 'picture'
|
# return 'audio'
|
||||||
elif material_byte == 0x2:
|
# elif material_byte == 0x3:
|
||||||
return 'audio'
|
# return 'data'
|
||||||
elif material_byte == 0x3:
|
# elif material_byte == 0x4:
|
||||||
return 'data'
|
# return 'other'
|
||||||
elif material_byte == 0x4:
|
# elif material_byte == 0x5:
|
||||||
return 'other'
|
# return 'picture_single_component'
|
||||||
elif material_byte == 0x5:
|
# elif material_byte == 0x6:
|
||||||
return 'picture_single_component'
|
# return 'picture_multiple_component'
|
||||||
elif material_byte == 0x6:
|
# elif material_byte == 0x7:
|
||||||
return 'picture_multiple_component'
|
# return 'audio_single_component'
|
||||||
elif material_byte == 0x7:
|
# elif material_byte == 0x9:
|
||||||
return 'audio_single_component'
|
# return 'audio_multiple_component'
|
||||||
elif material_byte == 0x9:
|
# elif material_byte == 0xb:
|
||||||
return 'audio_multiple_component'
|
# return 'auxiliary_single_component'
|
||||||
elif material_byte == 0xb:
|
# elif material_byte == 0xc:
|
||||||
return 'auxiliary_single_component'
|
# return 'auxiliary_multiple_component'
|
||||||
elif material_byte == 0xc:
|
# elif material_byte == 0xd:
|
||||||
return 'auxiliary_multiple_component'
|
# return 'mixed_components'
|
||||||
elif material_byte == 0xd:
|
# elif material_byte == 0xf:
|
||||||
return 'mixed_components'
|
# return 'not_identified'
|
||||||
elif material_byte == 0xf:
|
# else:
|
||||||
return 'not_identified'
|
# return 'not_recognized'
|
||||||
else:
|
#
|
||||||
return 'not_recognized'
|
# @property
|
||||||
|
# def material_number_creation_method(self) -> str:
|
||||||
@property
|
# method_byte = self.raw_umid[11]
|
||||||
def material_number_creation_method(self) -> str:
|
# method_byte = (method_byte << 4) & 0xf
|
||||||
method_byte = self.raw_umid[11]
|
# if method_byte == 0x0:
|
||||||
method_byte = (method_byte << 4) & 0xf
|
# return 'undefined'
|
||||||
if method_byte == 0x0:
|
# elif method_byte == 0x1:
|
||||||
return 'undefined'
|
# return 'smpte'
|
||||||
elif method_byte == 0x1:
|
# elif method_byte == 0x2:
|
||||||
return 'smpte'
|
# return 'uuid'
|
||||||
elif method_byte == 0x2:
|
# elif method_byte == 0x3:
|
||||||
return 'uuid'
|
# return 'masked'
|
||||||
elif method_byte == 0x3:
|
# elif method_byte == 0x4:
|
||||||
return 'masked'
|
# return 'ieee1394'
|
||||||
elif method_byte == 0x4:
|
# elif 0x5 <= method_byte <= 0x7:
|
||||||
return 'ieee1394'
|
# return 'reserved_undefined'
|
||||||
elif 0x5 <= method_byte <= 0x7:
|
# else:
|
||||||
return 'reserved_undefined'
|
# return 'unrecognized'
|
||||||
else:
|
#
|
||||||
return 'unrecognized'
|
# @property
|
||||||
|
# def instance_number_creation_method(self) -> str:
|
||||||
@property
|
# method_byte = self.raw_umid[11]
|
||||||
def instance_number_creation_method(self) -> str:
|
# method_byte = method_byte & 0xf
|
||||||
method_byte = self.raw_umid[11]
|
# if method_byte == 0x0:
|
||||||
method_byte = method_byte & 0xf
|
# return 'undefined'
|
||||||
if method_byte == 0x0:
|
# elif method_byte == 0x01:
|
||||||
return 'undefined'
|
# return 'local_registration'
|
||||||
elif method_byte == 0x01:
|
# elif method_byte == 0x02:
|
||||||
return 'local_registration'
|
# return '24_bit_prs'
|
||||||
elif method_byte == 0x02:
|
# elif method_byte == 0x03:
|
||||||
return '24_bit_prs'
|
# return 'copy_number_and_16_bit_prs'
|
||||||
elif method_byte == 0x03:
|
# elif 0x04 <= method_byte <= 0x0e:
|
||||||
return 'copy_number_and_16_bit_prs'
|
# return 'reserved_undefined'
|
||||||
elif 0x04 <= method_byte <= 0x0e:
|
# elif method_byte == 0x0f:
|
||||||
return 'reserved_undefined'
|
# return 'live_stream'
|
||||||
elif method_byte == 0x0f:
|
# else:
|
||||||
return 'live_stream'
|
# return 'unrecognized'
|
||||||
else:
|
#
|
||||||
return 'unrecognized'
|
# @property
|
||||||
|
# def indicated_length(self) -> str:
|
||||||
@property
|
# if self.raw_umid[12] == 0x13:
|
||||||
def indicated_length(self) -> str:
|
# return 'basic'
|
||||||
if self.raw_umid[12] == 0x13:
|
# elif self.raw_umid[12] == 0x33:
|
||||||
return 'basic'
|
# return 'extended'
|
||||||
elif self.raw_umid[12] == 0x33:
|
#
|
||||||
return 'extended'
|
# @property
|
||||||
|
# def instance_number(self) -> bytearray:
|
||||||
@property
|
# return self.raw_umid[13:3]
|
||||||
def instance_number(self) -> bytearray:
|
#
|
||||||
return self.raw_umid[13:3]
|
# @property
|
||||||
|
# def material_number(self) -> bytearray:
|
||||||
@property
|
# return self.raw_umid[16:16]
|
||||||
def material_number(self) -> bytearray:
|
#
|
||||||
return self.raw_umid[16:16]
|
# @property
|
||||||
|
# def source_pack(self) -> Union[bytearray, None]:
|
||||||
@property
|
# if self.indicated_length == 'extended':
|
||||||
def source_pack(self) -> Union[bytearray, None]:
|
# return self.raw_umid[32:32]
|
||||||
if self.indicated_length == 'extended':
|
# else:
|
||||||
return self.raw_umid[32:32]
|
# return None
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
wavinfo/wave_axml_reader.py
Normal file
9
wavinfo/wave_axml_reader.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from lxml import etree as ET
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from ear.fileio.bw64 import Bw64Reader
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,85 +1,89 @@
|
|||||||
import struct
|
import struct
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
Read Broadcast-WAV extended metadata.
|
Read Broadcast-WAV extended metadata.
|
||||||
:param best_data: The bytes-like data.
|
:param bext_data: The bytes-like data.
|
||||||
"param encoding: The encoding to use when decoding the text fields of the
|
:param encoding: The encoding to use when decoding the text fields of the
|
||||||
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
|
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
|
||||||
"""
|
"""
|
||||||
packstring = "<256s"+ "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
||||||
|
|
||||||
rest_starts = struct.calcsize(packstring)
|
rest_starts = struct.calcsize(packstring)
|
||||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||||
|
|
||||||
def sanatize_bytes(bytes):
|
def sanitize_bytes(b : bytes) -> str:
|
||||||
first_null = next( (index for index, byte in enumerate(bytes) if byte == 0 ), None )
|
first_null = next((index for index, byte in enumerate(b) if byte == 0), None)
|
||||||
if first_null is not None:
|
trimmed = b if first_null is None else b[:first_null]
|
||||||
trimmed = bytes[:first_null]
|
|
||||||
else:
|
|
||||||
trimmed = bytes
|
|
||||||
|
|
||||||
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 = sanatize_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 = sanatize_bytes(unpacked[1])
|
self.originator : str = sanitize_bytes(unpacked[1])
|
||||||
#: A unique identifer for the file, a serial number.
|
#: A unique identifier for the file, a serial number.
|
||||||
self.originator_ref = sanatize_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 = sanatize_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 = sanatize_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 = sanatize_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]
|
||||||
|
|
||||||
if self.version > 1:
|
if self.version > 1:
|
||||||
self.loudness_value = unpacked[8] / 100.0
|
self.loudness_value = unpacked[8] / 100.0
|
||||||
self.loudness_range = unpacked[9] / 100.0
|
self.loudness_range = unpacked[9] / 100.0
|
||||||
self.max_true_peak = unpacked[10] / 100.0
|
self.max_true_peak = unpacked[10] / 100.0
|
||||||
self.max_momentary_loudness = unpacked[11] / 100.0
|
self.max_momentary_loudness = unpacked[11] / 100.0
|
||||||
self.max_shortterm_loudness = unpacked[12] / 100.0
|
self.max_shortterm_loudness = unpacked[12] / 100.0
|
||||||
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'description': self.description,
|
if self.umid is not None:
|
||||||
'originator': self.originator,
|
umid_parsed = UMIDParser(self.umid)
|
||||||
'originator_ref': self.originator_ref,
|
umid_str = umid_parsed.basic_umid_to_str()
|
||||||
'originator_date': self.originator_date,
|
else:
|
||||||
'originator_time': self.originator_time,
|
umid_str = None
|
||||||
'time_reference': self.time_reference,
|
|
||||||
'version': self.version,
|
|
||||||
'coding_history': self.coding_history,
|
|
||||||
'loudness_value': self.loudness_value,
|
|
||||||
'loudness_range': self.loudness_range,
|
|
||||||
'max_true_peak': self.max_true_peak,
|
|
||||||
'max_momentary_loudness': self.max_momentary_loudness,
|
|
||||||
'max_shortterm_loudness': self.max_shortterm_loudness
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return {'description': self.description,
|
||||||
|
'originator': self.originator,
|
||||||
|
'originator_ref': self.originator_ref,
|
||||||
|
'originator_date': self.originator_date,
|
||||||
|
'originator_time': self.originator_time,
|
||||||
|
'time_reference': self.time_reference,
|
||||||
|
'version': self.version,
|
||||||
|
'umid': umid_str,
|
||||||
|
'coding_history': self.coding_history,
|
||||||
|
'loudness_value': self.loudness_value,
|
||||||
|
'loudness_range': self.loudness_range,
|
||||||
|
'max_true_peak': self.max_true_peak,
|
||||||
|
'max_momentary_loudness': self.max_momentary_loudness,
|
||||||
|
'max_shortterm_loudness': self.max_shortterm_loudness
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
from .riff_parser import parse_chunk, ListChunkDescriptor
|
from .riff_parser import parse_chunk, ListChunkDescriptor
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
class WavInfoChunkReader:
|
class WavInfoChunkReader:
|
||||||
|
|
||||||
def __init__(self, f, encoding):
|
def __init__(self, f, encoding):
|
||||||
@@ -9,53 +10,47 @@ class WavInfoChunkReader:
|
|||||||
f.seek(0)
|
f.seek(0)
|
||||||
parsed_chunks = parse_chunk(f)
|
parsed_chunks = parse_chunk(f)
|
||||||
|
|
||||||
list_chunks = [chunk for chunk in parsed_chunks.children \
|
list_chunks = [chunk for chunk in parsed_chunks.children if type(chunk) is ListChunkDescriptor]
|
||||||
if type(chunk) is ListChunkDescriptor]
|
|
||||||
|
self.info_chunk = next((chunk for chunk in list_chunks if chunk.signature == b'INFO'), None)
|
||||||
|
|
||||||
self.info_chunk = next((chunk for chunk in list_chunks \
|
|
||||||
if chunk.signature == b'INFO'), None)
|
|
||||||
|
|
||||||
#: 'ICOP' Copyright
|
#: 'ICOP' Copyright
|
||||||
self.copyright = 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 : 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')
|
||||||
#: 'ISFT' Software
|
|
||||||
self.software = self._get_field(f,b'ISFT')
|
|
||||||
#: '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) -> Optional[str]:
|
||||||
def _get_field(self, f, field_ident):
|
search = next(((chunk.start, chunk.length) for chunk in self.info_chunk.children if chunk.ident == field_ident),
|
||||||
|
None)
|
||||||
search = next( ( (chunk.start, chunk.length) for chunk in self.info_chunk.children \
|
|
||||||
if chunk.ident == field_ident ), None)
|
|
||||||
|
|
||||||
if search is not None:
|
if search is not None:
|
||||||
f.seek(search[0])
|
f.seek(search[0])
|
||||||
@@ -64,32 +59,30 @@ class WavInfoChunkReader:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
A dictionary with all of the key/values read from the INFO scope.
|
A dictionary with all of the key/values read from the INFO scope.
|
||||||
"""
|
"""
|
||||||
return {'copyright': self.copyright,
|
return {'copyright': self.copyright,
|
||||||
'product': self.product,
|
'product': self.product,
|
||||||
'genre': self.genre,
|
'album': self.album,
|
||||||
'artist': self.artist,
|
'genre': self.genre,
|
||||||
'comment': self.comment,
|
'artist': self.artist,
|
||||||
|
'comment': self.comment,
|
||||||
'software': self.software,
|
'software': self.software,
|
||||||
'created_date': self.created_date,
|
'created_date': self.created_date,
|
||||||
'engineer': self.engineer,
|
'engineer': self.engineer,
|
||||||
'keywords': self.keywords,
|
'keywords': self.keywords,
|
||||||
'title': self.title,
|
'title': self.title,
|
||||||
'source': self.source,
|
'source': self.source,
|
||||||
'tape': self.tape,
|
'tape': self.tape,
|
||||||
'commissioned': self.commissioned,
|
'commissioned': self.commissioned,
|
||||||
'software': self.software,
|
'archival_location': self.archival_location,
|
||||||
'archival_location':self.archival_location,
|
'subject': self.subject,
|
||||||
'subject': self.subject,
|
'technician': self.technician
|
||||||
'technician':self.technician
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return_val = self.to_dict()
|
||||||
|
return_val.update({'encoding': self.encoding})
|
||||||
|
return str(return_val)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#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'])
|
||||||
|
|
||||||
|
|
||||||
class WavIXMLFormat:
|
class WavIXMLFormat:
|
||||||
"""
|
"""
|
||||||
iXML recorder metadata.
|
iXML recorder metadata.
|
||||||
@@ -16,20 +16,16 @@ class WavIXMLFormat:
|
|||||||
:param xml: A bytes-like object containing the iXML payload.
|
:param xml: A bytes-like object containing the iXML payload.
|
||||||
"""
|
"""
|
||||||
self.source = xml
|
self.source = xml
|
||||||
xmlBytes = io.BytesIO(xml)
|
xml_bytes = io.BytesIO(xml)
|
||||||
try:
|
parser = ET.XMLParser(recover=True)
|
||||||
parser = ET.XMLParser(recover=True)
|
self.parsed = ET.parse(xml_bytes, parser=parser)
|
||||||
self.parsed = ET.parse(xmlBytes, parser=parser)
|
|
||||||
except ET.ParseError as err:
|
|
||||||
print("Error parsing iXML: " + str(err))
|
|
||||||
decoded = xml.decode(encoding='utf_8_sig')
|
|
||||||
print(decoded)
|
|
||||||
self.parsed = ET.parse(io.StringIO(decoded))
|
|
||||||
|
|
||||||
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):
|
||||||
@@ -52,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.
|
||||||
@@ -88,10 +84,8 @@ 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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
#-*- coding: utf-8 -*-
|
#-*- coding: utf-8 -*-
|
||||||
import struct
|
import struct
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
if sys.version[0] == '3':
|
import pathlib
|
||||||
import pathlib
|
|
||||||
else:
|
|
||||||
import urlparse, urllib
|
|
||||||
|
|
||||||
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
|
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
|
||||||
from .wave_ixml_reader import WavIXMLFormat
|
from .wave_ixml_reader import WavIXMLFormat
|
||||||
@@ -15,12 +11,14 @@ from .wave_bext_reader import WavBextReader
|
|||||||
from .wave_info_reader import WavInfoChunkReader
|
from .wave_info_reader import WavInfoChunkReader
|
||||||
|
|
||||||
#: 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')
|
||||||
|
|
||||||
#: The format of the audio samples.
|
#: The format of the audio samples.
|
||||||
WavAudioFormat = namedtuple('WavAudioFormat','audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
|
WavAudioFormat = namedtuple('WavAudioFormat',
|
||||||
|
'audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
|
||||||
|
|
||||||
class WavInfoReader():
|
|
||||||
|
class WavInfoReader:
|
||||||
"""
|
"""
|
||||||
Parse a WAV audio file for metadata.
|
Parse a WAV audio file for metadata.
|
||||||
"""
|
"""
|
||||||
@@ -29,66 +27,71 @@ 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.
|
:param info_encoding:
|
||||||
latin_1/Win CP1252 has always been a pretty good guess for this.
|
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
|
:param bext_encoding:
|
||||||
fields of the Broadcast-WAV extension. Per EBU 3285 this is ASCII
|
The text encoding to use when decoding the string
|
||||||
but this parameter is available to you if you encounter a werido.
|
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)
|
|
||||||
|
self.info_encoding = info_encoding
|
||||||
|
self.bext_encoding = bext_encoding
|
||||||
|
|
||||||
|
if hasattr(path, 'read'):
|
||||||
|
self.get_wav_info(path)
|
||||||
|
self.url = 'about:blank'
|
||||||
|
self.path = repr(path)
|
||||||
|
else:
|
||||||
|
absolute_path = os.path.abspath(path)
|
||||||
|
|
||||||
if sys.version[0] == '3':
|
|
||||||
#: `file://` url for the file.
|
#: `file://` url for the file.
|
||||||
self.url = pathlib.Path(absolute_path).as_uri()
|
self.url = pathlib.Path(absolute_path).as_uri()
|
||||||
else:
|
|
||||||
self.url = urlparse.urljoin('file:', urllib.pathname2url(absolute_path))
|
|
||||||
|
|
||||||
with open(path, 'rb') as f:
|
# for __repr__()
|
||||||
chunks = parse_chunk(f)
|
self.path = absolute_path
|
||||||
|
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
self.get_wav_info(f)
|
||||||
|
|
||||||
|
def get_wav_info(self, wavfile):
|
||||||
|
chunks = parse_chunk(wavfile)
|
||||||
|
|
||||||
self.main_list = chunks.children
|
self.main_list = chunks.children
|
||||||
f.seek(0)
|
wavfile.seek(0)
|
||||||
|
|
||||||
#: :class:`wavinfo.wave_reader.WavAudioFormat`
|
#: :class:`wavinfo.wave_reader.WavAudioFormat`
|
||||||
self.fmt = self._get_format(f)
|
self.fmt = self._get_format(wavfile)
|
||||||
|
|
||||||
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
|
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
|
||||||
self.bext = self._get_bext(f, encoding=bext_encoding)
|
self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
|
||||||
|
|
||||||
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
|
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
|
||||||
self.ixml = self._get_ixml(f)
|
self.ixml = self._get_ixml(wavfile)
|
||||||
|
|
||||||
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
|
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
|
||||||
self.info = self._get_info(f, encoding=info_encoding)
|
self.info = self._get_info(wavfile, encoding=self.info_encoding)
|
||||||
self.data = self._describe_data(f)
|
self.data = self._describe_data()
|
||||||
|
|
||||||
def _find_chunk_data(self, ident, from_stream, default_none=False):
|
def _find_chunk_data(self, ident, from_stream, default_none=False):
|
||||||
chunk_descriptor = None
|
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor and chunk.ident == ident)
|
||||||
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor)
|
chunk_descriptor = next(top_chunks, None) if default_none else next(top_chunks)
|
||||||
|
return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None
|
||||||
|
|
||||||
if default_none:
|
def _describe_data(self):
|
||||||
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident),None)
|
|
||||||
else:
|
|
||||||
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident))
|
|
||||||
|
|
||||||
if chunk_descriptor:
|
|
||||||
return chunk_descriptor.read_data(from_stream)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _describe_data(self,f):
|
|
||||||
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 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))
|
||||||
|
|
||||||
|
def _get_format(self, f):
|
||||||
def _get_format(self,f):
|
fmt_data = self._find_chunk_data(b'fmt ', f)
|
||||||
fmt_data = self._find_chunk_data(b'fmt ',f)
|
|
||||||
|
|
||||||
# The format chunk is
|
# The format chunk is
|
||||||
# audio_format U16
|
# audio_format U16
|
||||||
@@ -102,42 +105,34 @@ class WavInfoReader():
|
|||||||
|
|
||||||
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
|
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
|
||||||
|
|
||||||
#0x0001 WAVE_FORMAT_PCM PCM
|
# 0x0001 WAVE_FORMAT_PCM PCM
|
||||||
#0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
|
# 0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
|
||||||
#0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
|
# 0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
|
||||||
#0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
|
# 0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
|
||||||
#0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
|
# 0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
|
||||||
|
|
||||||
#https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html
|
# https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html
|
||||||
return WavAudioFormat(audio_format = unpacked[0],
|
return WavAudioFormat(audio_format=unpacked[0],
|
||||||
channel_count = unpacked[1],
|
channel_count=unpacked[1],
|
||||||
sample_rate = unpacked[2],
|
sample_rate=unpacked[2],
|
||||||
byte_rate = unpacked[3],
|
byte_rate=unpacked[3],
|
||||||
block_align = unpacked[4],
|
block_align=unpacked[4],
|
||||||
bits_per_sample = unpacked[5]
|
bits_per_sample=unpacked[5]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_info(self, f, encoding):
|
def _get_info(self, f, encoding):
|
||||||
finder = (chunk.signature for chunk in self.main_list \
|
finder = (chunk.signature for chunk in self.main_list if type(chunk) is ListChunkDescriptor)
|
||||||
if type(chunk) is ListChunkDescriptor)
|
|
||||||
|
|
||||||
if b'INFO' in finder:
|
if b'INFO' in finder:
|
||||||
return WavInfoChunkReader(f, encoding)
|
return WavInfoChunkReader(f, encoding)
|
||||||
|
|
||||||
def _get_bext(self, f, encoding):
|
def _get_bext(self, f, encoding):
|
||||||
bext_data = self._find_chunk_data(b'bext',f,default_none=True)
|
bext_data = self._find_chunk_data(b'bext', f, default_none=True)
|
||||||
if bext_data:
|
return WavBextReader(bext_data, encoding) if bext_data else None
|
||||||
return WavBextReader(bext_data, encoding)
|
|
||||||
else:
|
|
||||||
return 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)
|
||||||
if ixml_data is None:
|
return None if ixml_data is None else WavIXMLFormat(ixml_data.rstrip(b'\0'))
|
||||||
return None
|
|
||||||
|
|
||||||
ixml_string = ixml_data.rstrip(b'\0')
|
|
||||||
return WavIXMLFormat(ixml_string)
|
|
||||||
|
|
||||||
def walk(self):
|
def walk(self):
|
||||||
"""
|
"""
|
||||||
@@ -147,13 +142,22 @@ class WavInfoReader():
|
|||||||
metadata field, and the value.
|
metadata field, and the value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ('fmt', 'data') #'bext', 'ixml', 'info')
|
scopes = ('fmt', 'data') # 'bext', 'ixml', 'info')
|
||||||
|
|
||||||
for scope in scopes:
|
for scope in scopes:
|
||||||
attr = self.__getattribute__(scope)
|
attr = self.__getattribute__(scope)
|
||||||
for field in attr._fields:
|
for field in attr._fields:
|
||||||
yield scope, field, attr.__getattribute__(field)
|
yield scope, field, attr.__getattribute__(field)
|
||||||
|
|
||||||
bext_dict = self.bext.to_dict()
|
if self.bext is not None:
|
||||||
for key in bext_dict.keys():
|
bext_dict = (self.bext or {}).to_dict()
|
||||||
yield 'bext', key, bext_dict[key]
|
for key in bext_dict.keys():
|
||||||
|
yield 'bext', key, bext_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):
|
||||||
|
return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)
|
||||||
|
|||||||
Reference in New Issue
Block a user