mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ce7b9ad9 | ||
|
|
00728f5af3 | ||
|
|
4e061a85f1 | ||
|
|
af5c83b8fc | ||
|
|
a7f77a49f7 | ||
|
|
0acbe58f0b | ||
|
|
8d908a3e34 | ||
|
|
c897d080bb | ||
|
|
710473f2aa | ||
|
|
cf48763b13 | ||
|
|
5651367df7 | ||
|
|
6d7373391e | ||
|
|
ff60f26f78 | ||
|
|
cc49df8f08 | ||
|
|
bdf5fc9349 | ||
|
|
4109f77372 | ||
|
|
7b9b64d799 | ||
|
|
e5cd098d44 | ||
|
|
957b23db92 | ||
|
|
733113819e | ||
|
|
df4cc8822e | ||
|
|
d5b6f15e28 | ||
|
|
b830b8cdc2 | ||
|
|
b23470ac19 | ||
|
|
8fe7eefb4a | ||
|
|
f0b7a0ddf6 | ||
|
|
e83603cb47 | ||
|
|
b6acdb1f7f | ||
|
|
faf809b8e2 | ||
|
|
f7a1896f99 | ||
|
|
40aee91162 | ||
|
|
9f8fc87d17 | ||
|
|
b2323a126f | ||
|
|
8fcc9787f6 | ||
|
|
52ea6fdb60 | ||
|
|
c26942db04 | ||
|
|
12eff79e5f | ||
|
|
d9e3e8deee | ||
|
|
c17fb242e3 | ||
|
|
64f3a640e3 | ||
|
|
5d4f97f6cc | ||
|
|
f9e5f28f7d | ||
|
|
3e6c485eb9 | ||
|
|
436bbe1686 | ||
|
|
ddb4d5cdca | ||
|
|
cec8165919 | ||
|
|
73a5034e02 | ||
|
|
9a46db4ae5 | ||
|
|
ccca30e234 | ||
|
|
c367acc185 | ||
|
|
2266cc5032 | ||
|
|
ec5b796181 | ||
|
|
97bdb23441 | ||
|
|
8f2fd69b00 | ||
|
|
a063fffb41 | ||
|
|
5b9d326e94 | ||
|
|
85775055a9 | ||
|
|
59509e4399 | ||
|
|
3a63ce9c8c | ||
|
|
5bfe0bd95b | ||
|
|
992de72cc9 | ||
|
|
ee305cebf4 | ||
|
|
ea4f484488 | ||
|
|
d00e07be36 | ||
|
|
68931348a6 | ||
|
|
68c75fc43f | ||
|
|
1eca249ba4 | ||
|
|
2052fa385a | ||
|
|
3096f02971 | ||
|
|
be47786439 | ||
|
|
ecde5359f1 | ||
|
|
8ae73213bc | ||
|
|
53217ce293 | ||
|
|
f9969d32cc | ||
|
|
04c402680b | ||
|
|
f10a546fe9 | ||
|
|
ec42ee1d3d | ||
|
|
bba4d67641 | ||
|
|
4bc7f94198 | ||
|
|
14eb8df496 | ||
|
|
a3aee8e785 | ||
|
|
9e9b6b512b |
26
.devcontainer/devcontainer.json
Normal file
26
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/anaconda:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/python:1": {
|
||||||
|
"version":"3.11"
|
||||||
|
},
|
||||||
|
"ghcr.io/meaningful-ooo/devcontainer-features/fish:1": {}
|
||||||
|
},
|
||||||
|
"postCreateCommand": "pip3 install -r requirements.txt && pip3 install -r docs/requirements.txt && pip3 install pytest && conda install -y ffmpeg",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.isort",
|
||||||
|
"ms-toolsai.jupyter",
|
||||||
|
"ms-toolsai.vscode-jupyter-cell-tags",
|
||||||
|
"ms-toolsai.jupyter-keymap",
|
||||||
|
"ms-toolsai.jupyter-renderers",
|
||||||
|
"ms-toolsai.vscode-jupyter-slideshow",
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"george-alisson.html-preview-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.github/workflows/python-package.yml
vendored
2
.github/workflows/python-package.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.5.0
|
- uses: actions/checkout@v2.5.0
|
||||||
|
|||||||
10
.github/workflows/pythonpublish.yml
vendored
10
.github/workflows/pythonpublish.yml
vendored
@@ -24,3 +24,13 @@ jobs:
|
|||||||
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
4
.gitignore
vendored
@@ -89,6 +89,8 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
docs_venv/
|
||||||
|
venv_docs/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@@ -106,3 +108,5 @@ venv.bak/
|
|||||||
# vim swap
|
# vim swap
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
|||||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (wavinfo)" project-jdk-type="Python SDK" />
|
|
||||||
<component name="PyPackaging">
|
|
||||||
<option name="earlyReleasesAsUpgrades" value="true" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
10
.idea/wavinfo.iml
generated
10
.idea/wavinfo.iml
generated
@@ -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.7 (wavinfo)" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
107
.idea/workspace.xml
generated
107
.idea/workspace.xml
generated
@@ -1,107 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="fb29ad56-fffe-478f-9c0e-1718aa04342e" name="Default Changelist" comment="">
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/dictionaries/jamiehardt.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dictionaries/jamiehardt.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/wavinfo.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/wavinfo.iml" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="FavoritesManager">
|
|
||||||
<favorites_list name="wavinfo" />
|
|
||||||
</component>
|
|
||||||
<component name="FileTemplateManagerImpl">
|
|
||||||
<option name="RECENT_TEMPLATES">
|
|
||||||
<list>
|
|
||||||
<option value="Python Script" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
<option name="UPDATE_TYPE" value="MERGE" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectId" id="1VtfP9ukGK4GNIXS2m0hRLG9d7w" />
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
<option name="showMembers" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent">
|
|
||||||
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
|
|
||||||
<property name="TODO_SCOPE" value="Project Files" />
|
|
||||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
|
||||||
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
|
|
||||||
</component>
|
|
||||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="project-level" UseSingleDictionary="true" transferred="true" />
|
|
||||||
<component name="SvnConfiguration">
|
|
||||||
<configuration />
|
|
||||||
</component>
|
|
||||||
<component name="TaskManager">
|
|
||||||
<task active="true" id="Default" summary="Default task">
|
|
||||||
<changelist id="fb29ad56-fffe-478f-9c0e-1718aa04342e" name="Default Changelist" comment="" />
|
|
||||||
<created>1578077284420</created>
|
|
||||||
<option name="number" value="Default" />
|
|
||||||
<option name="presentableId" value="Default" />
|
|
||||||
<updated>1578077284420</updated>
|
|
||||||
</task>
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
<component name="Vcs.Log.History.Properties">
|
|
||||||
<option name="COLUMN_ORDER">
|
|
||||||
<list>
|
|
||||||
<option value="0" />
|
|
||||||
<option value="2" />
|
|
||||||
<option value="3" />
|
|
||||||
<option value="1" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="Vcs.Log.Tabs.Properties">
|
|
||||||
<option name="TAB_STATES">
|
|
||||||
<map>
|
|
||||||
<entry key="MAIN">
|
|
||||||
<value>
|
|
||||||
<State />
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="oldMeFiltersMigrated" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="WindowStateProjectService">
|
|
||||||
<state x="655" y="287" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog" timestamp="1598547965709">
|
|
||||||
<screen x="0" y="0" width="1920" height="1080" />
|
|
||||||
</state>
|
|
||||||
<state x="655" y="287" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598547965709" />
|
|
||||||
<state x="0" y="0" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog" timestamp="1598555737671">
|
|
||||||
<screen x="0" y="0" width="1920" height="1080" />
|
|
||||||
</state>
|
|
||||||
<state x="0" y="0" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598555737671" />
|
|
||||||
<state x="476" y="183" key="SettingsEditor" timestamp="1598548365517">
|
|
||||||
<screen x="0" y="0" width="1920" height="1080" />
|
|
||||||
</state>
|
|
||||||
<state x="476" y="183" key="SettingsEditor/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598548365517" />
|
|
||||||
<state x="114" y="123" width="1706" height="842" key="com.intellij.history.integration.ui.views.FileHistoryDialog" timestamp="1597689482054">
|
|
||||||
<screen x="0" y="0" width="1920" height="1080" />
|
|
||||||
</state>
|
|
||||||
<state x="114" y="123" width="1706" height="842" key="com.intellij.history.integration.ui.views.FileHistoryDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1597689482054" />
|
|
||||||
<state x="577" y="376" key="com.intellij.ide.util.TipDialog" timestamp="1601767353495">
|
|
||||||
<screen x="0" y="0" width="1920" height="1080" />
|
|
||||||
</state>
|
|
||||||
<state x="577" y="376" key="com.intellij.ide.util.TipDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598550975228" />
|
|
||||||
<state x="577" y="376" key="com.intellij.ide.util.TipDialog/1920.0.1920.1080/0.0.1920.1080@0.0.1920.1080" timestamp="1601767353495" />
|
|
||||||
<state x="772" y="468" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2" timestamp="1598547948993">
|
|
||||||
<screen x="0" y="0" width="1920" height="1080" />
|
|
||||||
</state>
|
|
||||||
<state x="772" y="468" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598547948993" />
|
|
||||||
<state x="632" y="254" width="670" height="676" key="search.everywhere.popup" timestamp="1598554315633">
|
|
||||||
<screen x="0" y="0" width="1920" height="1080" />
|
|
||||||
</state>
|
|
||||||
<state x="632" y="254" width="670" height="676" key="search.everywhere.popup/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598554315633" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Contributions to this project are very welcome!
|
||||||
|
|
||||||
|
If you discover a bug or would like better support for a feature, please do the following:
|
||||||
|
|
||||||
|
1. Submit an Issue.
|
||||||
|
|
||||||
|
I'm actively developing this project and will review incoming issues.
|
||||||
|
1. Check out the source code and submit a PR.
|
||||||
|
|
||||||
|
If you're facile with Python and understand what you'd like to fix, submit a PR and I'll
|
||||||
|
review it as soon as I can. There's a `.devcontainer` available so you can creates commits
|
||||||
|
on this project in a GitHub codespace.
|
||||||
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 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
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -1,34 +1,38 @@
|
|||||||
[](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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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, 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 and schema, including channel, pack formats, object, content and programme.
|
||||||
|
* [__Dolby Digital Plus__][ebu3285s6] and Dolby Atmos `dbmd` metadata.
|
||||||
|
* [__iXML__][ixml] production recorder metadata, including project, scene, and take tags, recorder notes
|
||||||
and file family information.
|
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].
|
|
||||||
* iXML `STEINBERG` sound library attributes.
|
|
||||||
* __NetMix__ library attributes.
|
|
||||||
* Pro Tools __embedded regions__.
|
* Pro Tools __embedded regions__.
|
||||||
|
* iXML `STEINBERG` sound library attributes.
|
||||||
|
|
||||||
[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.
|
||||||
|
|
||||||
@@ -38,6 +42,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:
|
||||||
@@ -46,17 +53,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)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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).
|
||||||
|
|||||||
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
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
Other wavinfo Classes
|
Other wavinfo Classes
|
||||||
===============
|
=====================
|
||||||
|
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_reader.WavInfoReader
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. automethod:: __init__
|
|
||||||
|
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_reader.WavAudioFormat
|
.. autoclass:: wavinfo.wave_reader.WavAudioFormat
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
|
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
94
docs/source/command_line.rst
Normal file
94
docs/source/command_line.rst
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
Using `wavinfo` from the Command Line
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
`wavinfo` installs a command-line entry point that will read wav files
|
||||||
|
from the command line and output metadata to stdout.
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
$ wavinfo [--ixml | --adm] INFILE +
|
||||||
|
|
||||||
|
By default, `wavinfo` will output a JSON dictionary for each file argument.
|
||||||
|
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
|
||||||
|
Two option flags will change the behavior of the command:
|
||||||
|
|
||||||
|
``--ixml``
|
||||||
|
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload
|
||||||
|
of each input wave file, or will emit an error message to stderr if iXML
|
||||||
|
metadata is not present.
|
||||||
|
|
||||||
|
``--adm``
|
||||||
|
The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata
|
||||||
|
payload of each input wave file, or will emit an error message to stderr if
|
||||||
|
ADM XML metadata is not present.
|
||||||
|
|
||||||
|
These options are mutually-exclusive, with `\-\-adm` taking precedence.
|
||||||
|
|
||||||
|
|
||||||
|
Example Output
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"filename": "tests/test_files/sounddevices/A101_1.WAV",
|
||||||
|
"run_date": "2022-11-26T17:56:38.342935",
|
||||||
|
"application": "wavinfo 2.1.0",
|
||||||
|
"scopes": {
|
||||||
|
"fmt": {
|
||||||
|
"audio_format": 1,
|
||||||
|
"channel_count": 2,
|
||||||
|
"sample_rate": 48000,
|
||||||
|
"byte_rate": 288000,
|
||||||
|
"block_align": 6,
|
||||||
|
"bits_per_sample": 24
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"byte_count": 1441434,
|
||||||
|
"frame_count": 240239
|
||||||
|
},
|
||||||
|
"ixml": {
|
||||||
|
"track_list": [
|
||||||
|
{
|
||||||
|
"channel_index": "1",
|
||||||
|
"interleave_index": "1",
|
||||||
|
"name": "MKH516 A",
|
||||||
|
"function": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel_index": "2",
|
||||||
|
"interleave_index": "2",
|
||||||
|
"name": "Boom",
|
||||||
|
"function": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"project": "BMH",
|
||||||
|
"scene": "A101",
|
||||||
|
"take": "1",
|
||||||
|
"tape": "18Y12M31",
|
||||||
|
"family_uid": "USSDVGR1112089007124001008206300",
|
||||||
|
"family_name": null
|
||||||
|
},
|
||||||
|
"bext": {
|
||||||
|
"description": "sSPEED=023.976-ND\r\nsTAKE=1\r\nsUBITS=$12311801\r\nsSWVER=2.67\r\nsPROJECT=BMH\r\nsSCENE=A101\r\nsFILENAME=A101_1.WAV\r\nsTAPE=18Y12M31\r\nsTRK1=MKH516 A\r\nsTRK2=Boom\r\nsNOTE=\r\n",
|
||||||
|
"originator": "Sound Dev: 702T S#GR1112089007",
|
||||||
|
"originator_ref": "USSDVGR1112089007124001008206301",
|
||||||
|
"originator_date": "2018-12-31",
|
||||||
|
"originator_time": "12:40:00",
|
||||||
|
"time_reference": 2190940753,
|
||||||
|
"version": 1,
|
||||||
|
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
|
||||||
|
"loudness_value": null,
|
||||||
|
"loudness_range": null,
|
||||||
|
"max_true_peak": null,
|
||||||
|
"max_momentary_loudness": null,
|
||||||
|
"max_shortterm_loudness": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ sys.path.insert(0, os.path.abspath('../..'))
|
|||||||
sys.path.insert(0, os.path.abspath("../../.."))
|
sys.path.insert(0, os.path.abspath("../../.."))
|
||||||
print(sys.path)
|
print(sys.path)
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
@@ -26,9 +27,9 @@ 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.7.1'
|
release = wavinfo.__version__
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|||||||
@@ -6,13 +6,19 @@
|
|||||||
Welcome to wavinfo's documentation!
|
Welcome to wavinfo's documentation!
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
|
The `wavinfo` package allows you to probe WAVE and RF64/WAVE files and
|
||||||
|
extract extended metadata, with an emphasis on film, video and professional
|
||||||
|
music production metadata.
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 1
|
||||||
:caption: Notes:
|
:glob:
|
||||||
|
:numbered:
|
||||||
metadata_scopes/bext.rst
|
|
||||||
metadata_scopes/ixml.rst
|
quickstart
|
||||||
metadata_scopes/info.rst
|
command_line
|
||||||
|
scopes/*
|
||||||
|
|
||||||
classes
|
classes
|
||||||
|
|
||||||
|
|||||||
24
docs/source/quickstart.rst
Normal file
24
docs/source/quickstart.rst
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
wavinfo Quickstart
|
||||||
|
====================
|
||||||
|
|
||||||
|
All metadata is read by an instance of :class:`WaveInfoReader<wavinfo.wave_reader.WavInfoReader>`.
|
||||||
|
Each type of metadata, iXML, Broadcast-WAV etc. is accessible through *scopes*, properties on an
|
||||||
|
instance of :class:`WaveInfoReader`.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: Using wavinfo
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
path = 'path/to/your/wave/audio.wav'
|
||||||
|
|
||||||
|
info = wavinfo.WavInfoReader(path)
|
||||||
|
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_reader.WavInfoReader
|
||||||
|
:members:
|
||||||
|
|
||||||
30
docs/source/scopes/adm.rst
Normal file
30
docs/source/scopes/adm.rst
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
ADM (Audio Definition Model) Metadata
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
`ADM metadata`_ is used in master recordings to describe the format and content
|
||||||
|
of the tracks. In practice on wave files, ADM tells a client which tracks are
|
||||||
|
members of multichannel stems or "beds" and their speaker assignment, and which
|
||||||
|
tracks are freely-positioned 3D objects. ADM also records the panning moves on
|
||||||
|
object tracks and their content group ("Dialogue", "Music", "Effects" etc.)
|
||||||
|
|
||||||
|
ADM wave files created with a Dolby Rendering and Mastering Unit are a common
|
||||||
|
deliverable in feature film and television production. The `Dolby Atmos ADM Profile`_
|
||||||
|
describes how the RMU translates its native Master format into ADM.
|
||||||
|
|
||||||
|
|
||||||
|
.. _ADM metadata: https://adm.ebu.io
|
||||||
|
.. _Dolby Atmos ADM Profile: https://developer.dolby.com/globalassets/documentation/technology/dolby_atmos_master_adm_profile_v1.0.pdf
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_adm_reader.WavADMReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_adm_reader.ChannelEntry
|
||||||
|
:members:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
Broadcast WAV Extension
|
Broadcast WAV Extension Metadata
|
||||||
=======================
|
================================
|
||||||
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
@@ -9,20 +9,26 @@ which includes a 256-character free text descrption, creating entity identifier
|
|||||||
recording application or equipment), the date and time of recording and a time reference for
|
recording application or equipment), the date and time of recording and a time reference for
|
||||||
timecode synchronization.
|
timecode synchronization.
|
||||||
|
|
||||||
The `coding_history` is designed to contain a record of every conversion performed on the audio
|
The :py:attr:`coding_history<wavinfo.wave_bext_reader.WavBextReader.coding_history>`
|
||||||
file.
|
is designed to contain a record of every conversion performed on the audio file.
|
||||||
|
|
||||||
In this example (from a Sound Devices 702T) the bext metadata contains scene/take slating
|
In this example (from a Sound Devices 702T) the bext metadata contains scene/take slating
|
||||||
information in the `description`. Here also the `originator_ref` is a serial number conforming
|
information in the :py:attr:`description<wavinfo.wave_bext_reader.WavBextReader.description>`.
|
||||||
to EBU Rec 99.
|
Here also the :py:attr:`originator_ref<wavinfo.wave_bext_reader.WavBextReader.originator_ref>`
|
||||||
|
is a serial number conforming to EBU Rec 99.
|
||||||
|
|
||||||
If the bext metadata conforms to EBU 3285 v1, it will contain the WAV's 32 or 64 byte SMPTE
|
If the bext metadata conforms to `EBU 3285 v1`_, it will contain the WAV's 32 or 64 byte `SMPTE
|
||||||
330M UMID. The 32-byte version of the UMID is usually just a random number, while the 64-byte
|
ST 330 UMID`_. The 32-byte version of the UMID is usually just a random number, while the 64-byte
|
||||||
UMID will also have information on the recording date and time, recording equipment and entity,
|
UMID will also have information on the recording date and time, recording equipment and entity,
|
||||||
and geolocation data.
|
and geolocation data.
|
||||||
|
|
||||||
If the bext metadata conforms to EBU 3285 v2, it will hold precomputed program loudness values
|
If the bext metadata conforms to `EBU 3285 v2`_, it will hold precomputed program loudness values
|
||||||
as described by EBU Rec 128.
|
as described by `EBU Rec 128`_.
|
||||||
|
|
||||||
|
.. _EBU 3285 v1: https://tech.ebu.ch/publications/tech3285s1
|
||||||
|
.. _SMPTE ST 330 UMID: https://standards.globalspec.com/std/1396751/smpte-st-330
|
||||||
|
.. _EBU 3285 v2: https://tech.ebu.ch/publications/tech3285s2
|
||||||
|
.. _EBU Rec 128: https://tech.ebu.ch/publications/r128
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
@@ -63,8 +69,6 @@ Result:
|
|||||||
Class Reference
|
Class Reference
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
.. module:: wavinfo
|
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
|
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
21
docs/source/scopes/dolby.rst
Normal file
21
docs/source/scopes/dolby.rst
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Dolby Metadata
|
||||||
|
==============
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
Dolby software and equipment creates detailed hinting metadata that can help
|
||||||
|
receiving applications decide how to present the audio content, particularly
|
||||||
|
how it should be downmixed, and dialogue normalization settings.
|
||||||
|
|
||||||
|
|
||||||
|
Class Reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: wavinfo.wave_dbmd_reader
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_dbmd_reader.WavDolbyMetadataReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_dbmd_reader.DolbyDigitalPlusMetadata
|
||||||
|
:members:
|
||||||
@@ -20,11 +20,20 @@ music library software.
|
|||||||
print("INFO Comment:", bullet.info.comment)
|
print("INFO Comment:", bullet.info.comment)
|
||||||
|
|
||||||
|
|
||||||
|
On Encodings
|
||||||
|
""""""""""""
|
||||||
|
According to Microsoft, the original developers of the RIFF file and RIFF INFO
|
||||||
|
metadata, these fields are always to be interpreted as ISO Latin 1 characters,
|
||||||
|
and this is the default encoding used by `wavinfo` for these fields. You can
|
||||||
|
select a different encoding (like Shift-JIS) by passing an encoding name (as
|
||||||
|
would be used by `string.encode()`) to `WavInfoReader.__init__()`'s
|
||||||
|
`info_encoding=` parameter.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Class Reference
|
Class Reference
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
.. module:: wavinfo
|
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
|
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@@ -37,8 +37,6 @@ Result:
|
|||||||
Class Reference
|
Class Reference
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
.. module:: wavinfo
|
|
||||||
|
|
||||||
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
|
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""
|
|
||||||
Wavinfo
|
|
||||||
"""
|
|
||||||
__version__ = '1.7.1'
|
|
||||||
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
|
|
||||||
__license__ = "MIT"
|
|
||||||
@@ -1,19 +1 @@
|
|||||||
attrs==21.4.0
|
|
||||||
ear==2.1.0
|
|
||||||
enum34==1.1.10
|
|
||||||
exceptiongroup==1.0.4
|
|
||||||
iniconfig==1.1.1
|
|
||||||
lxml==4.9.1
|
lxml==4.9.1
|
||||||
multipledispatch==0.6.0
|
|
||||||
numpy==1.23.3
|
|
||||||
packaging==21.3
|
|
||||||
pluggy==1.0.0
|
|
||||||
pyparsing==3.0.9
|
|
||||||
pytest==7.2.0
|
|
||||||
ruamel.yaml==0.17.21
|
|
||||||
ruamel.yaml.clib==0.2.6
|
|
||||||
scipy==1.9.1
|
|
||||||
six==1.16.0
|
|
||||||
style==1.1.0
|
|
||||||
tomli==2.0.1
|
|
||||||
update==0.0.1
|
|
||||||
|
|||||||
7
setup.py
7
setup.py
@@ -1,5 +1,5 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
from metadata 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()
|
||||||
@@ -28,9 +28,10 @@ setup(name='wavinfo',
|
|||||||
'Topic :: Multimedia :: Sound/Audio',
|
'Topic :: Multimedia :: Sound/Audio',
|
||||||
"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.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'
|
||||||
|
|||||||
51
tests/test_adm.py
Normal file
51
tests/test_adm.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
|
||||||
|
class TestADMWave(TestCase):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.protools_adm_wav = "tests/test_files/protools/Test_ADM_ProTools.wav"
|
||||||
|
return super().setUp()
|
||||||
|
|
||||||
|
def test_chna(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
|
||||||
|
adm = info.adm
|
||||||
|
self.assertIsNotNone(adm)
|
||||||
|
|
||||||
|
self.assertEqual(len(adm.channel_uids), 14)
|
||||||
|
|
||||||
|
def test_to_dict(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
adm = info.adm
|
||||||
|
dict = adm.to_dict()
|
||||||
|
self.assertIsNotNone(dict)
|
||||||
|
|
||||||
|
def test_programme(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
adm = info.adm
|
||||||
|
pdict = adm.programme()
|
||||||
|
self.assertIn("programme_id", pdict.keys())
|
||||||
|
self.assertIn("programme_name", pdict.keys())
|
||||||
|
self.assertEqual(pdict['programme_id'], 'APR_1001')
|
||||||
|
self.assertEqual(pdict['programme_name'], 'Atmos_Master')
|
||||||
|
self.assertIn("contents", pdict.keys())
|
||||||
|
self.assertEqual(len(pdict["contents"]), 3)
|
||||||
|
|
||||||
|
def test_track_info(self):
|
||||||
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
|
adm = info.adm
|
||||||
|
|
||||||
|
t1 = adm.track_info(0)
|
||||||
|
self.assertTrue("channel_format_name" in t1.keys())
|
||||||
|
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
|
||||||
|
|
||||||
|
self.assertTrue("pack_format_name" in t1.keys())
|
||||||
|
self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"])
|
||||||
|
|
||||||
|
t10 = adm.track_info(10)
|
||||||
|
self.assertTrue("content_name" in t10.keys())
|
||||||
|
self.assertEqual("Dialog", t10["content_name"])
|
||||||
|
|
||||||
49
tests/test_dolby.py
Normal file
49
tests/test_dolby.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import wavinfo
|
||||||
|
from wavinfo.wave_dbmd_reader import SegmentType, DolbyAtmosMetadata, DolbyDigitalPlusMetadata
|
||||||
|
|
||||||
|
class TestDolby(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.test_file = "tests/test_files/protools/Test_ADM_ProTools.wav"
|
||||||
|
|
||||||
|
def test_version(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
self.assertEqual((1,0,0,6), d.version)
|
||||||
|
|
||||||
|
def test_segments(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
ddp = [x for x in d.segment_list if x[0] == SegmentType.DolbyDigitalPlus]
|
||||||
|
atmos = [x for x in d.segment_list if x[0] == SegmentType.DolbyAtmos]
|
||||||
|
|
||||||
|
self.assertEqual(len(ddp), 1)
|
||||||
|
self.assertEqual(len(atmos), 1)
|
||||||
|
|
||||||
|
def test_checksums(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
for seg in d.segment_list:
|
||||||
|
self.assertTrue(seg[1])
|
||||||
|
|
||||||
|
def test_ddp(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
ddp = d.dolby_digital_plus()
|
||||||
|
self.assertEqual(len(ddp), 1, "Failed to find exactly one Dolby Digital Plus metadata segment")
|
||||||
|
self.assertTrue( ddp[0].audio_coding_mode, DolbyDigitalPlusMetadata.AudioCodingMode.CH_ORD_3_2 )
|
||||||
|
self.assertTrue( ddp[0].lfe_on)
|
||||||
|
|
||||||
|
def test_atmos(self):
|
||||||
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
|
d = t1.dolby
|
||||||
|
|
||||||
|
atmos = d.dolby_atmos()
|
||||||
|
self.assertEqual(len(atmos), 1, "Failed to find exactly one Atmos metadata segment")
|
||||||
|
|
||||||
|
|
||||||
BIN
tests/test_files/problems/DinerAmbience VAL085101-glued.wav
Normal file
BIN
tests/test_files/problems/DinerAmbience VAL085101-glued.wav
Normal file
Binary file not shown.
BIN
tests/test_files/protools/Test_ADM_ProTools.wav
Normal file
BIN
tests/test_files/protools/Test_ADM_ProTools.wav
Normal file
Binary file not shown.
@@ -1,5 +1,4 @@
|
|||||||
import os.path
|
import os.path
|
||||||
import sys
|
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
methods to probe a WAV file for various kinds of production metadata.
|
methods to probe a WAV file for various kinds of production metadata.
|
||||||
|
|
||||||
Go to the documentation for wavinfo.WavInfoReader for more information.
|
See 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__ = '2.1.0'
|
||||||
|
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
|
||||||
|
__license__ = "MIT"
|
||||||
|
|||||||
@@ -1,33 +1,69 @@
|
|||||||
from optparse import OptionParser, OptionGroup
|
from optparse import OptionParser, OptionGroup
|
||||||
import datetime
|
import datetime
|
||||||
from . import WavInfoReader
|
from . import WavInfoReader
|
||||||
|
from . import __version__
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class MyJSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, Enum):
|
||||||
|
return o._name_
|
||||||
|
else:
|
||||||
|
return super().default(o)
|
||||||
|
|
||||||
|
class MissingDataError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
|
||||||
parser.usage = 'wavinfo [FILE.wav]*'
|
parser.usage = 'wavinfo (--adm | --ixml) [FILES]'
|
||||||
|
|
||||||
# parser.add_option('-f', dest='output_format', help='Set the output format',
|
# parser.add_option('-f', dest='output_format', help='Set the output format',
|
||||||
# default='json',
|
# default='json',
|
||||||
# metavar='FORMAT')
|
# metavar='FORMAT')
|
||||||
|
|
||||||
|
parser.add_option('--adm', dest='adm', help='Output ADM XML',
|
||||||
|
default=False, action='store_true')
|
||||||
|
|
||||||
|
parser.add_option('--ixml', dest='ixml', help='Output iXML',
|
||||||
|
default=False, action='store_true')
|
||||||
|
|
||||||
(options, args) = parser.parse_args(sys.argv)
|
(options, args) = parser.parse_args(sys.argv)
|
||||||
for arg in args[1:]:
|
for arg in args[1:]:
|
||||||
try:
|
try:
|
||||||
this_file = WavInfoReader(path=arg)
|
this_file = WavInfoReader(path=arg)
|
||||||
ret_dict = {'file_argument': arg, 'run_date': datetime.datetime.now().isoformat() , 'scopes': {}}
|
if options.adm:
|
||||||
for scope, name, value in this_file.walk():
|
if this_file.adm:
|
||||||
if scope not in ret_dict['scopes'].keys():
|
sys.stdout.write(this_file.adm.xml_str())
|
||||||
ret_dict['scopes'][scope] = {}
|
else:
|
||||||
|
raise MissingDataError("adm")
|
||||||
|
elif options.ixml:
|
||||||
|
if this_file.ixml:
|
||||||
|
sys.stdout.write(this_file.ixml.xml_bytes())
|
||||||
|
else:
|
||||||
|
raise MissingDataError("ixml")
|
||||||
|
else:
|
||||||
|
ret_dict = {
|
||||||
|
'filename': arg,
|
||||||
|
'run_date': datetime.datetime.now().isoformat() ,
|
||||||
|
'application': "wavinfo " + __version__,
|
||||||
|
'scopes': {}
|
||||||
|
}
|
||||||
|
for scope, name, value in this_file.walk():
|
||||||
|
if scope not in ret_dict['scopes'].keys():
|
||||||
|
ret_dict['scopes'][scope] = {}
|
||||||
|
|
||||||
ret_dict['scopes'][scope][name] = value
|
ret_dict['scopes'][scope][name] = value
|
||||||
|
|
||||||
json.dump(ret_dict, fp=sys.stdout, indent=2)
|
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
|
||||||
|
except MissingDataError as e:
|
||||||
|
print("MissingDataError: Missing metadata (%s) in file %s" % (e, arg), file=sys.stderr)
|
||||||
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
raise e
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
# 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...
|
|
||||||
165
wavinfo/wave_adm_reader.py
Normal file
165
wavinfo/wave_adm_reader.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
"""
|
||||||
|
ADM Reader
|
||||||
|
"""
|
||||||
|
|
||||||
|
from struct import unpack, unpack_from, calcsize
|
||||||
|
from io import BytesIO
|
||||||
|
from collections import namedtuple
|
||||||
|
from typing import Iterable, Tuple
|
||||||
|
|
||||||
|
from lxml import etree as ET
|
||||||
|
|
||||||
|
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
|
||||||
|
|
||||||
|
class WavADMReader:
|
||||||
|
"""
|
||||||
|
Reads XML data from an EBU ADM (Audio Definiton Model) WAV File.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, axml_data: bytes, chna_data: bytes):
|
||||||
|
header_fmt = "<HH"
|
||||||
|
uid_fmt = "<H12s14s11sx"
|
||||||
|
|
||||||
|
#: An :mod:`lxml.etree` of the ADM XML document
|
||||||
|
self.axml = ET.parse(BytesIO(axml_data))
|
||||||
|
|
||||||
|
_, uid_count = unpack(header_fmt, chna_data[0:4])
|
||||||
|
|
||||||
|
#: A list of :class:`ChannelEntry` objects parsed from the
|
||||||
|
#: `chna` metadata chunk.
|
||||||
|
#:
|
||||||
|
#: .. note::
|
||||||
|
#: In-file, the `chna` track indexes start at 1. However, this interface
|
||||||
|
#: numbers the first track 0, in order to maintain consistency with other
|
||||||
|
#: libraries.
|
||||||
|
self.channel_uids = []
|
||||||
|
|
||||||
|
offset = calcsize(header_fmt)
|
||||||
|
for _ in range(uid_count):
|
||||||
|
|
||||||
|
track_index, uid, track_ref, pack_ref = unpack_from(uid_fmt, chna_data, offset)
|
||||||
|
|
||||||
|
# these values are either ascii or all null
|
||||||
|
|
||||||
|
self.channel_uids.append(ChannelEntry(track_index - 1,
|
||||||
|
uid.decode('ascii') , track_ref.decode('ascii'), pack_ref.decode('ascii')))
|
||||||
|
|
||||||
|
offset += calcsize(uid_fmt)
|
||||||
|
|
||||||
|
def xml_str(self) -> str:
|
||||||
|
"""ADM XML as a string"""
|
||||||
|
return ET.tostring(self.axml).decode("utf-8")
|
||||||
|
|
||||||
|
def programme(self) -> dict:
|
||||||
|
"""
|
||||||
|
Extract the ADM audioProgramme data structure and some of its reference properties
|
||||||
|
"""
|
||||||
|
ret_dict = dict()
|
||||||
|
|
||||||
|
nsmap = self.axml.getroot().nsmap
|
||||||
|
|
||||||
|
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
||||||
|
|
||||||
|
program = afext.find("audioProgramme", namespaces=nsmap)
|
||||||
|
ret_dict['programme_id'] = program.get("audioProgrammeID")
|
||||||
|
ret_dict['programme_name'] = program.get("audioProgrammeName")
|
||||||
|
ret_dict['programme_start'] = program.get("start")
|
||||||
|
ret_dict['programme_end'] = program.get("end")
|
||||||
|
ret_dict['contents'] = []
|
||||||
|
|
||||||
|
for content_ref in program.findall("audioContentIDRef", namespaces=nsmap):
|
||||||
|
content_dict = dict()
|
||||||
|
content_dict['content_id'] = cid = content_ref.text
|
||||||
|
content = afext.find("audioContent[@audioContentID='%s']" % cid, namespaces=nsmap)
|
||||||
|
content_dict['content_name'] = content.get("audioContentName")
|
||||||
|
content_dict['objects'] = []
|
||||||
|
|
||||||
|
for object_ref in content.findall("audioObjectIDRef", namespaces=nsmap):
|
||||||
|
object_dict = dict()
|
||||||
|
object_dict['object_id'] = oid = object_ref.text
|
||||||
|
object = afext.find("audioObject[@audioObjectID='%s']" % oid, namespaces=nsmap)
|
||||||
|
pack = object.find("audioPackFormatIDRef", namespaces=nsmap)
|
||||||
|
object_dict['object_name'] = object.get("audioObjectName")
|
||||||
|
object_dict['object_start'] = object.get("start")
|
||||||
|
object_dict['object_duration'] = object.get("duration")
|
||||||
|
object_dict['pack_id'] = pack.text
|
||||||
|
track_uid_list = []
|
||||||
|
for t in object.findall("audioTrackUIDRef", namespaces=nsmap):
|
||||||
|
track_uid_list.append(t.text)
|
||||||
|
|
||||||
|
object_dict['track_uids'] = track_uid_list
|
||||||
|
content_dict['objects'].append(object_dict)
|
||||||
|
|
||||||
|
ret_dict['contents'].append(content_dict)
|
||||||
|
|
||||||
|
return ret_dict
|
||||||
|
|
||||||
|
def track_info(self, index):
|
||||||
|
"""
|
||||||
|
Information about a track in the WAV file.
|
||||||
|
|
||||||
|
:param index: index of audio track (indexed from zero)
|
||||||
|
:returns: a dictionary with *content_name*, *content_id*, *object_name*, *object_id*,
|
||||||
|
*pack_format_name*, *pack_type*, *channel_format_name*
|
||||||
|
"""
|
||||||
|
channel_info = next((x for x in self.channel_uids if x.track_index == index), None)
|
||||||
|
|
||||||
|
if channel_info is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret_dict = {}
|
||||||
|
|
||||||
|
nsmap = self.axml.getroot().nsmap
|
||||||
|
|
||||||
|
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
||||||
|
|
||||||
|
trackformat_elem = afext.find("audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref,
|
||||||
|
namespaces=nsmap)
|
||||||
|
|
||||||
|
stream_id = trackformat_elem[0].text
|
||||||
|
|
||||||
|
channelformatref_elem = afext.find("audioStreamFormat[@audioStreamFormatID='%s']/audioChannelFormatIDRef" % stream_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
channelformat_id = channelformatref_elem.text
|
||||||
|
|
||||||
|
packformatref_elem = afext.find("audioStreamFormat[@audioStreamFormatID='%s']/audioPackFormatIDRef" % stream_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
packformat_id = packformatref_elem.text
|
||||||
|
|
||||||
|
channelformat_elem = afext.find("audioChannelFormat[@audioChannelFormatID='%s']" % channelformat_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
ret_dict['channel_format_name'] = channelformat_elem.get("audioChannelFormatName")
|
||||||
|
|
||||||
|
packformat_elem = afext.find("audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
ret_dict['pack_type'] = packformat_elem.get("typeDefinition")
|
||||||
|
ret_dict['pack_format_name'] = packformat_elem.get("audioPackFormatName")
|
||||||
|
|
||||||
|
object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']" % packformat_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
|
||||||
|
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
|
||||||
|
object_id = object_elem.get("audioObjectID")
|
||||||
|
ret_dict['object_id'] = object_id
|
||||||
|
|
||||||
|
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']" % object_id,
|
||||||
|
namespaces=nsmap)
|
||||||
|
|
||||||
|
ret_dict['content_name'] = content_elem.get("audioContentName")
|
||||||
|
ret_dict['content_id'] = content_elem.get("audioContentID")
|
||||||
|
|
||||||
|
return ret_dict
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
Get ADM metadata as a dictionary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def make_entry(channel_uid_rec):
|
||||||
|
rd = channel_uid_rec._asdict()
|
||||||
|
rd.update(self.track_info(channel_uid_rec.track_index))
|
||||||
|
return rd
|
||||||
|
|
||||||
|
return dict(channel_entries=list(map(lambda z: make_entry(z), self.channel_uids)),
|
||||||
|
programme=self.programme())
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from lxml import etree as ET
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from ear.fileio.bw64 import Bw64Reader
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ class WavBextReader:
|
|||||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||||
|
|
||||||
def sanitize_bytes(b : bytes) -> str:
|
def sanitize_bytes(b : bytes) -> str:
|
||||||
|
# honestly can't remember why I'm stripping nulls this way
|
||||||
first_null = next((index for index, byte in enumerate(b) if byte == 0), None)
|
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)
|
||||||
@@ -29,9 +30,9 @@ class WavBextReader:
|
|||||||
self.originator : str = 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 : str = 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 YYYY-MM-DD in the local calendar
|
||||||
self.originator_date : str = 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 on the local clock
|
||||||
self.originator_time : str = 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.
|
||||||
|
|||||||
661
wavinfo/wave_dbmd_reader.py
Normal file
661
wavinfo/wave_dbmd_reader.py
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
"""
|
||||||
|
Reading Dolby Bitstream Metadata
|
||||||
|
|
||||||
|
Unless otherwise stated, all § references here are to
|
||||||
|
`EBU Tech 3285 Supplement 6`_.
|
||||||
|
|
||||||
|
.. _EBU Tech 3285 Supplement 6: https://tech.ebu.ch/docs/tech/tech3285s6.pdf
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import IntEnum, Enum
|
||||||
|
from struct import unpack
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import List, Optional, Tuple, Any, Union
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
class SegmentType(IntEnum):
|
||||||
|
"""
|
||||||
|
Metadata segment type.
|
||||||
|
"""
|
||||||
|
EndMarker = 0x0
|
||||||
|
DolbyE = 0x1
|
||||||
|
# Reserved2 = 0x2
|
||||||
|
DolbyDigital = 0x3
|
||||||
|
# Reserved4 = 0x4
|
||||||
|
# Reserved5 = 0x5
|
||||||
|
# Reserved6 = 0x6
|
||||||
|
DolbyDigitalPlus = 0x7
|
||||||
|
AudioInfo = 0x8
|
||||||
|
DolbyAtmos = 0x9
|
||||||
|
DolbyAtmosSupplemental = 0xa
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _missing_(cls,val):
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DolbyDigitalPlusMetadata:
|
||||||
|
"""
|
||||||
|
*Dolby Digital Plus* is Dolby's brand for multichannel surround
|
||||||
|
on discrete formats that aren't AC-3 (Dolby Digital) or Dolby E. This
|
||||||
|
metadata segment is present in ADM wave files created with a Dolby Atmos
|
||||||
|
Production Suite.
|
||||||
|
|
||||||
|
Where an AC-3 bitstream can contain multiple programs, a Dolby Digital
|
||||||
|
Plus bitstream will only contain one program.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class DownMixLevelToken(Enum):
|
||||||
|
"""
|
||||||
|
A gain coefficient used in several metadata fields for downmix
|
||||||
|
scenarios.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUS_3DB = 0b000
|
||||||
|
"+3 dB"
|
||||||
|
|
||||||
|
PLUS_1_5DB = 0b001
|
||||||
|
"+1.5 dB"
|
||||||
|
|
||||||
|
UNITY = 0b010
|
||||||
|
"0dB"
|
||||||
|
|
||||||
|
MINUS_1_5DB = 0b011
|
||||||
|
"-1.5 dB"
|
||||||
|
|
||||||
|
MINUS_3DB = 0b100
|
||||||
|
"-3 dB"
|
||||||
|
|
||||||
|
MINUS_4_5DB = 0b101
|
||||||
|
"-4.5 dB"
|
||||||
|
|
||||||
|
MINUS_6DB = 0b110
|
||||||
|
"-6 dB"
|
||||||
|
|
||||||
|
MUTE = 0b111
|
||||||
|
"-∞ dB"
|
||||||
|
|
||||||
|
|
||||||
|
class DolbySurroundEncodingMode(Enum):
|
||||||
|
"""
|
||||||
|
Dolby surround endcoding mode.
|
||||||
|
"""
|
||||||
|
RESERVED = 0b11
|
||||||
|
IN_USE = 0b10
|
||||||
|
NOT_IN_USE = 0b01
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
|
||||||
|
|
||||||
|
class BitStreamMode(Enum):
|
||||||
|
"""
|
||||||
|
Dolby Digital Plus `bsmod` field
|
||||||
|
§ 4.3.2.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPLETE_MAIN = 0b000
|
||||||
|
"main audio service: complete main"
|
||||||
|
|
||||||
|
MUSIC_AND_EFFECTS = 0b001
|
||||||
|
"main audio service: music and effects"
|
||||||
|
|
||||||
|
VISUALLY_IMPAIRED = 0b010
|
||||||
|
"associated service: visually impaired"
|
||||||
|
|
||||||
|
HEARING_IMPAIRED = 0b011
|
||||||
|
"associated service: hearing impaired"
|
||||||
|
|
||||||
|
DIALOGUE_ONLY = 0b100
|
||||||
|
"associated service: dialogue"
|
||||||
|
|
||||||
|
COMMENTARY = 0b101
|
||||||
|
"associated service: commentary"
|
||||||
|
|
||||||
|
EMERGENCY = 0b110
|
||||||
|
"associated service: emergency"
|
||||||
|
|
||||||
|
VOICEOVER_KARAOKE = 0b111
|
||||||
|
"""
|
||||||
|
associated service: voice over *OR* main audio service: karaoke.
|
||||||
|
If `acmod` is `0b001` (mono 1/0), this is voice-over, otherwise it
|
||||||
|
should be interpreted as karaoke.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class AudioCodingMode(Enum):
|
||||||
|
"""
|
||||||
|
Dolby Digital Plus `acmod` field
|
||||||
|
§ 4.3.2.3
|
||||||
|
"""
|
||||||
|
RESERVED = 0b000
|
||||||
|
CH_ORD_1_0 = 0b001
|
||||||
|
"Mono"
|
||||||
|
CH_ORD_2_0 = 0b010
|
||||||
|
"L/R stereo"
|
||||||
|
CH_ORD_3_0 = 0b011
|
||||||
|
"LCR stereo"
|
||||||
|
CH_ORD_2_1 = 0b100
|
||||||
|
"LR + mono surround"
|
||||||
|
CH_ORD_3_1 = 0b101
|
||||||
|
"LCR + mono surround"
|
||||||
|
CH_ORD_2_2 = 0b110
|
||||||
|
"LR + LR surround"
|
||||||
|
CH_ORD_3_2 = 0b111
|
||||||
|
"LCR + LR surround"
|
||||||
|
|
||||||
|
|
||||||
|
class CenterDownMixLevel(Enum):
|
||||||
|
"""
|
||||||
|
§ 4.3.3.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
DOWN_3DB = 0b00
|
||||||
|
"Attenuate 3 dB"
|
||||||
|
|
||||||
|
DOWN_45DB = 0b01
|
||||||
|
"Attenuate 4.5 dB"
|
||||||
|
|
||||||
|
DOWN_6DB = 0b10
|
||||||
|
"Attenuate 6 dB"
|
||||||
|
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class SurroundDownMixLevel(Enum):
|
||||||
|
"""
|
||||||
|
Dolby Digital Plus `surmixlev` field
|
||||||
|
§ 4.3.3.2
|
||||||
|
"""
|
||||||
|
DOWN_3DB = 0b00
|
||||||
|
DOWN_6DB = 0b01
|
||||||
|
MUTE = 0b10
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class LanguageCode(int):
|
||||||
|
"""
|
||||||
|
§ 4.3.4.1
|
||||||
|
|
||||||
|
Per ATSC/A52 § 5.4.2.12, this is not in use and always 0xFF.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MixLevel(int):
|
||||||
|
"""
|
||||||
|
§ 4.3.6.2
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DialnormLevel(int):
|
||||||
|
"""
|
||||||
|
§ 4.3.4.4
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RoomType(Enum):
|
||||||
|
"""
|
||||||
|
`roomtyp` 4.3.6.3
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
LARGE_ROOM_X_CURVE = 0b01
|
||||||
|
SMALL_ROOM_FLAT_CURVE = 0b10
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class PreferredDownMixMode(Enum):
|
||||||
|
"""
|
||||||
|
Indicates the creating engineer's preference of what the receiver should
|
||||||
|
downmix.
|
||||||
|
§ 4.3.8.1
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
PRO_LOGIC = 0b01
|
||||||
|
STEREO = 0b10
|
||||||
|
PRO_LOGIC_2 = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class SurroundEXMode(IntEnum):
|
||||||
|
"""
|
||||||
|
Dolby Surround-EX mode.
|
||||||
|
`dsurexmod` § 4.3.9.1
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
NOT_SEX = 0b01
|
||||||
|
SEX = 0b10
|
||||||
|
PRO_LOGIC_2 = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class HeadphoneMode(IntEnum):
|
||||||
|
"""
|
||||||
|
`dheadphonmod` § 4.3.9.2
|
||||||
|
"""
|
||||||
|
NOT_INDICATED = 0b00
|
||||||
|
NOT_DOLBY_HEADPHONE = 0b01
|
||||||
|
DOLBY_HEADPHONE = 0b10
|
||||||
|
RESERVED = 0b11
|
||||||
|
|
||||||
|
|
||||||
|
class ADConverterType(Enum):
|
||||||
|
STANDARD = 0
|
||||||
|
HDCD = 1
|
||||||
|
|
||||||
|
|
||||||
|
class StreamDependency(Enum):
|
||||||
|
"""
|
||||||
|
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
INDEPENDENT = 0
|
||||||
|
DEPENDENT = 1
|
||||||
|
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
|
||||||
|
RESERVED = 3
|
||||||
|
|
||||||
|
|
||||||
|
class RFCompressionProfile(Enum):
|
||||||
|
"""
|
||||||
|
`compr1` RF compression profile
|
||||||
|
§ 4.3.10 (fig 42)
|
||||||
|
"""
|
||||||
|
NONE = 0
|
||||||
|
FILM_STANDARD = 1
|
||||||
|
FILM_LIGHT = 2
|
||||||
|
MUSIC_STANDARD = 3
|
||||||
|
MUSIC_LIGHT = 4
|
||||||
|
SPEECH = 5
|
||||||
|
|
||||||
|
#: Program ID number, this identifies the program in a multi-program
|
||||||
|
#: element. § 4.3.1
|
||||||
|
program_id: int
|
||||||
|
|
||||||
|
#: `True` if LFE is enabled. § 4.3.2.1
|
||||||
|
lfe_on: bool
|
||||||
|
|
||||||
|
#: The kind of service of this stream. `bsmod` § 4.3.2.2
|
||||||
|
bitstream_mode: BitStreamMode
|
||||||
|
|
||||||
|
#: Indicates which channels are in use. `acmod` § 4.3.2.3
|
||||||
|
audio_coding_mode: AudioCodingMode
|
||||||
|
|
||||||
|
#: When the front three channels are in use, gives the center
|
||||||
|
#: downmix level. ``
|
||||||
|
center_downmix_level: CenterDownMixLevel
|
||||||
|
|
||||||
|
#: When the surround channels are in use, gives the surround
|
||||||
|
#: downmix level.
|
||||||
|
surround_downmix_level: SurroundDownMixLevel
|
||||||
|
|
||||||
|
#: If the `acmod` is LR, this indicates if the channels
|
||||||
|
#: are encoded in Dolby Surround.
|
||||||
|
dolby_surround_encoded: DolbySurroundEncodingMode
|
||||||
|
|
||||||
|
#: `True` if there is a langcode present in the metadata.
|
||||||
|
langcode_present: bool
|
||||||
|
|
||||||
|
#: `True` if this bitstream is copyrighted.
|
||||||
|
copyright_bitstream: bool
|
||||||
|
|
||||||
|
#: `True` if this bitstream is original.
|
||||||
|
original_bitstream: bool
|
||||||
|
|
||||||
|
dialnorm: DialnormLevel
|
||||||
|
|
||||||
|
#: Language code
|
||||||
|
langcode: int
|
||||||
|
|
||||||
|
#: `True` if `mixlevel` and `roomtype` are valid
|
||||||
|
prod_info_exists: bool
|
||||||
|
|
||||||
|
#: Mix level
|
||||||
|
mixlevel: MixLevel
|
||||||
|
|
||||||
|
#: Room Type
|
||||||
|
roomtype: RoomType
|
||||||
|
|
||||||
|
#: LoRo preferred center downmix level
|
||||||
|
loro_center_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: LoRo preferred surround downmix level
|
||||||
|
loro_surround_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: Preferred downmix mode
|
||||||
|
downmix_mode: PreferredDownMixMode
|
||||||
|
|
||||||
|
#: LtRt preferred center downmix level
|
||||||
|
ltrt_center_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: LtRt preferred surround downmix level
|
||||||
|
ltrt_surround_downmix_level: DownMixLevelToken
|
||||||
|
|
||||||
|
#: Surround-EX mode
|
||||||
|
surround_ex_mode: SurroundEXMode
|
||||||
|
|
||||||
|
#: Dolby Headphone mode
|
||||||
|
dolby_headphone_encoded: HeadphoneMode
|
||||||
|
|
||||||
|
ad_converter_type: ADConverterType
|
||||||
|
compression_profile: RFCompressionProfile
|
||||||
|
dynamic_range: RFCompressionProfile
|
||||||
|
|
||||||
|
#: Indicates if this stream can be decoded independently or not
|
||||||
|
stream_dependency: StreamDependency
|
||||||
|
|
||||||
|
#: Data rate of this bitstream in kilobits per second
|
||||||
|
datarate_kbps: int
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(buffer: bytes):
|
||||||
|
assert len(buffer) == 96, "Dolby Digital Plus segment incorrect size, "
|
||||||
|
"expected 96 got %i" % len(buffer)
|
||||||
|
|
||||||
|
def program_id(b) -> int:
|
||||||
|
return b
|
||||||
|
|
||||||
|
def program_info(b):
|
||||||
|
return (b & 0x40) > 0, \
|
||||||
|
DolbyDigitalPlusMetadata.BitStreamMode(b & 0x38 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.AudioCodingMode(b & 0x7)
|
||||||
|
|
||||||
|
def ddplus_reserved1(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def surround_config(b):
|
||||||
|
return DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4), \
|
||||||
|
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2), \
|
||||||
|
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3)
|
||||||
|
|
||||||
|
def dialnorm_info(b):
|
||||||
|
return (b & 0x80) > 0 , b & 0x40 > 0, b & 0x20 > 0, \
|
||||||
|
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
|
||||||
|
|
||||||
|
def langcod(b) -> int:
|
||||||
|
return b
|
||||||
|
|
||||||
|
def audio_prod_info(b):
|
||||||
|
return (b & 0x80) > 0, \
|
||||||
|
DolbyDigitalPlusMetadata.MixLevel(b & 0x7c >> 2), \
|
||||||
|
DolbyDigitalPlusMetadata.RoomType(b & 0x3)
|
||||||
|
|
||||||
|
# loro_center_downmix_level, loro_surround_downmix_level
|
||||||
|
def ext_bsi1_word1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
||||||
|
|
||||||
|
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
|
||||||
|
def ext_bsi1_word2(b):
|
||||||
|
return DolbyDigitalPlusMetadata.PreferredDownMixMode(b & 0xC0 >> 6), \
|
||||||
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
||||||
|
|
||||||
|
#surround_ex_mode, dolby_headphone_encoded, ad_converter_type
|
||||||
|
def ext_bsi2_word1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \
|
||||||
|
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \
|
||||||
|
DolbyDigitalPlusMetadata.ADConverterType( b & 0x4 >> 2)
|
||||||
|
|
||||||
|
def ddplus_reserved2(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def compr1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.RFCompressionProfile(b)
|
||||||
|
|
||||||
|
def dynrng1(b):
|
||||||
|
DolbyDigitalPlusMetadata.RFCompressionProfile(b)
|
||||||
|
|
||||||
|
def ddplus_reserved3(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ddplus_info1(b):
|
||||||
|
return DolbyDigitalPlusMetadata.StreamDependency(b & 0xc >> 2)
|
||||||
|
|
||||||
|
def ddplus_reserved4(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def datarate(b) -> int:
|
||||||
|
return unpack("<H", b)[0]
|
||||||
|
|
||||||
|
def reserved(_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
pid = program_id(buffer[0])
|
||||||
|
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
|
||||||
|
ddplus_reserved1(buffer[2:2])
|
||||||
|
center_downmix_level, surround_downmix_level, dolby_surround_encoded = surround_config(buffer[4])
|
||||||
|
langcode_present, copyright_bitstream, original_bitstream, dialnorm = dialnorm_info(buffer[5])
|
||||||
|
langcode = langcod(buffer[6])
|
||||||
|
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
|
||||||
|
|
||||||
|
loro_center_downmix_level, loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
|
||||||
|
downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9])
|
||||||
|
surround_ex_mode, dolby_headphone_encoded, ad_converter_type = ext_bsi2_word1(buffer[10])
|
||||||
|
|
||||||
|
ddplus_reserved2(buffer[11:14])
|
||||||
|
compression = compr1(buffer[14])
|
||||||
|
dynamic_range = dynrng1(buffer[15])
|
||||||
|
ddplus_reserved3(buffer[16:19])
|
||||||
|
stream_info = ddplus_info1(buffer[19])
|
||||||
|
ddplus_reserved4(buffer[20:25])
|
||||||
|
data_rate = datarate(buffer[25:27])
|
||||||
|
reserved(buffer[27:69])
|
||||||
|
|
||||||
|
return DolbyDigitalPlusMetadata(program_id=pid,
|
||||||
|
lfe_on=lfe_on,
|
||||||
|
bitstream_mode=bitstream_mode,
|
||||||
|
audio_coding_mode=audio_coding_mode,
|
||||||
|
center_downmix_level=center_downmix_level,
|
||||||
|
surround_downmix_level=surround_downmix_level,
|
||||||
|
dolby_surround_encoded=dolby_surround_encoded,
|
||||||
|
langcode_present=langcode_present,
|
||||||
|
copyright_bitstream=copyright_bitstream,
|
||||||
|
original_bitstream=original_bitstream,
|
||||||
|
dialnorm=dialnorm,
|
||||||
|
langcode=langcode,
|
||||||
|
prod_info_exists=prod_info_exists,
|
||||||
|
mixlevel=mixlevel,
|
||||||
|
roomtype=roomtype,
|
||||||
|
loro_center_downmix_level=loro_center_downmix_level,
|
||||||
|
loro_surround_downmix_level=loro_surround_downmix_level,
|
||||||
|
downmix_mode=downmix_mode,
|
||||||
|
ltrt_center_downmix_level=ltrt_center_downmix_level,
|
||||||
|
ltrt_surround_downmix_level=ltrt_surround_downmix_level,
|
||||||
|
surround_ex_mode=surround_ex_mode,
|
||||||
|
dolby_headphone_encoded=dolby_headphone_encoded,
|
||||||
|
ad_converter_type=ad_converter_type,
|
||||||
|
compression_profile=compression,
|
||||||
|
dynamic_range=dynamic_range,
|
||||||
|
stream_dependency=stream_info,
|
||||||
|
datarate_kbps=data_rate)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DolbyAtmosMetadata:
|
||||||
|
"""
|
||||||
|
Dolby Atmos Metadata Segment
|
||||||
|
|
||||||
|
https://github.com/DolbyLaboratories/dbmd-atmos-parser/
|
||||||
|
"""
|
||||||
|
|
||||||
|
class WarpMode(Enum):
|
||||||
|
NORMAL = 0x00
|
||||||
|
WARPING = 0x01
|
||||||
|
DOWNMIX_PLIIX = 0x02
|
||||||
|
DOWNMIX_LORO = 0x03
|
||||||
|
NOT_INDICATED = 0x04
|
||||||
|
|
||||||
|
tool_name: str
|
||||||
|
tool_version: Tuple[int,int,int]
|
||||||
|
warp_mode: WarpMode
|
||||||
|
|
||||||
|
SEGMENT_LENGTH = 248
|
||||||
|
TOOL_NAME_LENGTH = 64
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, data: bytes):
|
||||||
|
assert len(data) == cls.SEGMENT_LENGTH, "DolbyAtmosMetadata segment "\
|
||||||
|
"is incorrect length, expected %i actual was %i" % (cls.SEGMENT_LENGTH, len(data))
|
||||||
|
|
||||||
|
h = BytesIO(data)
|
||||||
|
|
||||||
|
h.seek(32, 1)
|
||||||
|
toolname = h.read(cls.TOOL_NAME_LENGTH)
|
||||||
|
toolname = unpack("%is" % cls.TOOL_NAME_LENGTH, toolname)[0]
|
||||||
|
toolname = toolname.decode('utf-8').strip('\0')
|
||||||
|
|
||||||
|
vers = h.read(3)
|
||||||
|
major, minor, fix = unpack("BBB", vers)
|
||||||
|
|
||||||
|
h.seek(53, 1)
|
||||||
|
|
||||||
|
a_val = unpack("B", h.read(1))[0]
|
||||||
|
warp_mode = a_val & 0x7
|
||||||
|
|
||||||
|
return DolbyAtmosMetadata(tool_name=toolname,
|
||||||
|
tool_version=(major, minor, fix), warp_mode=DolbyAtmosMetadata.WarpMode(warp_mode))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DolbyAtmosSupplementalMetadata:
|
||||||
|
"""
|
||||||
|
Dolby Atmos supplemental metadata segment.
|
||||||
|
|
||||||
|
https://github.com/DolbyLaboratories/dbmd-atmos-parser/blob/master/dbmd_atmos_parse/src/dbmd_atmos_parse.c
|
||||||
|
"""
|
||||||
|
|
||||||
|
class BinauralRenderMode(Enum):
|
||||||
|
BYPASS = 0x00
|
||||||
|
NEAR = 0x01
|
||||||
|
FAR = 0x02
|
||||||
|
MID = 0x03
|
||||||
|
NOT_INDICATED = 0x04
|
||||||
|
|
||||||
|
|
||||||
|
object_count: int
|
||||||
|
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
|
||||||
|
trim_modes: List[int]
|
||||||
|
|
||||||
|
|
||||||
|
MAGIC = 0xf8726fbd
|
||||||
|
TRIM_CONFIG_COUNT = 9
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, data: bytes):
|
||||||
|
|
||||||
|
trim_modes = []
|
||||||
|
render_modes = []
|
||||||
|
|
||||||
|
h = BytesIO(data)
|
||||||
|
magic = unpack("<I", h.read(4))
|
||||||
|
assert magic == cls.MAGIC, "Magic value was not found"
|
||||||
|
|
||||||
|
object_count = unpack("<H", h.read(2))
|
||||||
|
|
||||||
|
h.read(1) #skip 1
|
||||||
|
|
||||||
|
for _ in range(cls.TRIM_CONFIG_COUNT):
|
||||||
|
auto_trim = unpack("B", h.read(1))
|
||||||
|
trim_modes.append(auto_trim)
|
||||||
|
|
||||||
|
h.read(14) #skip 14
|
||||||
|
|
||||||
|
h.read(object_count) # skip object_count bytes
|
||||||
|
|
||||||
|
for _ in range(object_count):
|
||||||
|
binaural_mode = unpack("B", h.read(1))
|
||||||
|
binaural_mode &= 0x7
|
||||||
|
render_modes.append(binaural_mode)
|
||||||
|
|
||||||
|
return DolbyAtmosSupplementalMetadata(object_count=object_count,
|
||||||
|
render_modes=render_modes,trim_modes=trim_modes)
|
||||||
|
|
||||||
|
|
||||||
|
class WavDolbyMetadataReader:
|
||||||
|
"""
|
||||||
|
Reads Dolby bitstream metadata.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: List of the Dolby Metadata Segments.
|
||||||
|
#:
|
||||||
|
#: Each list entry is a tuple of `SegmentType`, a `bool`
|
||||||
|
#: indicating if the segment's checksum was valid, and the
|
||||||
|
#: segment's parsed dataclass (or a `bytes` array if it was
|
||||||
|
#: not recognized).
|
||||||
|
segment_list: Tuple[Union[SegmentType, int], bool, Any]
|
||||||
|
|
||||||
|
version: Tuple[int,int,int,int]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def segment_checksum(bs: bytes, size: int):
|
||||||
|
retval = size
|
||||||
|
for b in bs:
|
||||||
|
retval += int(b)
|
||||||
|
retval &= 0xff
|
||||||
|
|
||||||
|
retval = ((~retval) + 1) & 0xff
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, dbmd_data) -> None:
|
||||||
|
self.segment_list = []
|
||||||
|
|
||||||
|
h = BytesIO(dbmd_data)
|
||||||
|
|
||||||
|
v_vec = []
|
||||||
|
for _ in range(4):
|
||||||
|
b = h.read(1)
|
||||||
|
v_vec.insert(0, unpack("B",b)[0])
|
||||||
|
|
||||||
|
self.version = tuple(v_vec)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
stype= SegmentType(unpack("B", h.read(1))[0])
|
||||||
|
if stype == SegmentType.EndMarker:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
seg_size = unpack("<H", h.read(2))[0]
|
||||||
|
seg_payload = h.read(seg_size)
|
||||||
|
expected_checksum = WavDolbyMetadataReader.segment_checksum(seg_payload, seg_size)
|
||||||
|
checksum = unpack("B", h.read(1))[0]
|
||||||
|
|
||||||
|
segment = seg_payload
|
||||||
|
if stype == SegmentType.DolbyDigitalPlus:
|
||||||
|
segment = DolbyDigitalPlusMetadata.load(segment)
|
||||||
|
elif stype == SegmentType.DolbyAtmos:
|
||||||
|
segment = DolbyAtmosMetadata.load(segment)
|
||||||
|
# elif stype == SegmentType.DolbyAtmosSupplemental:
|
||||||
|
# segment = DolbyAtmosSupplementalMetadata.load(segment)
|
||||||
|
|
||||||
|
self.segment_list.append( (stype, checksum == expected_checksum, segment) )
|
||||||
|
|
||||||
|
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
|
||||||
|
"""
|
||||||
|
Every valid Dolby Digital Plus metadata segment in the file.
|
||||||
|
"""
|
||||||
|
return [x[2] for x in self.segment_list \
|
||||||
|
if x[0] == SegmentType.DolbyDigitalPlus and x[1]]
|
||||||
|
|
||||||
|
def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
|
||||||
|
"""
|
||||||
|
Every valid Dolby Atmos metadata segment in the file.
|
||||||
|
"""
|
||||||
|
return [x[2] for x in self.segment_list \
|
||||||
|
if x[0] == SegmentType.DolbyAtmos and x[1]]
|
||||||
|
|
||||||
|
# def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
||||||
|
# """
|
||||||
|
# Every valid Dolby Atmos Supplemental metadata segment in the file.
|
||||||
|
# """
|
||||||
|
# return [x[2] for x in self.segment_list \
|
||||||
|
# if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
|
||||||
|
ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
|
||||||
|
atmos = map(lambda x: asdict(x), self.dolby_atmos())
|
||||||
|
#atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
|
||||||
|
|
||||||
|
return dict(dolby_digital_plus=list(ddp),
|
||||||
|
dolby_atmos=list(atmos))
|
||||||
@@ -27,6 +27,9 @@ class WavIXMLFormat:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def xml_bytes(self):
|
||||||
|
return ET.tostring(self.parsed).decode("utf-8")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def raw_xml(self):
|
def raw_xml(self):
|
||||||
"""
|
"""
|
||||||
@@ -38,7 +41,7 @@ class WavIXMLFormat:
|
|||||||
def track_list(self):
|
def track_list(self):
|
||||||
"""
|
"""
|
||||||
A description of each track.
|
A description of each track.
|
||||||
:return: An Iterator
|
:returns: An Iterator
|
||||||
"""
|
"""
|
||||||
for track in self.parsed.find("./TRACK_LIST").iter():
|
for track in self.parsed.find("./TRACK_LIST").iter():
|
||||||
if track.tag == 'TRACK':
|
if track.tag == 'TRACK':
|
||||||
@@ -89,3 +92,13 @@ class WavIXMLFormat:
|
|||||||
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
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ import struct
|
|||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from typing import Optional, Generator, Any
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
|
from .wave_dbmd_reader import WavDolbyMetadataReader
|
||||||
|
|
||||||
#: 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')
|
||||||
@@ -21,6 +25,8 @@ WavAudioFormat = namedtuple('WavAudioFormat',
|
|||||||
class WavInfoReader:
|
class WavInfoReader:
|
||||||
"""
|
"""
|
||||||
Parse a WAV audio file for metadata.
|
Parse a WAV audio file for metadata.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
|
def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
|
||||||
@@ -39,6 +45,8 @@ class WavInfoReader:
|
|||||||
The text encoding to use when decoding the string
|
The text encoding to use when decoding the string
|
||||||
fields of the Broadcast-WAV extension. Per EBU 3285 this is ASCII
|
fields of the Broadcast-WAV extension. Per EBU 3285 this is ASCII
|
||||||
but this parameter is available to you if you encounter a weirdo.
|
but this parameter is available to you if you encounter a weirdo.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.info_encoding = info_encoding
|
self.info_encoding = info_encoding
|
||||||
@@ -52,10 +60,30 @@ class WavInfoReader:
|
|||||||
absolute_path = os.path.abspath(path)
|
absolute_path = os.path.abspath(path)
|
||||||
|
|
||||||
#: `file://` url for the file.
|
#: `file://` url for the file.
|
||||||
self.url = pathlib.Path(absolute_path).as_uri()
|
self.url: pathlib.Path = pathlib.Path(absolute_path).as_uri()
|
||||||
|
|
||||||
# for __repr__()
|
|
||||||
self.path = absolute_path
|
self.path = absolute_path
|
||||||
|
|
||||||
|
#: Wave audio data format.
|
||||||
|
self.fmt :Optional[WavAudioFormat] = None
|
||||||
|
|
||||||
|
#: Statistics of the `data` section.
|
||||||
|
self.data :Optional[WavDataDescriptor] = None
|
||||||
|
|
||||||
|
#: Broadcast-Wave metadata.
|
||||||
|
self.bext :Optional[WavBextReader] = None
|
||||||
|
|
||||||
|
#: iXML metadata.
|
||||||
|
self.ixml :Optional[WavIXMLFormat] = None
|
||||||
|
|
||||||
|
#: ADM Audio Definiton Model metadata.
|
||||||
|
self.adm :Optional[WavADMReader]= None
|
||||||
|
|
||||||
|
#: Dolby bitstream metadata.
|
||||||
|
self.dolby :Optional[WavDolbyMetadataReader] = None
|
||||||
|
|
||||||
|
#: RIFF INFO metadata.
|
||||||
|
self.info :Optional[WavInfoChunkReader]= None
|
||||||
|
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
self.get_wav_info(f)
|
self.get_wav_info(f)
|
||||||
@@ -66,17 +94,12 @@ class WavInfoReader:
|
|||||||
self.main_list = chunks.children
|
self.main_list = chunks.children
|
||||||
wavfile.seek(0)
|
wavfile.seek(0)
|
||||||
|
|
||||||
#: :class:`wavinfo.wave_reader.WavAudioFormat`
|
|
||||||
self.fmt = self._get_format(wavfile)
|
self.fmt = self._get_format(wavfile)
|
||||||
|
|
||||||
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
|
|
||||||
self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
|
self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
|
||||||
|
|
||||||
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
|
|
||||||
self.ixml = self._get_ixml(wavfile)
|
self.ixml = self._get_ixml(wavfile)
|
||||||
|
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.info = self._get_info(wavfile, encoding=self.info_encoding)
|
||||||
|
self.dolby = self._get_dbmd(wavfile)
|
||||||
self.data = self._describe_data()
|
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):
|
||||||
@@ -85,7 +108,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))
|
||||||
@@ -130,34 +153,40 @@ 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_dbmd(self, f):
|
||||||
|
dbmd_data = self._find_chunk_data(b'dbmd', f, default_none=True)
|
||||||
|
return WavDolbyMetadataReader(dbmd_data=dbmd_data) if dbmd_data 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 WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
|
||||||
|
|
||||||
def walk(self):
|
def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()"
|
||||||
"""
|
"""
|
||||||
Walk all of the available metadata fields.
|
Walk all of the available metadata fields.
|
||||||
|
|
||||||
:yields: a string, the :scope: of the metadatum, the string :name: of the
|
:yields: tuples of the *scope*, *key*, and *value* of
|
||||||
metadata field, and the value.
|
each metadatum. The *scope* value will be one of
|
||||||
|
"fmt", "data", "ixml", "bext", "info", "dolby", or "adm".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ('fmt', 'data') # 'bext', 'ixml', 'info')
|
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'dolby')
|
||||||
|
|
||||||
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:
|
|
||||||
bext_dict = (self.bext or {}).to_dict()
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
dict = self.__getattribute__(scope).to_dict() if self.__getattribute__(scope) else {}
|
||||||
|
for key in dict.keys():
|
||||||
|
yield scope, key, dict[key]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)
|
return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)
|
||||||
|
|||||||
Reference in New Issue
Block a user