mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c6871bbc | ||
|
|
c62dfb7a8a | ||
|
|
6c8cc47788 | ||
|
|
a90d3f4b38 | ||
|
|
3ede4de06a | ||
|
|
5d4bb13ad6 | ||
|
|
e132c5846c | ||
|
|
b87b43aab4 | ||
|
|
6b42a6bb09 | ||
|
|
7a315be242 | ||
|
|
eb3e2adc27 | ||
|
|
3e7faefedb | ||
|
|
54ce5abe77 | ||
|
|
2f124b0d56 | ||
|
|
cd11b0924b | ||
|
|
ff862aafe9 | ||
|
|
e0432458cc | ||
|
|
b4613ed6f4 | ||
|
|
8d44d411d7 | ||
|
|
6005f79e60 | ||
|
|
841b86f3f4 | ||
|
|
5c90d5ff47 | ||
|
|
60e329fdb4 | ||
|
|
15db4c9ffa | ||
|
|
49ac961b94 | ||
|
|
7fc530b2cd | ||
|
|
4dfc1ab33c | ||
|
|
4770c781b2 | ||
|
|
45c2aae640 | ||
|
|
792e5505b6 | ||
|
|
8f575c5131 | ||
|
|
128dffef4e | ||
|
|
ae1594d496 | ||
|
|
b5713479cd | ||
|
|
f1be6852b6 | ||
|
|
242fa51b32 | ||
|
|
177e913c86 | ||
|
|
1827b46e34 | ||
|
|
4b6407c1f4 | ||
|
|
99bfa99b3b | ||
|
|
b7aeccacf5 | ||
|
|
9d5f8899d5 | ||
|
|
0ce18d9f13 | ||
|
|
3f3fbc7632 | ||
|
|
809ff71bb4 | ||
|
|
a6ed0152db | ||
|
|
526bc0c29c | ||
|
|
d3c8207a33 | ||
|
|
185c810e7c | ||
|
|
ebc224d5d2 | ||
|
|
976d5844e2 | ||
|
|
f1fb83f208 | ||
|
|
8d0d51b7fa | ||
|
|
188ba855b2 | ||
|
|
2f1511d935 | ||
|
|
8c8fdc1bb3 | ||
|
|
8564fd1fd8 | ||
|
|
77aadd563f | ||
|
|
c2ddee8f6a | ||
|
|
dc08bd39fe | ||
|
|
12de7b078f | ||
|
|
7992640fb8 | ||
|
|
88da763ca1 | ||
|
|
1e0f31a794 | ||
|
|
0e1094421e | ||
|
|
4f0c26f5ca | ||
|
|
71f0aed5ff | ||
|
|
f8feec8119 | ||
|
|
16bc4b016c | ||
|
|
83bf656ad3 | ||
|
|
9e259e9d6c | ||
|
|
c20b17e82c | ||
|
|
5315575e35 | ||
|
|
52613b78f3 | ||
|
|
0c24c2d986 | ||
|
|
2a86d7824d | ||
|
|
c2be0e77c5 | ||
|
|
9b8b964e74 | ||
|
|
488c0f2aa9 | ||
|
|
da51b22c59 | ||
|
|
a4ffe7dd6b | ||
|
|
0ec5425cd8 | ||
|
|
8e965f53e5 | ||
|
|
9e16f6ab1f | ||
|
|
477c71830e | ||
|
|
ed8b5f167e | ||
|
|
97c25ab61f | ||
|
|
910b3854c7 | ||
|
|
f12bb0eea4 | ||
|
|
c88599f4fd | ||
|
|
68ccb09f53 | ||
|
|
b7d9f6758d | ||
|
|
e3adde7498 | ||
|
|
96885dfb4e | ||
|
|
126f1ea7c3 | ||
|
|
0bb664357e | ||
|
|
9e2c60caf8 | ||
|
|
82d73b0316 | ||
|
|
0612e62a7b | ||
|
|
8fd509482c | ||
|
|
a9dbfdf5ec | ||
|
|
413826b18f | ||
|
|
71201adee4 | ||
|
|
d88117878c | ||
|
|
b20c5dd1bd | ||
|
|
6c9fb38482 | ||
|
|
f5c0700e47 | ||
|
|
3c101badac | ||
|
|
50962aefcc | ||
|
|
f52b2a195a | ||
|
|
4586d19a5f | ||
|
|
3e897d030c | ||
|
|
9f943aeb61 | ||
|
|
a9c3600ad2 | ||
|
|
27d9a2f005 | ||
|
|
0c5c0a2088 | ||
|
|
b150cd6d8e | ||
|
|
849ade92a4 | ||
|
|
482a3f86d1 | ||
|
|
991a12cbb5 | ||
|
|
37b816045d | ||
|
|
99aa29c5f3 |
13
.coveragerc
Normal file
13
.coveragerc
Normal file
@@ -0,0 +1,13 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = wavinfo
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
if self.debug:
|
||||
pragma: no cover
|
||||
raise NotImplementedError
|
||||
if __name__ == .__main__.:
|
||||
ignore_errors = True
|
||||
omit =
|
||||
tests/*
|
||||
26
.github/workflows/pythonpublish.yml
vendored
Normal file
26
.github/workflows/pythonpublish.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine lxml
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -105,3 +105,4 @@ venv.bak/
|
||||
|
||||
# vim swap
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
# Default ignored files
|
||||
/workspace.xml
|
||||
8
.idea/dictionaries/jamiehardt.xml
generated
Normal file
8
.idea/dictionaries/jamiehardt.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="jamiehardt">
|
||||
<words>
|
||||
<w>ident</w>
|
||||
<w>umid</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?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
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?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
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
11
.idea/wavinfo.iml
generated
Normal file
11
.idea/wavinfo.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
23
.travis.yml
23
.travis.yml
@@ -1,11 +1,26 @@
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
# - "2.7"
|
||||
- "3.6"
|
||||
- "3.5"
|
||||
- "3.4"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
script:
|
||||
- "python3 setup.py test"
|
||||
- "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 install -y ffmpeg
|
||||
- "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:
|
||||
- "pip3 install setuptools"
|
||||
- "pip install setuptools"
|
||||
# after_success:
|
||||
# - "codecov"
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Jamie Hardt
|
||||
Copyright (c) 2020 Jamie Hardt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
87
README.md
87
README.md
@@ -1,38 +1,38 @@
|
||||
[](https://travis-ci.com/iluvcapra/wavinfo)
|
||||
[](https://wavinfo.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/wavinfo/) 
|
||||
|
||||
|
||||
# wavinfo
|
||||
|
||||
|
||||
The `wavinfo` package allows you to probe WAVE files and extract extended metadata, with an emphasis on
|
||||
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:
|
||||
|
||||
* __Broadcast-WAVE__ metadata, compliant with [EBU Tech 3285v2 (2011)][ebu], including embedded program loudness and coding history, if extant.
|
||||
* [__iXML__ production recorder metadata][ixml], including project, scene, and take tags, recorder notes and file family information.
|
||||
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count information.
|
||||
* __Broadcast-WAVE__ metadata, compliant with [EBU Tech 3285v2 (2011)][ebu], including embedded program
|
||||
loudness and coding history, if extant. This also includes the [SMPTE 330M __UMID__][smpte_330m2011]
|
||||
Unique Materials Identifier.
|
||||
* [__iXML__ production recorder metadata][ixml], including project, scene, and take tags, recorder notes
|
||||
and file family information.
|
||||
* Most of the common __RIFF INFO__ metadata fields.
|
||||
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count
|
||||
information.
|
||||
|
||||
In progress:
|
||||
* [SMPTE 330M __UMID__][smpte_330m2011] Unique Materials Identifier.
|
||||
* iXML `STEINBERG` sound library attributes.
|
||||
* Most of the common __RIFF INFO__ metadata fields.
|
||||
* Pro Tools __embedded regions__.
|
||||
|
||||
This module is presently under construction and not sutiable for production at this time.
|
||||
|
||||
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
|
||||
[smpte_330m2011]:http://standards.smpte.org/content/978-1-61482-678-1/st-330-2011/SEC1.abstract
|
||||
[ixml]:http://www.ixml.info
|
||||
[eburf64]:https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
|
||||
|
||||
|
||||
|
||||
## Demonstration
|
||||
|
||||
|
||||
The entry point for wavinfo is the WavInfoReader class.
|
||||
|
||||
|
||||
```python
|
||||
from wavinfo import WavInfoReader
|
||||
|
||||
@@ -41,12 +41,16 @@ path = '../tests/test_files/A101_1.WAV'
|
||||
info = WavInfoReader(path)
|
||||
```
|
||||
|
||||
The package also installs a shell command:
|
||||
|
||||
```sh
|
||||
$ 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)
|
||||
@@ -54,61 +58,10 @@ The length of the file in frames (interleaved samples) and bytes is available, a
|
||||
>>> (48000, 2, 6, 24)
|
||||
```
|
||||
|
||||
## Broadcast WAV Extension
|
||||
## Other Resources
|
||||
|
||||
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).
|
||||
|
||||
|
||||
|
||||
```python
|
||||
print(info.bext.description)
|
||||
print("----------")
|
||||
print("Originator:", info.bext.originator)
|
||||
print("Originator Ref:", info.bext.originator_ref)
|
||||
print("Originator Date:", info.bext.originator_date)
|
||||
print("Originator Time:", info.bext.originator_time)
|
||||
print("Time Reference:", info.bext.time_reference)
|
||||
print(info.bext.coding_history)
|
||||
```
|
||||
|
||||
sSPEED=023.976-ND
|
||||
sTAKE=1
|
||||
sUBITS=$12311801
|
||||
sSWVER=2.67
|
||||
sPROJECT=BMH
|
||||
sSCENE=A101
|
||||
sFILENAME=A101_1.WAV
|
||||
sTAPE=18Y12M31
|
||||
sTRK1=MKH516 A
|
||||
sTRK2=Boom
|
||||
sNOTE=
|
||||
|
||||
----------
|
||||
Originator: Sound Dev: 702T S#GR1112089007
|
||||
Originator Ref: USSDVGR1112089007124001008206301
|
||||
Originator Date: 2018-12-31
|
||||
Originator Time: 12:40:00
|
||||
Time Reference: 2190940753
|
||||
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
||||
|
||||
|
||||
|
||||
## iXML Production Recorder Metadata
|
||||
|
||||
|
||||
```python
|
||||
print("iXML Project:", info.ixml.project)
|
||||
print("iXML Scene:", info.ixml.scene)
|
||||
print("iXML Take:", info.ixml.take)
|
||||
print("iXML Tape:", info.ixml.tape)
|
||||
print("iXML File Family Name:", info.ixml.family_name)
|
||||
print("iXML File Family UID:", info.ixml.family_uid)
|
||||
```
|
||||
|
||||
iXML Project: BMH
|
||||
iXML Scene: A101
|
||||
iXML Take: 1
|
||||
iXML Tape: 18Y12M31
|
||||
iXML File Family Name: None
|
||||
iXML File Family UID: USSDVGR1112089007124001008206300
|
||||
|
||||
|
||||
|
||||
|
||||
19
docs/Makefile
Normal file
19
docs/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
184
docs/conf.py
Normal file
184
docs/conf.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'wavinfo'
|
||||
copyright = u'2019, Jamie Hardt'
|
||||
author = u'Jamie Hardt'
|
||||
|
||||
# The short X.Y version
|
||||
version = u''
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = u'v1.1'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = None
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'wavinfodoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'wavinfo.tex', u'wavinfo Documentation',
|
||||
u'Jamie Hardt', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'wavinfo', u'wavinfo Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'wavinfo', u'wavinfo Documentation',
|
||||
author, 'wavinfo', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#
|
||||
# epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# -- Options for todo extension ----------------------------------------------
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
40
docs/index.rst
Normal file
40
docs/index.rst
Normal file
@@ -0,0 +1,40 @@
|
||||
.. wavinfo documentation master file, created by
|
||||
sphinx-quickstart on Thu Jan 3 17:09:28 2019.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to wavinfo's documentation!
|
||||
===================================
|
||||
|
||||
|
||||
.. module:: wavinfo
|
||||
|
||||
.. autoclass:: WavInfoReader
|
||||
:members:
|
||||
|
||||
.. automethod:: __init__
|
||||
|
||||
|
||||
.. autoclass:: wavinfo.wave_reader.WavAudioFormat
|
||||
:members:
|
||||
|
||||
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
|
||||
:members:
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Notes:
|
||||
|
||||
metadata_scopes/bext.rst
|
||||
metadata_scopes/ixml.rst
|
||||
metadata_scopes/info.rst
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@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
|
||||
65
docs/metadata_scopes/bext.rst
Normal file
65
docs/metadata_scopes/bext.rst
Normal file
@@ -0,0 +1,65 @@
|
||||
Broadcast WAV Extension
|
||||
=======================
|
||||
|
||||
.. module:: wavinfo
|
||||
|
||||
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
|
||||
:members:
|
||||
|
||||
|
||||
Notes
|
||||
-----
|
||||
A WAV file produced to Broadcast-WAV specifications will have the broadcast metadata extension,
|
||||
which includes a 256-character free text descrption, creating entity identifier (usually the
|
||||
recording application or equipment), the date and time of recording and a time reference for
|
||||
timecode synchronization.
|
||||
|
||||
The `coding_history` 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
|
||||
information in the `description`. Here also the `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
|
||||
330M 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,
|
||||
and geolocation data.
|
||||
|
||||
If the bext metadata conforms to EBU 3285 v2, it will hold precomputed program loudness values
|
||||
as described by EBU Rec 128.
|
||||
|
||||
.. code:: python
|
||||
|
||||
print(info.bext.description)
|
||||
print("----------")
|
||||
print("Originator:", info.bext.originator)
|
||||
print("Originator Ref:", info.bext.originator_ref)
|
||||
print("Originator Date:", info.bext.originator_date)
|
||||
print("Originator Time:", info.bext.originator_time)
|
||||
print("Time Reference:", info.bext.time_reference)
|
||||
print(info.bext.coding_history)
|
||||
|
||||
Result:
|
||||
|
||||
::
|
||||
|
||||
sSPEED=023.976-ND
|
||||
sTAKE=1
|
||||
sUBITS=$12311801
|
||||
sSWVER=2.67
|
||||
sPROJECT=BMH
|
||||
sSCENE=A101
|
||||
sFILENAME=A101_1.WAV
|
||||
sTAPE=18Y12M31
|
||||
sTRK1=MKH516 A
|
||||
sTRK2=Boom
|
||||
sNOTE=
|
||||
|
||||
----------
|
||||
Originator: Sound Dev: 702T S#GR1112089007
|
||||
Originator Ref: USSDVGR1112089007124001008206301
|
||||
Originator Date: 2018-12-31
|
||||
Originator Time: 12:40:00
|
||||
Time Reference: 2190940753
|
||||
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
||||
32
docs/metadata_scopes/info.rst
Normal file
32
docs/metadata_scopes/info.rst
Normal file
@@ -0,0 +1,32 @@
|
||||
INFO Metadata
|
||||
=============
|
||||
|
||||
|
||||
.. module:: wavinfo
|
||||
|
||||
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
INFO Metadata is a standard method for saving tagged text data in a WAV or AVI
|
||||
file. INFO fields are often read by the file explorer and host OS, and used in
|
||||
music library software.
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
bullet_path = '../tests/test_files/BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'
|
||||
|
||||
bullet = WavInfoReader(bullet_path)
|
||||
|
||||
print("INFO Artist:", bullet.info.artist)
|
||||
print("INFO Copyright:", bullet.info.copyright)
|
||||
print("INFO Comment:", bullet.info.comment)
|
||||
|
||||
|
||||
|
||||
|
||||
44
docs/metadata_scopes/ixml.rst
Normal file
44
docs/metadata_scopes/ixml.rst
Normal file
@@ -0,0 +1,44 @@
|
||||
iXML Production Recorder Metadata
|
||||
=================================
|
||||
|
||||
|
||||
.. module:: wavinfo
|
||||
|
||||
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
|
||||
:members:
|
||||
|
||||
|
||||
Notes
|
||||
-----
|
||||
iXML allows an XML document to be embedded in a WAV file.
|
||||
|
||||
The iXML website recommends a schema for recorder information but
|
||||
there is no official DTD and vendors mostly do their own thing, apart from
|
||||
hitting a few key xpaths. iXML is used by most location/production recorders
|
||||
to save slating information, timecode and sync points in a reliable way.
|
||||
|
||||
iXML is also used to link "families" of WAV files together, so WAV files
|
||||
recorded simultaneously or contiguously can be related by a receiving client.
|
||||
|
||||
.. code:: python
|
||||
|
||||
print("iXML Project:", info.ixml.project)
|
||||
print("iXML Scene:", info.ixml.scene)
|
||||
print("iXML Take:", info.ixml.take)
|
||||
print("iXML Tape:", info.ixml.tape)
|
||||
print("iXML File Family Name:", info.ixml.family_name)
|
||||
print("iXML File Family UID:", info.ixml.family_uid)
|
||||
|
||||
Result:
|
||||
|
||||
::
|
||||
|
||||
iXML Project: BMH
|
||||
iXML Scene: A101
|
||||
iXML Take: 1
|
||||
iXML Tape: 18Y12M31
|
||||
iXML File Family Name: None
|
||||
iXML File Family UID: USSDVGR1112089007124001008206300
|
||||
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from wavinfo import WavInfoReader\n",
|
||||
"\n",
|
||||
"path = '../tests/test_files/A101_1.WAV'\n",
|
||||
"path = '../tests/test_files/sounddevices/A101_1.WAV'\n",
|
||||
"\n",
|
||||
"info = WavInfoReader(path)"
|
||||
]
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -42,7 +42,7 @@
|
||||
"(240239, 1441434)"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -62,7 +62,7 @@
|
||||
"(48000, 2, 6, 24)"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -130,7 +130,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -142,9 +142,7 @@
|
||||
"iXML Take: 1\n",
|
||||
"iXML Tape: 18Y12M31\n",
|
||||
"iXML File Family Name: None\n",
|
||||
"iXML File Family UID: USSDVGR1112089007124001008206300\n",
|
||||
"A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
|
||||
"\n"
|
||||
"iXML File Family UID: USSDVGR1112089007124001008206300\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -157,6 +155,13 @@
|
||||
"print(\"iXML File Family UID:\", info.ixml.family_uid)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"WavBextFormat(description='dUBITS=12311804\\r\\ndSCENE=A101\\r\\ndTAKE=4\\r\\ndTAPE=18Y12M31\\r\\ndFRAMERATE=23.976ND\\r\\ndSPEED=023.976-NDF\\r\\ndTRK1=MKH516 A\\r\\ndTRK2=Boom\\r\\n', originator='Sound Dev: 702T S#GR1112089007', originator_ref='aa4CKtcd13Vk', originator_date='2018-12-31', originator_time='12:40:07', time_reference=2191709524, version=0, umid=None, loudness_value=0.0, loudness_range=0.0, max_true_peak=0.0, max_momentary_loudness=0.0, max_shortterm_loudness=0.0, coding_history='A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\\r\\n')\n"
|
||||
"<wavinfo.wave_bext_reader.WavBextReader object at 0x10d5f8ac8>\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -126,6 +126,63 @@
|
||||
"print(len(regn_bin))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{ 'artist': 'Frank Bry',\n",
|
||||
" 'comment': 'BULLET Impact Plastic LCD TV Screen Shatter Debris 2x',\n",
|
||||
" 'copyright': '2018 Creative Sound Design, LLC (The Recordist Christmas '\n",
|
||||
" '2018) www.therecordist.com',\n",
|
||||
" 'created_date': '2018-11-15',\n",
|
||||
" 'engineer': None,\n",
|
||||
" 'genre': 'Bullets',\n",
|
||||
" 'keywords': None,\n",
|
||||
" 'product': 'The Recordist Christmas 2018',\n",
|
||||
" 'software': 'Soundminer',\n",
|
||||
" 'source': None,\n",
|
||||
" 'tape': None,\n",
|
||||
" 'title': None}\n",
|
||||
"{ 'coding_history': '',\n",
|
||||
" 'description': 'BULLET Impact Plastic LCD TV Screen Shatter Debris 2x',\n",
|
||||
" 'loudness_range': None,\n",
|
||||
" 'loudness_value': None,\n",
|
||||
" 'max_momentary_loudness': None,\n",
|
||||
" 'max_shortterm_loudness': None,\n",
|
||||
" 'max_true_peak': None,\n",
|
||||
" 'originator': 'TheRecordist',\n",
|
||||
" 'originator_date': '2018-12-20',\n",
|
||||
" 'originator_ref': 'aaiAKt3fCGTk',\n",
|
||||
" 'originator_time': '12:15:37',\n",
|
||||
" 'time_reference': 57882,\n",
|
||||
" 'version': 0}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"path = '../tests/test_files/BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'\n",
|
||||
"\n",
|
||||
"info = wavinfo.WavInfoReader(path)\n",
|
||||
"\n",
|
||||
"with open(path,'rb') as f:\n",
|
||||
" chunk_tree = wavinfo.wave_parser.parse_chunk(f)\n",
|
||||
" \n",
|
||||
"pp.pprint(info.info.to_dict())\n",
|
||||
"pp.pprint(info.bext.to_dict())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
|
||||
35
setup.py
35
setup.py
@@ -1,21 +1,40 @@
|
||||
from setuptools import setup
|
||||
from wavinfo import __author__, __license__, __version__
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(name='wavinfo',
|
||||
version='0.2',
|
||||
author='Jamie Hardt',
|
||||
version=__version__,
|
||||
author=__author__,
|
||||
author_email='jamiehardt@me.com',
|
||||
description='WAVE sound file metadata parser.',
|
||||
description='Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.',
|
||||
long_description_content_type="text/markdown",
|
||||
long_description=long_description,
|
||||
license=__license__,
|
||||
url='https://github.com/iluvcapra/wavinfo',
|
||||
classifiers=['Development Status :: 4 - Beta',
|
||||
project_urls={
|
||||
'Source':
|
||||
'https://github.com/iluvcapra/wavinfo',
|
||||
'Documentation':
|
||||
'https://wavinfo.readthedocs.io/',
|
||||
'Issues':
|
||||
'https://github.com/iluvcapra/wavinfo/issues',
|
||||
},
|
||||
packages=['wavinfo'],
|
||||
classifiers=['Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Topic :: Multimedia',
|
||||
'Topic :: Multimedia :: Sound/Audio',
|
||||
"Programming Language :: Python :: 3.4",
|
||||
'Topic :: Multimedia :: Sound/Audio',
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6"],
|
||||
packages=['wavinfo'])
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8"],
|
||||
keywords='waveform metadata audio ebu smpte avi library film tv editing editorial',
|
||||
install_requires=['lxml'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'wavinfo = wavinfo.__main__:main'
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from . import test_wave_parsing
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
BIN
tests/test_files/pyramix/Testfile_PYR_BWF.wav
Normal file
BIN
tests/test_files/pyramix/Testfile_PYR_BWF.wav
Normal file
Binary file not shown.
BIN
tests/test_files/rf64/Pro_Tools_2019_RF64.wav.gz
Normal file
BIN
tests/test_files/rf64/Pro_Tools_2019_RF64.wav.gz
Normal file
Binary file not shown.
BIN
tests/test_files/rf64/Testfile_SEQ_RF64_Big.wav.gz
Executable file
BIN
tests/test_files/rf64/Testfile_SEQ_RF64_Big.wav.gz
Executable file
Binary file not shown.
BIN
tests/test_files/sound_grinder_pro/new_camera bumb 1.wav
Normal file
BIN
tests/test_files/sound_grinder_pro/new_camera bumb 1.wav
Normal file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr1.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr1.WAV
Executable file
Binary file not shown.
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr2.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr2.WAV
Executable file
Binary file not shown.
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr3.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr3.WAV
Executable file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr4.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr4.WAV
Executable file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr7.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr7.WAV
Executable file
Binary file not shown.
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr8.WAV
Executable file
BIN
tests/test_files/zoom_F8/DISCRETE/Scene_1_2AG-T001.TAKE/Scene_1_2AG-T001_Tr8.WAV
Executable file
Binary file not shown.
Binary file not shown.
BIN
tests/test_files/zoom_F8/MULTI/Scene_1_2AG-T001.WAV
Executable file
BIN
tests/test_files/zoom_F8/MULTI/Scene_1_2AG-T001.WAV
Executable file
Binary file not shown.
@@ -1,45 +1,20 @@
|
||||
import os.path
|
||||
import json
|
||||
import subprocess
|
||||
from subprocess import PIPE
|
||||
import sys
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from .utils import all_files, ffprobe
|
||||
|
||||
import wavinfo
|
||||
|
||||
FFPROBE='/usr/local/bin/ffprobe'
|
||||
|
||||
|
||||
def ffprobe(path):
|
||||
|
||||
arguments = [ FFPROBE , "-of", "json" , "-show_format", "-show_streams", path ]
|
||||
|
||||
process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE)
|
||||
|
||||
if process.returncode == 0:
|
||||
return json.loads(process.stdout)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
class TestWaveInfo(TestCase):
|
||||
|
||||
|
||||
def all_files(self):
|
||||
for dirpath, dirnames, filenames in os.walk('tests/test_files'):
|
||||
for filename in filenames:
|
||||
name, ext = os.path.splitext(filename)
|
||||
if ext in ['.wav','.WAV']:
|
||||
yield os.path.join(dirpath, filename)
|
||||
|
||||
|
||||
def test_sanity(self):
|
||||
for wav_file in self.all_files():
|
||||
for wav_file in all_files():
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
self.assertTrue(info is not None)
|
||||
|
||||
def test_fmt_against_ffprobe(self):
|
||||
for wav_file in self.all_files():
|
||||
for wav_file in all_files():
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
ffprobe_info = ffprobe(wav_file)
|
||||
|
||||
@@ -55,33 +30,40 @@ class TestWaveInfo(TestCase):
|
||||
self.assertEqual( info.fmt.byte_rate , byte_rate )
|
||||
|
||||
def test_data_against_ffprobe(self):
|
||||
for wav_file in self.all_files():
|
||||
for wav_file in all_files():
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
ffprobe_info = ffprobe(wav_file)
|
||||
|
||||
self.assertEqual( info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts'] ))
|
||||
|
||||
def test_bext_against_ffprobe(self):
|
||||
for wav_file in self.all_files():
|
||||
for wav_file in all_files():
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
ffprobe_info = ffprobe(wav_file)
|
||||
if info.bext:
|
||||
if 'comment' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual( info.bext.description, ffprobe_info['format']['tags']['comment'] )
|
||||
else:
|
||||
self.assertEqual( info.bext.description , '')
|
||||
|
||||
if 'encoded_by' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual( info.bext.originator, ffprobe_info['format']['tags']['encoded_by'] )
|
||||
else:
|
||||
self.assertEqual( info.bext.originator, '')
|
||||
|
||||
if 'originator_reference' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual( info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'] )
|
||||
else:
|
||||
self.assertEqual( info.bext.originator_ref, '')
|
||||
|
||||
self.assertEqual( info.bext.description, ffprobe_info['format']['tags']['comment'] )
|
||||
self.assertEqual( info.bext.originator, ffprobe_info['format']['tags']['encoded_by'] )
|
||||
if 'originator_reference' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual( info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'] )
|
||||
else:
|
||||
self.assertEqual( info.bext.originator_ref, '')
|
||||
# these don't always reflect the bext info
|
||||
# self.assertEqual( info.bext.originator_date, ffprobe_info['format']['tags']['date'] )
|
||||
# self.assertEqual( info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'] )
|
||||
self.assertEqual( info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']) )
|
||||
|
||||
# these don't always reflect the bext info
|
||||
#self.assertEqual( info.bext.originator_date, ffprobe_info['format']['tags']['date'] )
|
||||
#self.assertEqual( info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'] )
|
||||
self.assertEqual( info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']) )
|
||||
|
||||
if 'coding_history' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual( info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'] )
|
||||
else:
|
||||
self.assertEqual( info.bext.coding_history, '' )
|
||||
if 'coding_history' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual( info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'] )
|
||||
else:
|
||||
self.assertEqual( info.bext.coding_history, '' )
|
||||
|
||||
def test_ixml(self):
|
||||
expected = {'A101_4.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '4',
|
||||
@@ -94,7 +76,7 @@ class TestWaveInfo(TestCase):
|
||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
|
||||
}
|
||||
|
||||
for wav_file in self.all_files():
|
||||
for wav_file in all_files():
|
||||
basename = os.path.basename(wav_file)
|
||||
if basename in expected:
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
@@ -105,3 +87,10 @@ class TestWaveInfo(TestCase):
|
||||
self.assertEqual( e['take'], info.ixml.take )
|
||||
self.assertEqual( e['tape'], info.ixml.tape )
|
||||
self.assertEqual( e['family_uid'], info.ixml.family_uid )
|
||||
|
||||
for track in info.ixml.track_list:
|
||||
self.assertIsNotNone(track.channel_index)
|
||||
if basename == 'A101_4.WAV' and track.channel_index == '1':
|
||||
self.assertTrue(track.name == 'MKH516 A')
|
||||
|
||||
|
||||
|
||||
12
tests/test_zoom_f8.py
Normal file
12
tests/test_zoom_f8.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import os.path
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
from subprocess import PIPE
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
import wavinfo
|
||||
|
||||
class TestZoomF8(TestCase):
|
||||
pass
|
||||
39
tests/utils.py
Normal file
39
tests/utils.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os.path
|
||||
import sys
|
||||
import subprocess
|
||||
from subprocess import PIPE
|
||||
import json
|
||||
|
||||
FFPROBE='ffprobe'
|
||||
|
||||
def ffprobe(path):
|
||||
arguments = [ FFPROBE , "-of", "json" , "-show_format", "-show_streams", path ]
|
||||
if int(sys.version[0]) < 3:
|
||||
process = subprocess.Popen(arguments, stdout=PIPE)
|
||||
process.wait()
|
||||
if process.returncode == 0:
|
||||
output = process.communicate()[0]
|
||||
if output:
|
||||
output_str = output.decode('utf-8')
|
||||
return json.loads(output_str)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE)
|
||||
if process.returncode == 0:
|
||||
output_str = process.stdout.decode('utf-8')
|
||||
return json.loads(output_str)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def all_files():
|
||||
for dirpath, _, filenames in os.walk('tests/test_files'):
|
||||
for filename in filenames:
|
||||
_, ext = os.path.splitext(filename)
|
||||
if ext in ['.wav','.WAV']:
|
||||
yield os.path.join(dirpath, filename)
|
||||
|
||||
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
"""
|
||||
methods to probe a WAV file for various kinds of production metadata.
|
||||
|
||||
Go to the documentation for wavinfo.WavInfoReader for more information.
|
||||
"""
|
||||
|
||||
from .wave_reader import WavInfoReader
|
||||
from .riff_parser import WavInfoEOFError
|
||||
|
||||
__version__ = '1.5'
|
||||
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
|
||||
__license__ = "MIT"
|
||||
32
wavinfo/__main__.py
Normal file
32
wavinfo/__main__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from optparse import OptionParser, OptionGroup
|
||||
import datetime
|
||||
from . import WavInfoReader
|
||||
import sys
|
||||
import json
|
||||
|
||||
def main():
|
||||
parser = OptionParser()
|
||||
|
||||
parser.usage = 'wavinfo [FILE.wav]*'
|
||||
|
||||
# parser.add_option('-f', dest='output_format', help='Set the output format',
|
||||
# default='json',
|
||||
# metavar='FORMAT')
|
||||
|
||||
(options, args) = parser.parse_args(sys.argv)
|
||||
for arg in args[1:]:
|
||||
try:
|
||||
this_file = WavInfoReader(path=arg)
|
||||
ret_dict = {'file_argument': arg, 'run_date': datetime.datetime.now().isoformat() , 'scopes': {}}
|
||||
for scope, name, value in this_file.walk():
|
||||
if scope not in ret_dict['scopes'].keys():
|
||||
ret_dict['scopes'][scope] = {}
|
||||
|
||||
ret_dict['scopes'][scope][name] = value
|
||||
|
||||
json.dump(ret_dict, fp=sys.stdout, indent=2)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
40
wavinfo/rf64_parser.py
Normal file
40
wavinfo/rf64_parser.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import struct
|
||||
from collections import namedtuple
|
||||
from . import riff_parser
|
||||
|
||||
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
|
||||
|
||||
|
||||
def parse_rf64(stream):
|
||||
#print("starting parse_rf64")
|
||||
start = stream.tell()
|
||||
assert( stream.read(4) == b'WAVE' )
|
||||
|
||||
ds64_chunk = riff_parser.parse_chunk(stream)
|
||||
|
||||
ds64_field_spec = "<QQQI"
|
||||
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
||||
assert(ds64_chunk.ident == b'ds64')
|
||||
|
||||
ds64_data = ds64_chunk.read_data(stream)
|
||||
assert(len(ds64_data) >= ds64_fields_size )
|
||||
|
||||
#print("Read ds64 chunk: len()",len(ds64_data))
|
||||
riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec , ds64_data[0:ds64_fields_size] )
|
||||
|
||||
bigchunk_table = {}
|
||||
chunksize64format = "<4sL"
|
||||
chunksize64size = struct.calcsize(chunksize64format)
|
||||
#print("Found chunks64s:", length_lookup_table)
|
||||
|
||||
for n in range(length_lookup_table):
|
||||
bigname, bigsize = struct.unpack_from( chunksize64format , ds64data, offset= ds64_fields_size )
|
||||
bigchunk_table[bigname] = bigsize
|
||||
|
||||
bigchunk_table[b'data'] = data_size
|
||||
bigchunk_table[b'RF64'] = riff_size
|
||||
|
||||
stream.seek(start, 0)
|
||||
#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 )
|
||||
|
||||
72
wavinfo/riff_parser.py
Normal file
72
wavinfo/riff_parser.py
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
import struct
|
||||
from collections import namedtuple
|
||||
from .rf64_parser import parse_rf64
|
||||
|
||||
|
||||
class WavInfoEOFError(EOFError):
|
||||
def __init__(self, identifier, chunk_start):
|
||||
self.identifier = identifier
|
||||
self.chunk_start = chunk_start
|
||||
|
||||
|
||||
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
|
||||
def find(self, chunk_path):
|
||||
if len(chunk_path) > 1:
|
||||
for chunk in self.children:
|
||||
if type(chunk) is ListChunkDescriptor and \
|
||||
chunk.signature is chunk_path[0]:
|
||||
return chunk.find(chunk_path[1:])
|
||||
else:
|
||||
for chunk in self.children:
|
||||
if type(chunk) is ChunkDescriptor and \
|
||||
chunk.ident is chunk_path[0]:
|
||||
return chunk
|
||||
|
||||
|
||||
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
|
||||
def read_data(self, from_stream):
|
||||
from_stream.seek(self.start)
|
||||
return from_stream.read(self.length)
|
||||
|
||||
|
||||
def parse_list_chunk(stream, length, rf64_context=None):
|
||||
start = stream.tell()
|
||||
signature = stream.read(4)
|
||||
|
||||
children = []
|
||||
while (stream.tell() - start + 8) < length:
|
||||
child_chunk = parse_chunk(stream, rf64_context=rf64_context)
|
||||
children.append(child_chunk)
|
||||
|
||||
stream.seek(start + length)
|
||||
|
||||
return ListChunkDescriptor(signature=signature, children=children)
|
||||
|
||||
|
||||
def parse_chunk(stream, rf64_context=None):
|
||||
header_start = stream.tell()
|
||||
ident = stream.read(4)
|
||||
size_bytes = stream.read(4)
|
||||
|
||||
if len(ident) != 4 or len(size_bytes) != 4:
|
||||
raise WavInfoEOFError(identifier=ident, chunk_start=header_start)
|
||||
|
||||
data_size = struct.unpack('<I', size_bytes)[0]
|
||||
|
||||
if data_size == 0xFFFFFFFF:
|
||||
if rf64_context is None and ident == b'RF64':
|
||||
rf64_context = parse_rf64(stream=stream)
|
||||
|
||||
data_size = rf64_context.bigchunk_table[ident]
|
||||
|
||||
displacement = data_size
|
||||
if displacement % 2 is not 0:
|
||||
displacement = displacement + 1
|
||||
|
||||
if ident in [b'RIFF', b'LIST', b'RF64']:
|
||||
return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
|
||||
else:
|
||||
data_start = stream.tell()
|
||||
stream.seek(displacement, 1)
|
||||
return ChunkDescriptor(ident=ident, start=data_start, length=data_size, rf64_context=rf64_context)
|
||||
129
wavinfo/umid_parser.py
Normal file
129
wavinfo/umid_parser.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import struct
|
||||
from typing import Union
|
||||
|
||||
class UMIDParser:
|
||||
"""
|
||||
Parse a raw binary SMPTE 330M Universal Materials Identifier
|
||||
|
||||
This implementation is based on SMPTE ST 330:2011
|
||||
"""
|
||||
def __init__(self, raw_umid: bytearray):
|
||||
self.raw_umid = raw_umid
|
||||
|
||||
@classmethod
|
||||
def binary_to_string(cls, binary_value):
|
||||
result_str = ''
|
||||
for n in range(len(binary_value)):
|
||||
result_str = f'{binary_value[n]:x}' + result_str
|
||||
|
||||
return result_str
|
||||
|
||||
@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):
|
||||
return "%024x-%06x-%032x" % (self.binary_to_string(self.universal_label),
|
||||
self.binary_to_string(self.instance_number),
|
||||
self.binary_to_string(self.material_number))
|
||||
|
||||
@property
|
||||
def universal_label_is_valid(self) -> bool:
|
||||
valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
|
||||
return self.universal_label[0:len(valid_preamble)] == valid_preamble
|
||||
|
||||
@property
|
||||
def material_type(self) -> str:
|
||||
material_byte = self.raw_umid[10]
|
||||
if material_byte == 0x1:
|
||||
return 'picture'
|
||||
elif material_byte == 0x2:
|
||||
return 'audio'
|
||||
elif material_byte == 0x3:
|
||||
return 'data'
|
||||
elif material_byte == 0x4:
|
||||
return 'other'
|
||||
elif material_byte == 0x5:
|
||||
return 'picture_single_component'
|
||||
elif material_byte == 0x6:
|
||||
return 'picture_multiple_component'
|
||||
elif material_byte == 0x7:
|
||||
return 'audio_single_component'
|
||||
elif material_byte == 0x9:
|
||||
return 'audio_multiple_component'
|
||||
elif material_byte == 0xb:
|
||||
return 'auxiliary_single_component'
|
||||
elif material_byte == 0xc:
|
||||
return 'auxiliary_multiple_component'
|
||||
elif material_byte == 0xd:
|
||||
return 'mixed_components'
|
||||
elif material_byte == 0xf:
|
||||
return 'not_identified'
|
||||
else:
|
||||
return 'not_recognized'
|
||||
|
||||
@property
|
||||
def material_number_creation_method(self) -> str:
|
||||
method_byte = self.raw_umid[11]
|
||||
method_byte = (method_byte << 4) & 0xf
|
||||
if method_byte == 0x0:
|
||||
return 'undefined'
|
||||
elif method_byte == 0x1:
|
||||
return 'smpte'
|
||||
elif method_byte == 0x2:
|
||||
return 'uuid'
|
||||
elif method_byte == 0x3:
|
||||
return 'masked'
|
||||
elif method_byte == 0x4:
|
||||
return 'ieee1394'
|
||||
elif 0x5 <= method_byte <= 0x7:
|
||||
return 'reserved_undefined'
|
||||
else:
|
||||
return 'unrecognized'
|
||||
|
||||
@property
|
||||
def instance_number_creation_method(self) -> str:
|
||||
method_byte = self.raw_umid[11]
|
||||
method_byte = method_byte & 0xf
|
||||
if method_byte == 0x0:
|
||||
return 'undefined'
|
||||
elif method_byte == 0x01:
|
||||
return 'local_registration'
|
||||
elif method_byte == 0x02:
|
||||
return '24_bit_prs'
|
||||
elif method_byte == 0x03:
|
||||
return 'copy_number_and_16_bit_prs'
|
||||
elif 0x04 <= method_byte <= 0x0e:
|
||||
return 'reserved_undefined'
|
||||
elif method_byte == 0x0f:
|
||||
return 'live_stream'
|
||||
else:
|
||||
return 'unrecognized'
|
||||
|
||||
@property
|
||||
def indicated_length(self) -> str:
|
||||
if self.raw_umid[12] == 0x13:
|
||||
return 'basic'
|
||||
elif self.raw_umid[12] == 0x33:
|
||||
return 'extended'
|
||||
|
||||
@property
|
||||
def instance_number(self) -> bytearray:
|
||||
return self.raw_umid[13:3]
|
||||
|
||||
@property
|
||||
def material_number(self) -> bytearray:
|
||||
return self.raw_umid[16:16]
|
||||
|
||||
@property
|
||||
def source_pack(self) -> Union[bytearray, None]:
|
||||
if self.indicated_length == 'extended':
|
||||
return self.raw_umid[32:32]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
85
wavinfo/wave_bext_reader.py
Normal file
85
wavinfo/wave_bext_reader.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import struct
|
||||
|
||||
class WavBextReader:
|
||||
def __init__(self,bext_data,encoding):
|
||||
"""
|
||||
Read Broadcast-WAV extended metadata.
|
||||
:param best_data: The bytes-like data.
|
||||
"param encoding: The encoding to use when decoding the text fields of the
|
||||
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
|
||||
"""
|
||||
packstring = "<256s"+ "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
||||
|
||||
rest_starts = struct.calcsize(packstring)
|
||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||
|
||||
def sanatize_bytes(bytes):
|
||||
first_null = next( (index for index, byte in enumerate(bytes) if byte == 0 ), None )
|
||||
if first_null is not None:
|
||||
trimmed = bytes[:first_null]
|
||||
else:
|
||||
trimmed = bytes
|
||||
|
||||
decoded = trimmed.decode(encoding)
|
||||
return decoded
|
||||
|
||||
#: Description. A free-text field up to 256 characters long.
|
||||
self.description = sanatize_bytes(unpacked[0])
|
||||
#: Originator. Usually the name of the encoding application, sometimes
|
||||
#: a artist name.
|
||||
self.originator = sanatize_bytes(unpacked[1])
|
||||
#: A unique identifer for the file, a serial number.
|
||||
self.originator_ref = sanatize_bytes(unpacked[2])
|
||||
#: Date of the recording, in the format YYY-MM-DD
|
||||
self.originator_date = sanatize_bytes(unpacked[3])
|
||||
#: Time of the recording, in the format HH:MM:SS.
|
||||
self.originator_time = sanatize_bytes(unpacked[4])
|
||||
#: The sample offset of the start of the file relative to an
|
||||
#: epoch, usually midnight the day of the recording.
|
||||
self.time_reference = unpacked[5]
|
||||
#: A variable-length text field containing a list of processes and
|
||||
#: and conversions performed on the file.
|
||||
self.coding_history = sanatize_bytes(bext_data[rest_starts:])
|
||||
#: BEXT version.
|
||||
self.version = unpacked[6]
|
||||
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
|
||||
#: may only be 32 bytes long.
|
||||
self.umid = None
|
||||
#: EBU R128 Integrated loudness, in LUFS.
|
||||
self.loudness_value = None
|
||||
#: EBU R128 Loudness rante, in LUFS.
|
||||
self.loudness_range = None
|
||||
#: True peak level, in dBFS TP
|
||||
self.max_true_peak = None
|
||||
#: EBU R128 Maximum momentary loudness, in LUFS
|
||||
self.max_momentary_loudness = None
|
||||
#: EBU R128 Maximum short-term loudness, in LUFS.
|
||||
self.max_shortterm_loudness = None
|
||||
|
||||
if self.version > 0:
|
||||
self.umid = unpacked[7]
|
||||
|
||||
if self.version > 1:
|
||||
self.loudness_value = unpacked[8] / 100.0
|
||||
self.loudness_range = unpacked[9] / 100.0
|
||||
self.max_true_peak = unpacked[10] / 100.0
|
||||
self.max_momentary_loudness = unpacked[11] / 100.0
|
||||
self.max_shortterm_loudness = unpacked[12] / 100.0
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
return {'description': self.description,
|
||||
'originator': self.originator,
|
||||
'originator_ref': self.originator_ref,
|
||||
'originator_date': self.originator_date,
|
||||
'originator_time': self.originator_time,
|
||||
'time_reference': self.time_reference,
|
||||
'version': self.version,
|
||||
'coding_history': self.coding_history,
|
||||
'loudness_value': self.loudness_value,
|
||||
'loudness_range': self.loudness_range,
|
||||
'max_true_peak': self.max_true_peak,
|
||||
'max_momentary_loudness': self.max_momentary_loudness,
|
||||
'max_shortterm_loudness': self.max_shortterm_loudness
|
||||
}
|
||||
|
||||
95
wavinfo/wave_info_reader.py
Normal file
95
wavinfo/wave_info_reader.py
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
from .riff_parser import parse_chunk, ListChunkDescriptor
|
||||
|
||||
class WavInfoChunkReader:
|
||||
|
||||
def __init__(self, f, encoding):
|
||||
self.encoding = encoding
|
||||
|
||||
f.seek(0)
|
||||
parsed_chunks = parse_chunk(f)
|
||||
|
||||
list_chunks = [chunk for chunk in parsed_chunks.children \
|
||||
if type(chunk) is ListChunkDescriptor]
|
||||
|
||||
self.info_chunk = next((chunk for chunk in list_chunks \
|
||||
if chunk.signature == b'INFO'), None)
|
||||
|
||||
#: 'ICOP' Copyright
|
||||
self.copyright = self._get_field(f,b'ICOP')
|
||||
#: 'IPRD' Product
|
||||
self.product = self._get_field(f,b'IPRD')
|
||||
#: 'IGNR' Genre
|
||||
self.genre = self._get_field(f,b'IGNR')
|
||||
#: 'ISBJ' Supject
|
||||
self.subject = self._get_field(f,b'ISBJ')
|
||||
#: 'IART' Artist, composer, author
|
||||
self.artist = self._get_field(f,b'IART')
|
||||
#: 'ICMT' Comment
|
||||
self.comment = self._get_field(f,b'ICMT')
|
||||
#: 'ISFT' Software, encoding application
|
||||
self.software = self._get_field(f,b'ISFT')
|
||||
#: 'ICRD' Created date
|
||||
self.created_date = self._get_field(f,b'ICRD')
|
||||
#: 'IENG' Engineer
|
||||
self.engineer = self._get_field(f,b'IENG')
|
||||
#: 'ITCH' Technician
|
||||
self.technician = self._get_field(f,b'ITCH')
|
||||
#: 'IKEY' Keywords, keyword list
|
||||
self.keywords = self._get_field(f,b'IKEY')
|
||||
#: 'INAM' Name, title
|
||||
self.title = self._get_field(f,b'INAM')
|
||||
#: 'ISRC' Source
|
||||
self.source = self._get_field(f,b'ISRC')
|
||||
#: 'TAPE' Tape
|
||||
self.tape = self._get_field(f,b'TAPE')
|
||||
#: 'IARL' Archival Location
|
||||
self.archival_location = self._get_field(f,b'IARL')
|
||||
#: 'ISFT' Software
|
||||
self.software = self._get_field(f,b'ISFT')
|
||||
#: 'ICSM' Commissioned
|
||||
self.commissioned = self._get_field(f,b'ICMS')
|
||||
|
||||
|
||||
|
||||
def _get_field(self, f, field_ident):
|
||||
|
||||
search = next( ( (chunk.start, chunk.length) for chunk in self.info_chunk.children \
|
||||
if chunk.ident == field_ident ), None)
|
||||
|
||||
if search is not None:
|
||||
f.seek(search[0])
|
||||
data = f.read(search[1])
|
||||
return data.decode(self.encoding).rstrip('\0')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
A dictionary with all of the key/values read from the INFO scope.
|
||||
"""
|
||||
return {'copyright': self.copyright,
|
||||
'product': self.product,
|
||||
'genre': self.genre,
|
||||
'artist': self.artist,
|
||||
'comment': self.comment,
|
||||
'software': self.software,
|
||||
'created_date': self.created_date,
|
||||
'engineer': self.engineer,
|
||||
'keywords': self.keywords,
|
||||
'title': self.title,
|
||||
'source': self.source,
|
||||
'tape': self.tape,
|
||||
'commissioned': self.commissioned,
|
||||
'software': self.software,
|
||||
'archival_location':self.archival_location,
|
||||
'subject': self.subject,
|
||||
'technician':self.technician
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +1,97 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
#import xml.etree.ElementTree as ET
|
||||
from lxml import etree as ET
|
||||
import io
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
|
||||
|
||||
class WavIXMLFormat:
|
||||
"""
|
||||
iXML recorder metadata, as defined by iXML 2.0
|
||||
iXML recorder metadata.
|
||||
"""
|
||||
def __init__(self, xml):
|
||||
"""
|
||||
Parse iXML.
|
||||
:param xml: A bytes-like object containing the iXML payload.
|
||||
"""
|
||||
self.source = xml
|
||||
self.parsed = ET.fromstring(xml)
|
||||
xmlBytes = io.BytesIO(xml)
|
||||
try:
|
||||
parser = ET.XMLParser(recover=True)
|
||||
self.parsed = ET.parse(xmlBytes, parser=parser)
|
||||
except ET.ParseError as err:
|
||||
print("Error parsing iXML: " + str(err))
|
||||
decoded = xml.decode(encoding='utf_8_sig')
|
||||
print(decoded)
|
||||
self.parsed = ET.parse(io.StringIO(decoded))
|
||||
|
||||
def _get_text_value(self, xpath):
|
||||
e = self.parsed.find("./" + xpath)
|
||||
if e is not None:
|
||||
return e.text
|
||||
|
||||
|
||||
@property
|
||||
def raw_xml(self):
|
||||
"""
|
||||
The root entity of the iXML document.
|
||||
"""
|
||||
return self.parsed
|
||||
|
||||
@property
|
||||
def track_list(self):
|
||||
"""
|
||||
A description of each track.
|
||||
:return: An Iterator
|
||||
"""
|
||||
for track in self.parsed.find("./TRACK_LIST").iter():
|
||||
if track.tag == 'TRACK':
|
||||
yield IXMLTrack(channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
|
||||
interleave_index=track.xpath('string(INTERLEAVE_INDEX/text())'),
|
||||
name=track.xpath('string(NAME/text())'),
|
||||
function=track.xpath('string(FUNCTION/text())'))
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
"""
|
||||
The project/film name entered for the recording.
|
||||
"""
|
||||
return self._get_text_value("PROJECT")
|
||||
|
||||
|
||||
@property
|
||||
def scene(self):
|
||||
"""
|
||||
Scene/slate.
|
||||
"""
|
||||
return self._get_text_value("SCENE")
|
||||
|
||||
@property
|
||||
def take(self):
|
||||
"""
|
||||
Take number.
|
||||
"""
|
||||
return self._get_text_value("TAKE")
|
||||
|
||||
@property
|
||||
def tape(self):
|
||||
"""
|
||||
Tape name.
|
||||
"""
|
||||
return self._get_text_value("TAPE")
|
||||
|
||||
@property
|
||||
def family_uid(self):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return self._get_text_value("FILE_SET/FAMILY_UID")
|
||||
|
||||
@property
|
||||
def family_name(self):
|
||||
"""
|
||||
The name of this file's file family.
|
||||
"""
|
||||
return self._get_text_value("FILE_SET/FAMILY_NAME")
|
||||
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
import struct
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
ListChunkDescriptor = namedtuple('ListChunkDescriptor' , 'signature children')
|
||||
|
||||
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length') ):
|
||||
def read_data(self, from_stream):
|
||||
from_stream.seek(self.start)
|
||||
return from_stream.read(self.length)
|
||||
|
||||
|
||||
def parse_list_chunk(stream, length):
|
||||
children = []
|
||||
|
||||
start = stream.tell()
|
||||
|
||||
signature = stream.read(4)
|
||||
|
||||
while (stream.tell() - start) < length:
|
||||
children.append(parse_chunk(stream))
|
||||
|
||||
return ListChunkDescriptor(signature=signature, children=children)
|
||||
|
||||
|
||||
def parse_chunk(stream):
|
||||
ident = stream.read(4)
|
||||
if len(ident) != 4:
|
||||
return
|
||||
|
||||
sizeb = stream.read(4)
|
||||
size = struct.unpack('<I',sizeb)[0]
|
||||
|
||||
displacement = size
|
||||
if displacement % 2 is not 0:
|
||||
displacement = displacement + 1
|
||||
|
||||
if ident in [b'RIFF',b'LIST']:
|
||||
return parse_list_chunk(stream=stream, length=size)
|
||||
else:
|
||||
start = stream.tell()
|
||||
stream.seek(displacement,1)
|
||||
return ChunkDescriptor(ident=ident, start=start, length=size)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,38 +1,68 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
import struct
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
|
||||
from .wave_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
|
||||
from .wave_ixml_reader import WavIXMLFormat
|
||||
if sys.version[0] == '3':
|
||||
import pathlib
|
||||
else:
|
||||
import urlparse, urllib
|
||||
|
||||
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
|
||||
from .wave_ixml_reader import WavIXMLFormat
|
||||
from .wave_bext_reader import WavBextReader
|
||||
from .wave_info_reader import WavInfoChunkReader
|
||||
|
||||
#: Calculated statistics about the audio data.
|
||||
WavDataDescriptor = namedtuple('WavDataDescriptor','byte_count frame_count')
|
||||
|
||||
WavInfoFormat = namedtuple("WavInfoFormat",'audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
|
||||
|
||||
WavBextFormat = namedtuple("WavBextFormat",'description originator originator_ref ' +
|
||||
'originator_date originator_time time_reference version umid ' +
|
||||
'loudness_value loudness_range max_true_peak max_momentary_loudness max_shortterm_loudness ' +
|
||||
'coding_history')
|
||||
|
||||
#: The format of the audio samples.
|
||||
WavAudioFormat = namedtuple('WavAudioFormat','audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
|
||||
|
||||
class WavInfoReader():
|
||||
"""
|
||||
format : WAV format
|
||||
bext : The Broadcast-WAV extension as definied by EBU Tech 3285 v2 (2011)
|
||||
|
||||
Parse a WAV audio file for metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
|
||||
"""
|
||||
Create a new reader object.
|
||||
|
||||
:param path: A filesystem path to the wav file you wish to probe.
|
||||
|
||||
:param info_encoding: The text encoding of the INFO metadata fields.
|
||||
latin_1/Win CP1252 has always been a pretty good guess for this.
|
||||
|
||||
:param bext_encoding: The text encoding to use when decoding the string
|
||||
fields of the Broadcast-WAV extension. Per EBU 3285 this is ASCII
|
||||
but this parameter is available to you if you encounter a werido.
|
||||
"""
|
||||
absolute_path = os.path.abspath(path)
|
||||
|
||||
if sys.version[0] == '3':
|
||||
#: `file://` url for the file.
|
||||
self.url = pathlib.Path(absolute_path).as_uri()
|
||||
else:
|
||||
self.url = urlparse.urljoin('file:', urllib.pathname2url(absolute_path))
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
chunks = parse_chunk(f)
|
||||
|
||||
self.main_list = chunks.children
|
||||
f.seek(0)
|
||||
|
||||
#: :class:`wavinfo.wave_reader.WavAudioFormat`
|
||||
self.fmt = self._get_format(f)
|
||||
self.bext = self._get_bext(f)
|
||||
|
||||
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
|
||||
self.bext = self._get_bext(f, encoding=bext_encoding)
|
||||
|
||||
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
|
||||
self.ixml = self._get_ixml(f)
|
||||
|
||||
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
|
||||
self.info = self._get_info(f, encoding=info_encoding)
|
||||
self.data = self._describe_data(f)
|
||||
|
||||
def _find_chunk_data(self, ident, from_stream, default_none=False):
|
||||
@@ -57,7 +87,6 @@ class WavInfoReader():
|
||||
frame_count= int(data_chunk.length / self.fmt.block_align))
|
||||
|
||||
|
||||
|
||||
def _get_format(self,f):
|
||||
fmt_data = self._find_chunk_data(b'fmt ',f)
|
||||
|
||||
@@ -78,100 +107,53 @@ class WavInfoReader():
|
||||
#0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
|
||||
#0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
|
||||
#0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
|
||||
if unpacked[0] == 0x0001:
|
||||
return WavInfoFormat(audio_format = unpacked[0],
|
||||
channel_count = unpacked[1],
|
||||
sample_rate = unpacked[2],
|
||||
byte_rate = unpacked[3],
|
||||
block_align = unpacked[4],
|
||||
|
||||
#https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html
|
||||
return WavAudioFormat(audio_format = unpacked[0],
|
||||
channel_count = unpacked[1],
|
||||
sample_rate = unpacked[2],
|
||||
byte_rate = unpacked[3],
|
||||
block_align = unpacked[4],
|
||||
bits_per_sample = unpacked[5]
|
||||
)
|
||||
|
||||
def _get_bext(self,f,encoding='ascii'):
|
||||
def _get_info(self, f, encoding):
|
||||
finder = (chunk.signature for chunk in self.main_list \
|
||||
if type(chunk) is ListChunkDescriptor)
|
||||
|
||||
if b'INFO' in finder:
|
||||
return WavInfoChunkReader(f, encoding)
|
||||
|
||||
def _get_bext(self, f, encoding):
|
||||
bext_data = self._find_chunk_data(b'bext',f,default_none=True)
|
||||
|
||||
# description[256]
|
||||
# originator[32]
|
||||
# originatorref[32]
|
||||
# originatordate[10] "YYYY:MM:DD"
|
||||
# originatortime[8] "HH:MM:SS"
|
||||
# lowtimeref U32
|
||||
# hightimeref U32
|
||||
# version U16
|
||||
# umid[64]
|
||||
#
|
||||
# EBU 3285 fields
|
||||
# loudnessvalue S16 (in LUFS*100)
|
||||
# loudnessrange S16 (in LUFS*100)
|
||||
# maxtruepeak S16 (in dbTB*100)
|
||||
# maxmomentaryloudness S16 (LUFS*100)
|
||||
# maxshorttermloudness S16 (LUFS*100)
|
||||
# reserved[180]
|
||||
# codinghistory []
|
||||
if bext_data is None:
|
||||
if bext_data:
|
||||
return WavBextReader(bext_data, encoding)
|
||||
else:
|
||||
return None
|
||||
|
||||
packstring = "<256s"+ "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
||||
|
||||
rest_starts = struct.calcsize(packstring)
|
||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||
|
||||
def sanatize_bytes(bytes):
|
||||
first_null = next( (index for index, byte in enumerate(bytes) if byte == 0 ), None )
|
||||
if first_null is not None:
|
||||
trimmed = bytes[:first_null]
|
||||
else:
|
||||
trimmed = bytes
|
||||
|
||||
decoded = trimmed.decode(encoding)
|
||||
return decoded
|
||||
|
||||
bext_version = unpacked[6]
|
||||
if bext_version > 0:
|
||||
umid = unpacked[6]
|
||||
else:
|
||||
umid = None
|
||||
|
||||
if bext_version > 1:
|
||||
loudness_value = unpacked[8] / 100.0,
|
||||
loudness_range = unpacked[9] / 100.0
|
||||
max_true_peak = unpacked[10] / 100.0
|
||||
max_momentary_loudness = unpacked[11] / 100.0
|
||||
max_shortterm_loudness = unpacked[12] / 100.0
|
||||
else:
|
||||
loudness_value = None
|
||||
loudness_range = None
|
||||
max_true_peak = None
|
||||
max_momentary_loudness = None
|
||||
max_shortterm_loudness = None
|
||||
|
||||
return WavBextFormat(description=sanatize_bytes(unpacked[0]),
|
||||
originator = sanatize_bytes(unpacked[1]),
|
||||
originator_ref = sanatize_bytes(unpacked[2]),
|
||||
originator_date = sanatize_bytes(unpacked[3]),
|
||||
originator_time = sanatize_bytes(unpacked[4]),
|
||||
time_reference = unpacked[5],
|
||||
version = unpacked[6],
|
||||
umid = umid,
|
||||
loudness_value = loudness_value,
|
||||
loudness_range = loudness_range,
|
||||
max_true_peak = max_true_peak,
|
||||
max_momentary_loudness = max_momentary_loudness,
|
||||
max_shortterm_loudness = max_shortterm_loudness,
|
||||
coding_history = sanatize_bytes(bext_data[rest_starts:])
|
||||
)
|
||||
|
||||
def _get_ixml(self,f):
|
||||
|
||||
ixml_data = self._find_chunk_data(b'iXML',f,default_none=True)
|
||||
if ixml_data is None:
|
||||
return None
|
||||
|
||||
ixml_string = ixml_data.decode('utf-8')
|
||||
ixml_string = ixml_data.rstrip(b'\0')
|
||||
return WavIXMLFormat(ixml_string)
|
||||
|
||||
def walk(self):
|
||||
"""
|
||||
Walk all of the available metadata fields.
|
||||
|
||||
:yields: a string, the :scope: of the metadatum, the string :name: of the
|
||||
metadata field, and the value.
|
||||
"""
|
||||
|
||||
scopes = ('fmt', 'data') #'bext', 'ixml', 'info')
|
||||
|
||||
for scope in scopes:
|
||||
attr = self.__getattribute__(scope)
|
||||
for field in attr._fields:
|
||||
yield scope, field, attr.__getattribute__(field)
|
||||
|
||||
bext_dict = self.bext.to_dict()
|
||||
for key in bext_dict.keys():
|
||||
yield 'bext', key, bext_dict[key]
|
||||
|
||||
Reference in New Issue
Block a user