mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2026-10-02 01:37:22 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50dbc35595 | |||
| 6f5cb795a3 | |||
| 7d5bb29a42 | |||
| f9de312610 | |||
| e6320e84c6 | |||
| 13d9cd4914 | |||
| 742c0b91fb | |||
| 8e56a1aeb2 | |||
| 6bc3636814 | |||
| e12ae4519e |
@@ -19,9 +19,9 @@ jobs:
|
|||||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.5.0
|
- uses: actions/checkout@v6.0.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v4.3.0
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
python -m pip install --group dev
|
python -m pip install --group dev
|
||||||
python -m pip install .
|
python -m pip install .
|
||||||
- name: Setup FFmpeg
|
- name: Setup FFmpeg
|
||||||
uses: FedericoCarboni/setup-ffmpeg@v2
|
uses: federicocarboni/setup-ffmpeg@v3.1
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
pytest
|
pytest
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ version: 2
|
|||||||
|
|
||||||
# Set the version of Python and other tools you might need
|
# Set the version of Python and other tools you might need
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-lts-latest
|
||||||
tools:
|
tools:
|
||||||
python: "3.13"
|
python: "3.13"
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
+32
-1
@@ -13,4 +13,35 @@ If you discover a bug or would like better support for a feature, please do the
|
|||||||
review it as soon as I can. There's a `.devcontainer` available so you can creates commits
|
review it as soon as I can. There's a `.devcontainer` available so you can creates commits
|
||||||
on this project in a GitHub codespace.
|
on this project in a GitHub codespace.
|
||||||
|
|
||||||
Contributions made by machine learning systems are politely declined.
|
|
||||||
|
## Regarding use of Agents
|
||||||
|
|
||||||
|
`wavinfo` is an open-source project that is offered free for no commerical gain, and
|
||||||
|
is developed and maintained for educational and creative reasons.
|
||||||
|
|
||||||
|
If you use an agent or LLM to produce code for it you are missing out on the benefits
|
||||||
|
of contributing to an open-source project, particularly community, collaboration with
|
||||||
|
other developers and designers, and being able to learn and experiment without the
|
||||||
|
burden of deadlines or worrying about business cases or profits.
|
||||||
|
|
||||||
|
This project is supposed to be fun, do not let machines have fun for you.
|
||||||
|
|
||||||
|
We can't prevent you from using LLMs to contribute to this project but we ask you
|
||||||
|
abide by the following eitiquette when doing so:
|
||||||
|
|
||||||
|
* All communication with the maintainers must be written by a human in their own
|
||||||
|
voice. Never use an LLM to craft thread comments, discussion posts, issues, emails
|
||||||
|
or other correspondence with other developers or the maintainers.
|
||||||
|
* PRs must be submitted by a person. Do not allow an agent to submit its own PRs to
|
||||||
|
this project.
|
||||||
|
* Especially if you are a new contributor to this project, please submit only one PR
|
||||||
|
at a time and please restrict the subject matter of the PR to a specific unit,
|
||||||
|
module or tool. All submissions have to be reviewed and understood by the
|
||||||
|
maintainers before they can be merged.
|
||||||
|
|
||||||
|
Obviously we can't verify if you follow all of these rules but certain telltale
|
||||||
|
traits of LLM-predicted text or code will raise a flag: lack of brevity in
|
||||||
|
descriptions or code comments, large amounts of text describing your process or
|
||||||
|
steps that add little to understanding the changes you've made, use of an
|
||||||
|
obsequious tone or being excessively accomodating, immediately doing requests
|
||||||
|
without further discussion or clarifications.
|
||||||
|
|||||||
+28
-29
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Configuration file for the Sphinx documentation builder.
|
# Configuration file for the Sphinx documentation builder.
|
||||||
#
|
#
|
||||||
@@ -15,15 +14,16 @@
|
|||||||
# import importlib
|
# import importlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.abspath('../../src'))
|
|
||||||
|
sys.path.insert(0, os.path.abspath("../../src"))
|
||||||
sys.path.insert(0, os.path.abspath("../../../src"))
|
sys.path.insert(0, os.path.abspath("../../../src"))
|
||||||
print(sys.path)
|
print(sys.path)
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = u'wavinfo'
|
project = "wavinfo"
|
||||||
copyright = u'2018-2025, Jamie Hardt'
|
copyright = "2018-2025, Jamie Hardt"
|
||||||
author = u'Jamie Hardt'
|
author = "Jamie Hardt"
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
version = "4.0"
|
version = "4.0"
|
||||||
@@ -42,34 +42,34 @@ release = "4.0.0"
|
|||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
"sphinx.ext.autodoc",
|
||||||
'sphinx.ext.todo',
|
"sphinx.ext.todo",
|
||||||
'sphinx.ext.coverage',
|
"sphinx.ext.coverage",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
# The suffix(es) of source filenames.
|
# The suffix(es) of source filenames.
|
||||||
# You can specify multiple suffix as a list of string:
|
# You can specify multiple suffix as a list of string:
|
||||||
#
|
#
|
||||||
# source_suffix = ['.rst', '.md']
|
# source_suffix = ['.rst', '.md']
|
||||||
source_suffix = '.rst'
|
source_suffix = ".rst"
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'index'
|
master_doc = "index"
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# Usually you set "language" from the command line for these cases.
|
||||||
language = 'en'
|
language = "en"
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = None
|
pygments_style = None
|
||||||
@@ -80,7 +80,7 @@ pygments_style = None
|
|||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
html_theme = 'sphinx_rtd_theme'
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a 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
|
# further. For a list of options available for each theme, see the
|
||||||
@@ -91,7 +91,7 @@ html_theme = 'sphinx_rtd_theme'
|
|||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# 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,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
html_static_path = ["_static"]
|
||||||
|
|
||||||
# Custom sidebar templates, must be a dictionary that maps document names
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
# to template names.
|
# to template names.
|
||||||
@@ -107,7 +107,7 @@ html_static_path = ['_static']
|
|||||||
# -- Options for HTMLHelp output ---------------------------------------------
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = 'wavinfodoc'
|
htmlhelp_basename = "wavinfodoc"
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ------------------------------------------------
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
@@ -116,15 +116,12 @@ latex_elements = {
|
|||||||
# The paper size ('letterpaper' or 'a4paper').
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
#
|
#
|
||||||
# 'papersize': 'letterpaper',
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
#
|
#
|
||||||
# 'pointsize': '10pt',
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
# Additional stuff for the LaTeX preamble.
|
||||||
#
|
#
|
||||||
# 'preamble': '',
|
# 'preamble': '',
|
||||||
|
|
||||||
# Latex figure (float) alignment
|
# Latex figure (float) alignment
|
||||||
#
|
#
|
||||||
# 'figure_align': 'htbp',
|
# 'figure_align': 'htbp',
|
||||||
@@ -134,8 +131,7 @@ latex_elements = {
|
|||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'wavinfo.tex', u'wavinfo Documentation',
|
(master_doc, "wavinfo.tex", "wavinfo Documentation", "Jamie Hardt", "manual"),
|
||||||
u'Jamie Hardt', 'manual'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -143,10 +139,7 @@ latex_documents = [
|
|||||||
|
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [(master_doc, "wavinfo", "wavinfo Documentation", [author], 1)]
|
||||||
(master_doc, 'wavinfo', u'wavinfo Documentation',
|
|
||||||
[author], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output ----------------------------------------------
|
# -- Options for Texinfo output ----------------------------------------------
|
||||||
@@ -155,9 +148,15 @@ man_pages = [
|
|||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'wavinfo', u'wavinfo Documentation',
|
(
|
||||||
author, 'wavinfo', 'One line description of project.',
|
master_doc,
|
||||||
'Miscellaneous'),
|
"wavinfo",
|
||||||
|
"wavinfo Documentation",
|
||||||
|
author,
|
||||||
|
"wavinfo",
|
||||||
|
"One line description of project.",
|
||||||
|
"Miscellaneous",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ epub_title = project
|
|||||||
# epub_uid = ''
|
# epub_uid = ''
|
||||||
|
|
||||||
# A list of files that should not be packed into the epub file.
|
# A list of files that should not be packed into the epub file.
|
||||||
epub_exclude_files = ['search.html']
|
epub_exclude_files = ["search.html"]
|
||||||
|
|
||||||
|
|
||||||
# -- Extension configuration -------------------------------------------------
|
# -- Extension configuration -------------------------------------------------
|
||||||
|
|||||||
+10
-3
@@ -26,7 +26,7 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"from wavinfo import WavInfoReader\n",
|
"from wavinfo import WavInfoReader\n",
|
||||||
"\n",
|
"\n",
|
||||||
"path = '../tests/test_files/sounddevices/A101_1.WAV'\n",
|
"path = \"../tests/test_files/sounddevices/A101_1.WAV\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"info = WavInfoReader(path)"
|
"info = WavInfoReader(path)"
|
||||||
]
|
]
|
||||||
@@ -113,7 +113,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)"
|
"(\n",
|
||||||
|
" info.fmt.sample_rate,\n",
|
||||||
|
" info.fmt.channel_count,\n",
|
||||||
|
" info.fmt.block_align,\n",
|
||||||
|
" info.fmt.bits_per_sample,\n",
|
||||||
|
")"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -271,7 +276,9 @@
|
|||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"path = \"../tests/test_files/cue_chunks/izotoperx_cues_test.wav\"\n",
|
"path = \"../tests/test_files/cue_chunks/izotoperx_cues_test.wav\"\n",
|
||||||
"info = WavInfoReader(path, info_encoding=\"utf-8\") # iZotope RX seems to encode marker text as UTF-8\n",
|
"info = WavInfoReader(\n",
|
||||||
|
" path, info_encoding=\"utf-8\"\n",
|
||||||
|
") # iZotope RX seems to encode marker text as UTF-8\n",
|
||||||
"\n",
|
"\n",
|
||||||
"for cue in info.cues.each_cue():\n",
|
"for cue in info.cues.each_cue():\n",
|
||||||
" print(f\"Cue ID: {cue[0]}\")\n",
|
" print(f\"Cue ID: {cue[0]}\")\n",
|
||||||
|
|||||||
+26
-11
@@ -45,7 +45,7 @@ dependencies = [
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.3.5",
|
"pytest>=8.3.5",
|
||||||
"ruff>=0.14.0",
|
"ruff>=0.16.8",
|
||||||
]
|
]
|
||||||
doc = [
|
doc = [
|
||||||
"sphinx>=7.1.2",
|
"sphinx>=7.1.2",
|
||||||
@@ -58,15 +58,30 @@ wavinfo = "wavinfo:__main__.main"
|
|||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "basic"
|
||||||
|
|
||||||
[tool.pylint]
|
[tool.ruff]
|
||||||
max-line-length = 88
|
line-length = 88
|
||||||
disable = [
|
indent-width = 4
|
||||||
"C0103", # (invalid-name)
|
|
||||||
"C0114", # (missing-module-docstring)
|
[tool.ruff.lint]
|
||||||
"C0115", # (missing-class-docstring)
|
fixable = ['ALL']
|
||||||
"C0116", # (missing-function-docstring)
|
ignore = [
|
||||||
"R0903", # (too-few-public-methods)
|
'FA100', #Add `from __future__ import annotations` to simplify `typing.Optional`
|
||||||
"R0913", # (too-many-arguments)
|
'UP022', # Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
||||||
"W0105", # (pointless-string-statement)
|
'SIM115', # Use a context manager for opening files
|
||||||
|
'UP031', #Use format specifiers instead of percent format
|
||||||
|
'C408', # Unnecessary `dict()` call (rewrite as a literal)
|
||||||
|
'C417' # Unnecessary `map()` usage (rewrite using a generator expression)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# [tool.pylint]
|
||||||
|
# max-line-length = 88
|
||||||
|
# disable = [
|
||||||
|
# "C0103", # (invalid-name)
|
||||||
|
# "C0114", # (missing-module-docstring)
|
||||||
|
# "C0115", # (missing-class-docstring)
|
||||||
|
# "C0116", # (missing-function-docstring)
|
||||||
|
# "R0903", # (too-few-public-methods)
|
||||||
|
# "R0913", # (too-many-arguments)
|
||||||
|
# "W0105", # (pointless-string-statement)
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.
|
Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__all__ = ['WavInfoReader', 'WavInfoEOFError']
|
__all__ = ["WavInfoEOFError", "WavInfoReader"]
|
||||||
|
|
||||||
from .wave_reader import WavInfoReader
|
|
||||||
from .riff_parser import WavInfoEOFError
|
from .riff_parser import WavInfoEOFError
|
||||||
|
from .wave_reader import WavInfoReader
|
||||||
|
|||||||
+51
-46
@@ -1,16 +1,16 @@
|
|||||||
from . import WavInfoReader
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from optparse import OptionParser
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from enum import Enum
|
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from cmd import Cmd
|
from cmd import Cmd
|
||||||
|
from enum import Enum
|
||||||
|
from optparse import OptionParser
|
||||||
from shlex import split
|
from shlex import split
|
||||||
from typing import List, Dict, Union
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
|
from . import WavInfoReader
|
||||||
|
|
||||||
|
|
||||||
class MyJSONEncoder(json.JSONEncoder):
|
class MyJSONEncoder(json.JSONEncoder):
|
||||||
@@ -18,7 +18,7 @@ class MyJSONEncoder(json.JSONEncoder):
|
|||||||
if isinstance(o, Enum):
|
if isinstance(o, Enum):
|
||||||
return o._name_
|
return o._name_
|
||||||
elif isinstance(o, bytes):
|
elif isinstance(o, bytes):
|
||||||
return 'base64:' + b64encode(o).decode('ascii')
|
return "base64:" + b64encode(o).decode("ascii")
|
||||||
else:
|
else:
|
||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
@@ -31,7 +31,11 @@ class MetaBrowser(Cmd):
|
|||||||
prompt = "(wavinfo) "
|
prompt = "(wavinfo) "
|
||||||
|
|
||||||
metadata: Union[List, Dict]
|
metadata: Union[List, Dict]
|
||||||
path: List[str] = []
|
path: List[str]
|
||||||
|
|
||||||
|
def preloop(self) -> None:
|
||||||
|
self.path = []
|
||||||
|
return super().preloop()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cwd(self):
|
def cwd(self):
|
||||||
@@ -50,7 +54,7 @@ class MetaBrowser(Cmd):
|
|||||||
if isinstance(val, int):
|
if isinstance(val, int):
|
||||||
print(f" - {key}: {val}")
|
print(f" - {key}: {val}")
|
||||||
elif isinstance(val, str):
|
elif isinstance(val, str):
|
||||||
print(f" - {key}: \"{val}\"")
|
print(f' - {key}: "{val}"')
|
||||||
elif isinstance(val, dict):
|
elif isinstance(val, dict):
|
||||||
print(f" - {key}: Dict ({len(val)} keys)")
|
print(f" - {key}: Dict ({len(val)} keys)")
|
||||||
elif isinstance(val, list):
|
elif isinstance(val, list):
|
||||||
@@ -63,7 +67,7 @@ class MetaBrowser(Cmd):
|
|||||||
print(f" - {key}: Unknown")
|
print(f" - {key}: Unknown")
|
||||||
|
|
||||||
def do_ls(self, _):
|
def do_ls(self, _):
|
||||||
'List items at the current node: LS'
|
"List items at the current node: LS"
|
||||||
root = self.cwd
|
root = self.cwd
|
||||||
|
|
||||||
if isinstance(root, list):
|
if isinstance(root, list):
|
||||||
@@ -91,10 +95,10 @@ class MetaBrowser(Cmd):
|
|||||||
else:
|
else:
|
||||||
print(f"Index {argv[0]} does not exist")
|
print(f"Index {argv[0]} does not exist")
|
||||||
elif isinstance(self.cwd, dict):
|
elif isinstance(self.cwd, dict):
|
||||||
if argv[0] in self.cwd.keys():
|
if argv[0] in self.cwd:
|
||||||
self.path = self.path + [argv[0]]
|
self.path = self.path + [argv[0]]
|
||||||
else:
|
else:
|
||||||
print(f"Key \"{argv[0]}\" does not exist")
|
print(f'Key "{argv[0]}" does not exist')
|
||||||
|
|
||||||
if len(self.path) > 0:
|
if len(self.path) > 0:
|
||||||
self.prompt = "(" + "/".join(self.path) + ") "
|
self.prompt = "(" + "/".join(self.path) + ") "
|
||||||
@@ -102,41 +106,40 @@ class MetaBrowser(Cmd):
|
|||||||
self.prompt = "(wavinfo) "
|
self.prompt = "(wavinfo) "
|
||||||
|
|
||||||
def do_bye(self, _):
|
def do_bye(self, _):
|
||||||
'Exit the interactive browser: BYE'
|
"Exit the interactive browser: BYE"
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
version = importlib.metadata.version('wavinfo')
|
version = importlib.metadata.version("wavinfo")
|
||||||
manpath = os.path.dirname(__file__) + "/man"
|
manpath = os.path.dirname(__file__) + "/man"
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
|
||||||
parser.usage = 'wavinfo (--adm | --ixml) <FILE> +'
|
parser.usage = "wavinfo (--adm | --ixml) <FILE> +"
|
||||||
|
|
||||||
# parser.add_option('--install-manpages',
|
# parser.add_option('--install-manpages',
|
||||||
# help="Install manual pages for wavinfo",
|
# help="Install manual pages for wavinfo",
|
||||||
# default=False,
|
# default=False,
|
||||||
# action='store_true')
|
# action='store_true')
|
||||||
|
|
||||||
parser.add_option('--man',
|
parser.add_option(
|
||||||
help="Read the manual and exit.",
|
"--man", help="Read the manual and exit.", default=False, action="store_true"
|
||||||
default=False,
|
)
|
||||||
action='store_true')
|
|
||||||
|
|
||||||
parser.add_option('--adm', dest='adm',
|
parser.add_option(
|
||||||
help='Output ADM XML',
|
"--adm", dest="adm", help="Output ADM XML", default=False, action="store_true"
|
||||||
default=False,
|
)
|
||||||
action='store_true')
|
|
||||||
|
|
||||||
parser.add_option('--ixml', dest='ixml',
|
parser.add_option(
|
||||||
help='Output iXML',
|
"--ixml", dest="ixml", help="Output iXML", default=False, action="store_true"
|
||||||
default=False,
|
)
|
||||||
action='store_true')
|
|
||||||
|
|
||||||
parser.add_option('-i',
|
parser.add_option(
|
||||||
help='Read metadata with an interactive prompt',
|
"-i",
|
||||||
default=False,
|
help="Read metadata with an interactive prompt",
|
||||||
action='store_true')
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
(options, args) = parser.parse_args(sys.argv)
|
(options, args) = parser.parse_args(sys.argv)
|
||||||
|
|
||||||
@@ -149,6 +152,7 @@ def main():
|
|||||||
|
|
||||||
if options.man:
|
if options.man:
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
print("Which man page?")
|
print("Which man page?")
|
||||||
print("1) wavinfo usage")
|
print("1) wavinfo usage")
|
||||||
print("7) General info on Wave file metadata")
|
print("7) General info on Wave file metadata")
|
||||||
@@ -176,29 +180,30 @@ def main():
|
|||||||
raise MissingDataError("ixml")
|
raise MissingDataError("ixml")
|
||||||
else:
|
else:
|
||||||
ret_dict = {
|
ret_dict = {
|
||||||
'filename': arg,
|
"filename": arg,
|
||||||
'run_date': datetime.datetime.now().isoformat(),
|
"run_date": datetime.datetime.now(
|
||||||
'application': f"wavinfo {version}",
|
tz=datetime.timezone.utc
|
||||||
'scopes': {}
|
).isoformat(),
|
||||||
|
"application": f"wavinfo {version}",
|
||||||
|
"scopes": {},
|
||||||
}
|
}
|
||||||
for scope, name, value in this_file.walk():
|
for scope, name, value in this_file.walk():
|
||||||
if scope not in ret_dict['scopes'].keys():
|
if scope not in ret_dict["scopes"]:
|
||||||
ret_dict['scopes'][scope] = {}
|
ret_dict["scopes"][scope] = {}
|
||||||
|
|
||||||
ret_dict['scopes'][scope][name] = value
|
ret_dict["scopes"][scope][name] = value
|
||||||
|
|
||||||
if options.i:
|
if options.i:
|
||||||
interactive_dict.append(ret_dict)
|
interactive_dict.append(ret_dict)
|
||||||
else:
|
else:
|
||||||
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout,
|
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
|
||||||
indent=2)
|
|
||||||
|
|
||||||
except MissingDataError as e:
|
except MissingDataError as e:
|
||||||
print("MissingDataError: Missing metadata (%s) in file %s" %
|
print(
|
||||||
(e, arg), file=sys.stderr)
|
"MissingDataError: Missing metadata (%s) in file %s" % (e, arg),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if len(interactive_dict) > 0:
|
if len(interactive_dict) > 0:
|
||||||
cli = MetaBrowser()
|
cli = MetaBrowser()
|
||||||
|
|||||||
+13
-12
@@ -1,6 +1,7 @@
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
# from collections import namedtuple
|
# from collections import namedtuple
|
||||||
from typing import NamedTuple, Dict
|
from typing import Dict, NamedTuple
|
||||||
|
|
||||||
from . import riff_parser
|
from . import riff_parser
|
||||||
|
|
||||||
@@ -10,38 +11,38 @@ class RF64Context(NamedTuple):
|
|||||||
bigchunk_table: Dict[str, int]
|
bigchunk_table: Dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
def parse_rf64(stream, signature=b'RF64') -> RF64Context:
|
def parse_rf64(stream, signature=b"RF64") -> RF64Context:
|
||||||
start = stream.tell()
|
start = stream.tell()
|
||||||
assert stream.read(4) == b'WAVE'
|
assert stream.read(4) == b"WAVE"
|
||||||
|
|
||||||
ds64_chunk = riff_parser.parse_chunk(stream)
|
ds64_chunk = riff_parser.parse_chunk(stream)
|
||||||
assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \
|
assert type(ds64_chunk) is riff_parser.ChunkDescriptor, (
|
||||||
f"Expected ds64 chunk here, found {type(ds64_chunk)}"
|
f"Expected ds64 chunk here, found {type(ds64_chunk)}"
|
||||||
|
)
|
||||||
|
|
||||||
ds64_field_spec = "<QQQI"
|
ds64_field_spec = "<QQQI"
|
||||||
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
||||||
assert ds64_chunk.ident == b'ds64'
|
assert ds64_chunk.ident == b"ds64"
|
||||||
|
|
||||||
ds64_data = ds64_chunk.read_data(stream)
|
ds64_data = ds64_chunk.read_data(stream)
|
||||||
assert len(ds64_data) >= ds64_fields_size
|
assert len(ds64_data) >= ds64_fields_size
|
||||||
|
|
||||||
riff_size, data_size, sample_count, length_lookup_table = struct.unpack(
|
riff_size, data_size, sample_count, length_lookup_table = struct.unpack(
|
||||||
ds64_field_spec, ds64_data[0:ds64_fields_size]
|
ds64_field_spec, ds64_data[0:ds64_fields_size]
|
||||||
)
|
)
|
||||||
|
|
||||||
bigchunk_table = {}
|
bigchunk_table = {}
|
||||||
chunksize64format = "<4sL"
|
chunksize64format = "<4sL"
|
||||||
# chunksize64size = struct.calcsize(chunksize64format)
|
# chunksize64size = struct.calcsize(chunksize64format)
|
||||||
|
|
||||||
for _ in range(length_lookup_table):
|
for _ in range(length_lookup_table):
|
||||||
bigname, bigsize = struct.unpack_from(chunksize64format,
|
bigname, bigsize = struct.unpack_from(
|
||||||
ds64_data,
|
chunksize64format, ds64_data, offset=ds64_fields_size
|
||||||
offset=ds64_fields_size)
|
)
|
||||||
bigchunk_table[bigname] = bigsize
|
bigchunk_table[bigname] = bigsize
|
||||||
|
|
||||||
bigchunk_table[b'data'] = data_size
|
bigchunk_table[b"data"] = data_size
|
||||||
bigchunk_table[signature] = riff_size
|
bigchunk_table[signature] = riff_size
|
||||||
|
|
||||||
stream.seek(start, 0)
|
stream.seek(start, 0)
|
||||||
return RF64Context(sample_count=sample_count,
|
return RF64Context(sample_count=sample_count, bigchunk_table=bigchunk_table)
|
||||||
bigchunk_table=bigchunk_table)
|
|
||||||
|
|||||||
+15
-13
@@ -1,7 +1,8 @@
|
|||||||
# from optparse import Option
|
# from optparse import Option
|
||||||
import struct
|
import struct
|
||||||
from .rf64_parser import parse_rf64, RF64Context
|
from typing import List, NamedTuple, Optional, Union
|
||||||
from typing import NamedTuple, Union, List, Optional
|
|
||||||
|
from .rf64_parser import RF64Context, parse_rf64
|
||||||
|
|
||||||
|
|
||||||
class WavInfoEOFError(EOFError):
|
class WavInfoEOFError(EOFError):
|
||||||
@@ -12,7 +13,7 @@ class WavInfoEOFError(EOFError):
|
|||||||
|
|
||||||
class ListChunkDescriptor(NamedTuple):
|
class ListChunkDescriptor(NamedTuple):
|
||||||
signature: bytes
|
signature: bytes
|
||||||
children: List[Union['ChunkDescriptor', 'ListChunkDescriptor']]
|
children: List[Union["ChunkDescriptor", "ListChunkDescriptor"]]
|
||||||
|
|
||||||
|
|
||||||
class ChunkDescriptor(NamedTuple):
|
class ChunkDescriptor(NamedTuple):
|
||||||
@@ -48,14 +49,15 @@ def parse_chunk(stream, rf64_context=None):
|
|||||||
if len(ident) != 4 or len(size_bytes) != 4:
|
if len(ident) != 4 or len(size_bytes) != 4:
|
||||||
raise WavInfoEOFError(identifier=ident, chunk_start=header_start)
|
raise WavInfoEOFError(identifier=ident, chunk_start=header_start)
|
||||||
|
|
||||||
data_size = struct.unpack('<I', size_bytes)[0]
|
data_size = struct.unpack("<I", size_bytes)[0]
|
||||||
|
|
||||||
if data_size == 0xFFFFFFFF:
|
if data_size == 0xFFFFFFFF:
|
||||||
if rf64_context is None and ident in {b'RF64', b'BW64'}:
|
if rf64_context is None and ident in {b"RF64", b"BW64"}:
|
||||||
rf64_context = parse_rf64(stream=stream, signature=ident)
|
rf64_context = parse_rf64(stream=stream, signature=ident)
|
||||||
|
|
||||||
assert rf64_context is not None, \
|
assert rf64_context is not None, (
|
||||||
"Sentinel data size 0xFFFFFFFF found outside of RF64 context"
|
"Sentinel data size 0xFFFFFFFF found outside of RF64 context"
|
||||||
|
)
|
||||||
|
|
||||||
data_size = rf64_context.bigchunk_table[ident]
|
data_size = rf64_context.bigchunk_table[ident]
|
||||||
|
|
||||||
@@ -63,14 +65,14 @@ def parse_chunk(stream, rf64_context=None):
|
|||||||
if displacement % 2:
|
if displacement % 2:
|
||||||
displacement += 1
|
displacement += 1
|
||||||
|
|
||||||
if ident in {b'RIFF', b'LIST', b'RF64', b'BW64', b'list'}:
|
if ident in {b"RIFF", b"LIST", b"RF64", b"BW64", b"list"}:
|
||||||
return parse_list_chunk(stream=stream, length=data_size,
|
return parse_list_chunk(
|
||||||
rf64_context=rf64_context)
|
stream=stream, length=data_size, rf64_context=rf64_context
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
data_start = stream.tell()
|
data_start = stream.tell()
|
||||||
stream.seek(displacement, 1)
|
stream.seek(displacement, 1)
|
||||||
return ChunkDescriptor(ident=ident,
|
return ChunkDescriptor(
|
||||||
start=data_start,
|
ident=ident, start=data_start, length=data_size, rf64_context=rf64_context
|
||||||
length=data_size,
|
)
|
||||||
rf64_context=rf64_context)
|
|
||||||
|
|||||||
+100
-77
@@ -2,15 +2,14 @@
|
|||||||
ADM Reader
|
ADM Reader
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from struct import unpack, unpack_from, calcsize
|
|
||||||
from io import BytesIO
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from io import BytesIO
|
||||||
|
from struct import calcsize, unpack, unpack_from
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from lxml import etree as ET
|
from lxml import etree as ET
|
||||||
|
|
||||||
|
ChannelEntry = namedtuple("ChannelEntry", "track_index uid track_ref pack_ref")
|
||||||
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
|
|
||||||
|
|
||||||
|
|
||||||
class WavADMReader:
|
class WavADMReader:
|
||||||
@@ -32,19 +31,19 @@ class WavADMReader:
|
|||||||
|
|
||||||
offset = calcsize(header_fmt)
|
offset = calcsize(header_fmt)
|
||||||
for _ in range(uid_count):
|
for _ in range(uid_count):
|
||||||
|
track_index, uid, track_ref, pack_ref = unpack_from(
|
||||||
track_index, uid, track_ref, pack_ref = unpack_from(uid_fmt,
|
uid_fmt, chna_data, offset
|
||||||
chna_data,
|
)
|
||||||
offset)
|
|
||||||
|
|
||||||
# these values are either ascii or all null
|
# these values are either ascii or all null
|
||||||
|
|
||||||
self.channel_uids.append(
|
self.channel_uids.append(
|
||||||
ChannelEntry(track_index - 1,
|
ChannelEntry(
|
||||||
uid.decode('ascii'),
|
track_index - 1,
|
||||||
track_ref.decode('ascii'),
|
uid.decode("ascii"),
|
||||||
pack_ref.decode('ascii')
|
track_ref.decode("ascii"),
|
||||||
)
|
pack_ref.decode("ascii"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
offset += calcsize(uid_fmt)
|
offset += calcsize(uid_fmt)
|
||||||
@@ -65,40 +64,40 @@ class WavADMReader:
|
|||||||
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
||||||
|
|
||||||
program = afext.find("audioProgramme", namespaces=nsmap)
|
program = afext.find("audioProgramme", namespaces=nsmap)
|
||||||
ret_dict['programme_id'] = program.get("audioProgrammeID")
|
ret_dict["programme_id"] = program.get("audioProgrammeID")
|
||||||
ret_dict['programme_name'] = program.get("audioProgrammeName")
|
ret_dict["programme_name"] = program.get("audioProgrammeName")
|
||||||
ret_dict['programme_start'] = program.get("start")
|
ret_dict["programme_start"] = program.get("start")
|
||||||
ret_dict['programme_end'] = program.get("end")
|
ret_dict["programme_end"] = program.get("end")
|
||||||
ret_dict['contents'] = []
|
ret_dict["contents"] = []
|
||||||
|
|
||||||
for content_ref in program.findall("audioContentIDRef",
|
for content_ref in program.findall("audioContentIDRef", namespaces=nsmap):
|
||||||
namespaces=nsmap):
|
|
||||||
content_dict = dict()
|
content_dict = dict()
|
||||||
content_dict['content_id'] = cid = content_ref.text
|
content_dict["content_id"] = cid = content_ref.text
|
||||||
content = afext.find("audioContent[@audioContentID='%s']" % cid,
|
content = afext.find(
|
||||||
namespaces=nsmap)
|
"audioContent[@audioContentID='%s']" % cid, namespaces=nsmap
|
||||||
content_dict['content_name'] = content.get("audioContentName")
|
)
|
||||||
content_dict['objects'] = []
|
content_dict["content_name"] = content.get("audioContentName")
|
||||||
|
content_dict["objects"] = []
|
||||||
|
|
||||||
for object_ref in content.findall("audioObjectIDRef",
|
for object_ref in content.findall("audioObjectIDRef", namespaces=nsmap):
|
||||||
namespaces=nsmap):
|
|
||||||
object_dict = dict()
|
object_dict = dict()
|
||||||
object_dict['object_id'] = oid = object_ref.text
|
object_dict["object_id"] = oid = object_ref.text
|
||||||
object = afext.find("audioObject[@audioObjectID='%s']" % oid,
|
object = afext.find(
|
||||||
namespaces=nsmap)
|
"audioObject[@audioObjectID='%s']" % oid, namespaces=nsmap
|
||||||
|
)
|
||||||
pack = object.find("audioPackFormatIDRef", namespaces=nsmap)
|
pack = object.find("audioPackFormatIDRef", namespaces=nsmap)
|
||||||
object_dict['object_name'] = object.get("audioObjectName")
|
object_dict["object_name"] = object.get("audioObjectName")
|
||||||
object_dict['object_start'] = object.get("start")
|
object_dict["object_start"] = object.get("start")
|
||||||
object_dict['object_duration'] = object.get("duration")
|
object_dict["object_duration"] = object.get("duration")
|
||||||
object_dict['pack_id'] = pack.text
|
object_dict["pack_id"] = pack.text
|
||||||
track_uid_list = []
|
track_uid_list = []
|
||||||
for t in object.findall("audioTrackUIDRef", namespaces=nsmap):
|
for t in object.findall("audioTrackUIDRef", namespaces=nsmap):
|
||||||
track_uid_list.append(t.text)
|
track_uid_list.append(t.text)
|
||||||
|
|
||||||
object_dict['track_uids'] = track_uid_list
|
object_dict["track_uids"] = track_uid_list
|
||||||
content_dict['objects'].append(object_dict)
|
content_dict["objects"].append(object_dict)
|
||||||
|
|
||||||
ret_dict['contents'].append(content_dict)
|
ret_dict["contents"].append(content_dict)
|
||||||
|
|
||||||
return ret_dict
|
return ret_dict
|
||||||
|
|
||||||
@@ -111,8 +110,9 @@ class WavADMReader:
|
|||||||
*object_name*, *object_id*,
|
*object_name*, *object_id*,
|
||||||
*pack_format_name*, *pack_type*, *channel_format_name*
|
*pack_format_name*, *pack_type*, *channel_format_name*
|
||||||
"""
|
"""
|
||||||
channel_info = next((x for x in self.channel_uids
|
channel_info = next(
|
||||||
if x.track_index == index), None)
|
(x for x in self.channel_uids if x.track_index == index), None
|
||||||
|
)
|
||||||
|
|
||||||
if channel_info is None:
|
if channel_info is None:
|
||||||
return None
|
return None
|
||||||
@@ -121,56 +121,78 @@ class WavADMReader:
|
|||||||
|
|
||||||
nsmap = self.axml.getroot().nsmap
|
nsmap = self.axml.getroot().nsmap
|
||||||
|
|
||||||
afext = self.axml.find(".//audioFormatExtended",
|
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
||||||
namespaces=nsmap)
|
|
||||||
|
|
||||||
trackformat_elem = afext.find(
|
trackformat_elem = afext.find(
|
||||||
"audioTrackFormat[@audioTrackFormatID='%s']"
|
"audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref,
|
||||||
% channel_info.track_ref, namespaces=nsmap)
|
namespaces=nsmap,
|
||||||
|
)
|
||||||
|
|
||||||
stream_id = trackformat_elem[0].text
|
stream_id = trackformat_elem[0].text
|
||||||
|
|
||||||
channelformatref_elem = afext.find(
|
channelformatref_elem = afext.find(
|
||||||
("audioStreamFormat[@audioStreamFormatID='%s']"
|
("audioStreamFormat[@audioStreamFormatID='%s']/audioChannelFormatIDRef")
|
||||||
"/audioChannelFormatIDRef") % stream_id,
|
% stream_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap,
|
||||||
channelformat_id = channelformatref_elem.text
|
)
|
||||||
|
if channelformatref_elem is not None:
|
||||||
|
channelformat_id = channelformatref_elem.text
|
||||||
|
else:
|
||||||
|
channelformat_id = None
|
||||||
|
|
||||||
packformatref_elem = afext.find(
|
packformatref_elem = afext.find(
|
||||||
("audioStreamFormat[@audioStreamFormatID='%s']"
|
("audioStreamFormat[@audioStreamFormatID='%s']/audioPackFormatIDRef")
|
||||||
"/audioPackFormatIDRef") % stream_id,
|
% stream_id,
|
||||||
namespaces=nsmap)
|
namespaces=nsmap,
|
||||||
packformat_id = packformatref_elem.text
|
)
|
||||||
|
|
||||||
channelformat_elem = afext\
|
if packformatref_elem is not None:
|
||||||
.find("audioChannelFormat[@audioChannelFormatID='%s']"
|
packformat_id = packformatref_elem.text
|
||||||
% channelformat_id,
|
else:
|
||||||
namespaces=nsmap)
|
packformat_id = None
|
||||||
ret_dict['channel_format_name'] = channelformat_elem.get(
|
|
||||||
"audioChannelFormatName")
|
if channelformat_id:
|
||||||
|
channelformat_elem = afext.find(
|
||||||
|
"audioChannelFormat[@audioChannelFormatID='%s']" % channelformat_id,
|
||||||
|
namespaces=nsmap,
|
||||||
|
)
|
||||||
|
ret_dict["channel_format_name"] = channelformat_elem.get(
|
||||||
|
"audioChannelFormatName"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ret_dict["channel_format_name"] = None
|
||||||
|
|
||||||
packformat_elem = afext.find(
|
packformat_elem = afext.find(
|
||||||
"audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
|
"audioPackFormat[@audioPackFormatID='%s']" % packformat_id, namespaces=nsmap
|
||||||
namespaces=nsmap)
|
)
|
||||||
ret_dict['pack_type'] = packformat_elem.get(
|
if packformat_elem is not None:
|
||||||
"typeDefinition")
|
ret_dict["pack_type"] = packformat_elem.get("typeDefinition")
|
||||||
ret_dict['pack_format_name'] = packformat_elem.get(
|
ret_dict["pack_format_name"] = packformat_elem.get("audioPackFormatName")
|
||||||
"audioPackFormatName")
|
else:
|
||||||
|
ret_dict["pack_type"] = None
|
||||||
|
ret_dict["pack_format_name"] = None
|
||||||
|
|
||||||
object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']"
|
object_elem = afext.find(
|
||||||
% packformat_id,
|
"audioObject[audioPackFormatIDRef = '%s']" % packformat_id, namespaces=nsmap
|
||||||
namespaces=nsmap)
|
)
|
||||||
|
|
||||||
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
|
if object_elem is not None:
|
||||||
object_id = object_elem.get("audioObjectID")
|
ret_dict["audio_object_name"] = object_elem.get("audioObjectName")
|
||||||
ret_dict['object_id'] = object_id
|
object_id = object_elem.get("audioObjectID")
|
||||||
|
ret_dict["object_id"] = object_id
|
||||||
|
|
||||||
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']"
|
content_elem = afext.find(
|
||||||
% object_id,
|
"audioContent/[audioObjectIDRef = '%s']" % object_id, namespaces=nsmap
|
||||||
namespaces=nsmap)
|
)
|
||||||
|
|
||||||
ret_dict['content_name'] = content_elem.get("audioContentName")
|
ret_dict["content_name"] = content_elem.get("audioContentName")
|
||||||
ret_dict['content_id'] = content_elem.get("audioContentID")
|
ret_dict["content_id"] = content_elem.get("audioContentID")
|
||||||
|
|
||||||
|
else:
|
||||||
|
ret_dict["audio_object_name"] = None
|
||||||
|
ret_dict["object_id"] = None
|
||||||
|
ret_dict["content_name"] = None
|
||||||
|
ret_dict["content_id"] = None
|
||||||
|
|
||||||
return ret_dict
|
return ret_dict
|
||||||
|
|
||||||
@@ -184,6 +206,7 @@ class WavADMReader:
|
|||||||
rd.update(self.track_info(channel_uid_rec.track_index))
|
rd.update(self.track_info(channel_uid_rec.track_index))
|
||||||
return rd
|
return rd
|
||||||
|
|
||||||
return dict(channel_entries=list(map(lambda z: make_entry(z),
|
return dict(
|
||||||
self.channel_uids)),
|
channel_entries=list(map(lambda z: make_entry(z), self.channel_uids)),
|
||||||
programme=self.programme())
|
programme=self.programme(),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import struct
|
import struct
|
||||||
# from .umid_parser import UMIDParser
|
|
||||||
|
|
||||||
|
# from .umid_parser import UMIDParser
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
@@ -13,16 +13,18 @@ class WavBextReader:
|
|||||||
the BEXT metadata scope. According to EBU Rec 3285 this shall be
|
the BEXT metadata scope. According to EBU Rec 3285 this shall be
|
||||||
ASCII.
|
ASCII.
|
||||||
"""
|
"""
|
||||||
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + \
|
packstring = (
|
||||||
"hhhhh" + "180s"
|
"<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
||||||
|
)
|
||||||
|
|
||||||
rest_starts = struct.calcsize(packstring)
|
rest_starts = struct.calcsize(packstring)
|
||||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||||
|
|
||||||
def sanitize_bytes(b: bytes) -> str:
|
def sanitize_bytes(b: bytes) -> str:
|
||||||
# honestly can't remember why I'm stripping nulls this way
|
# honestly can't remember why I'm stripping nulls this way
|
||||||
first_null = next((index for index, byte in enumerate(b)
|
first_null = next(
|
||||||
if byte == 0), None)
|
(index for index, byte in enumerate(b) if byte == 0), None
|
||||||
|
)
|
||||||
trimmed = b if first_null is None else b[:first_null]
|
trimmed = b if first_null is None else b[:first_null]
|
||||||
decoded = trimmed.decode(encoding)
|
decoded = trimmed.decode(encoding)
|
||||||
return decoded
|
return decoded
|
||||||
@@ -91,18 +93,19 @@ class WavBextReader:
|
|||||||
|
|
||||||
# umid_str = None
|
# umid_str = None
|
||||||
|
|
||||||
return {'description': self.description,
|
return {
|
||||||
'originator': self.originator,
|
"description": self.description,
|
||||||
'originator_ref': self.originator_ref,
|
"originator": self.originator,
|
||||||
'originator_date': self.originator_date,
|
"originator_ref": self.originator_ref,
|
||||||
'originator_time': self.originator_time,
|
"originator_date": self.originator_date,
|
||||||
'time_reference': self.time_reference,
|
"originator_time": self.originator_time,
|
||||||
'version': self.version,
|
"time_reference": self.time_reference,
|
||||||
'umid': self.umid,
|
"version": self.version,
|
||||||
'coding_history': self.coding_history,
|
"umid": self.umid,
|
||||||
'loudness_value': self.loudness_value,
|
"coding_history": self.coding_history,
|
||||||
'loudness_range': self.loudness_range,
|
"loudness_value": self.loudness_value,
|
||||||
'max_true_peak': self.max_true_peak,
|
"loudness_range": self.loudness_range,
|
||||||
'max_momentary_loudness': self.max_momentary_loudness,
|
"max_true_peak": self.max_true_peak,
|
||||||
'max_shortterm_loudness': self.max_shortterm_loudness
|
"max_momentary_loudness": self.max_momentary_loudness,
|
||||||
}
|
"max_shortterm_loudness": self.max_shortterm_loudness,
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ IBM Corporation and Microsoft Corporation
|
|||||||
|
|
||||||
https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
|
||||||
from .riff_parser import ChunkDescriptor
|
|
||||||
|
|
||||||
from struct import unpack, calcsize
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Tuple, NamedTuple, List, Dict, Any, Generator
|
from struct import calcsize, unpack
|
||||||
|
from typing import Any, Dict, Generator, List, NamedTuple, Optional, Tuple
|
||||||
|
|
||||||
|
from .riff_parser import ChunkDescriptor
|
||||||
|
|
||||||
#: Country Codes used in the RIFF standard to resolve locale. These codes
|
#: Country Codes used in the RIFF standard to resolve locale. These codes
|
||||||
#: appear in CSET and LTXT metadata.
|
#: appear in CSET and LTXT metadata.
|
||||||
@@ -100,6 +101,7 @@ class CueEntry(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
A ``cue`` element structure.
|
A ``cue`` element structure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: Cue "name" or id number
|
#: Cue "name" or id number
|
||||||
name: int
|
name: int
|
||||||
#: Cue position, as a frame count in the play order of the WAVE file. In
|
#: Cue position, as a frame count in the play order of the WAVE file. In
|
||||||
@@ -118,29 +120,37 @@ class CueEntry(NamedTuple):
|
|||||||
return calcsize(cls.Format)
|
return calcsize(cls.Format)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls, data: bytes) -> 'CueEntry':
|
def read(cls, data: bytes) -> "CueEntry":
|
||||||
assert len(data) == cls.format_size(), \
|
assert len(data) == cls.format_size(), (
|
||||||
(f"cue data size incorrect, expected {calcsize(cls.Format)} "
|
f"cue data size incorrect, expected {calcsize(cls.Format)} "
|
||||||
"found {len(data)}")
|
"found {len(data)}"
|
||||||
|
)
|
||||||
|
|
||||||
parsed = unpack(cls.Format, data)
|
parsed = unpack(cls.Format, data)
|
||||||
|
|
||||||
return cls(name=parsed[0], position=parsed[1], chunk_id=parsed[2],
|
return cls(
|
||||||
chunk_start=parsed[3], block_start=parsed[4],
|
name=parsed[0],
|
||||||
sample_offset=parsed[5])
|
position=parsed[1],
|
||||||
|
chunk_id=parsed[2],
|
||||||
|
chunk_start=parsed[3],
|
||||||
|
block_start=parsed[4],
|
||||||
|
sample_offset=parsed[5],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LabelEntry(NamedTuple):
|
class LabelEntry(NamedTuple):
|
||||||
"""
|
"""
|
||||||
A ``labl`` structure.
|
A ``labl`` structure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: int
|
name: int
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls, data: bytes, encoding: str):
|
def read(cls, data: bytes, encoding: str):
|
||||||
return cls(name=unpack("<I", data[0:4])[0],
|
return cls(
|
||||||
text=data[4:].decode(encoding).rstrip("\0"))
|
name=unpack("<I", data[0:4])[0], text=data[4:].decode(encoding).rstrip("\0")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
NoteEntry = LabelEntry
|
NoteEntry = LabelEntry
|
||||||
@@ -150,6 +160,7 @@ class RangeLabel(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
A ``ltxt`` structure.
|
A ``ltxt`` structure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: int
|
name: int
|
||||||
length: int
|
length: int
|
||||||
purpose: str
|
purpose: str
|
||||||
@@ -162,21 +173,26 @@ class RangeLabel(NamedTuple):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def read(cls, data: bytes, fallback_encoding: str):
|
def read(cls, data: bytes, fallback_encoding: str):
|
||||||
leader_struct_fmt = "<II4sHHHH"
|
leader_struct_fmt = "<II4sHHHH"
|
||||||
parsed = unpack(leader_struct_fmt, data[0:calcsize(leader_struct_fmt)])
|
parsed = unpack(leader_struct_fmt, data[0 : calcsize(leader_struct_fmt)])
|
||||||
text_data = data[calcsize(leader_struct_fmt):]
|
text_data = data[calcsize(leader_struct_fmt) :]
|
||||||
|
|
||||||
if data[6] != 0:
|
if data[6] != 0:
|
||||||
fallback_encoding = f"cp{data[6]}"
|
fallback_encoding = f"cp{data[6]}"
|
||||||
|
|
||||||
return cls(name=parsed[0], length=parsed[1], purpose=parsed[2],
|
return cls(
|
||||||
country=parsed[3], language=parsed[4],
|
name=parsed[0],
|
||||||
dialect=parsed[5], codepage=parsed[6],
|
length=parsed[1],
|
||||||
text=text_data.decode(fallback_encoding))
|
purpose=parsed[2],
|
||||||
|
country=parsed[3],
|
||||||
|
language=parsed[4],
|
||||||
|
dialect=parsed[5],
|
||||||
|
codepage=parsed[6],
|
||||||
|
text=text_data.decode(fallback_encoding),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WavCuesReader:
|
class WavCuesReader:
|
||||||
|
|
||||||
#: Every ``cue`` entry in the file
|
#: Every ``cue`` entry in the file
|
||||||
cues: List[CueEntry]
|
cues: List[CueEntry]
|
||||||
|
|
||||||
@@ -190,13 +206,15 @@ class WavCuesReader:
|
|||||||
notes: List[NoteEntry]
|
notes: List[NoteEntry]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_all(cls, f,
|
def read_all(
|
||||||
cues: Optional[ChunkDescriptor],
|
cls,
|
||||||
labls: List[ChunkDescriptor],
|
f,
|
||||||
ltxts: List[ChunkDescriptor],
|
cues: Optional[ChunkDescriptor],
|
||||||
notes: List[ChunkDescriptor],
|
labls: List[ChunkDescriptor],
|
||||||
fallback_encoding: str) -> 'WavCuesReader':
|
ltxts: List[ChunkDescriptor],
|
||||||
|
notes: List[ChunkDescriptor],
|
||||||
|
fallback_encoding: str,
|
||||||
|
) -> "WavCuesReader":
|
||||||
cue_list = []
|
cue_list = []
|
||||||
if cues is not None:
|
if cues is not None:
|
||||||
cues_data = cues.read_data(f)
|
cues_data = cues.read_data(f)
|
||||||
@@ -205,33 +223,31 @@ class WavCuesReader:
|
|||||||
cues_count = unpack("<I", cues_data[0:offset])
|
cues_count = unpack("<I", cues_data[0:offset])
|
||||||
|
|
||||||
for _ in range(cues_count[0]):
|
for _ in range(cues_count[0]):
|
||||||
cue_bytes = cues_data[offset: offset + CueEntry.format_size()]
|
cue_bytes = cues_data[offset : offset + CueEntry.format_size()]
|
||||||
cue_list.append(CueEntry.read(cue_bytes))
|
cue_list.append(CueEntry.read(cue_bytes))
|
||||||
offset += CueEntry.format_size()
|
offset += CueEntry.format_size()
|
||||||
|
|
||||||
label_list = []
|
label_list = []
|
||||||
for labl in labls:
|
for labl in labls:
|
||||||
label_list.append(
|
label_list.append(
|
||||||
LabelEntry.read(labl.read_data(f),
|
LabelEntry.read(labl.read_data(f), encoding=fallback_encoding)
|
||||||
encoding=fallback_encoding)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
range_list = []
|
range_list = []
|
||||||
for r in ltxts:
|
for r in ltxts:
|
||||||
range_list.append(
|
range_list.append(
|
||||||
RangeLabel.read(r.read_data(f),
|
RangeLabel.read(r.read_data(f), fallback_encoding=fallback_encoding)
|
||||||
fallback_encoding=fallback_encoding)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
note_list = []
|
note_list = []
|
||||||
for note in notes:
|
for note in notes:
|
||||||
note_list.append(
|
note_list.append(
|
||||||
NoteEntry.read(note.read_data(f),
|
NoteEntry.read(note.read_data(f), encoding=fallback_encoding)
|
||||||
encoding=fallback_encoding)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return WavCuesReader(cues=cue_list, labels=label_list,
|
return WavCuesReader(
|
||||||
ranges=range_list, notes=note_list)
|
cues=cue_list, labels=label_list, ranges=range_list, notes=note_list
|
||||||
|
)
|
||||||
|
|
||||||
def each_cue(self) -> Generator[Tuple[int, int], None, None]:
|
def each_cue(self) -> Generator[Tuple[int, int], None, None]:
|
||||||
"""
|
"""
|
||||||
@@ -242,8 +258,7 @@ class WavCuesReader:
|
|||||||
for cue in self.cues:
|
for cue in self.cues:
|
||||||
yield (cue.name, cue.sample_offset)
|
yield (cue.name, cue.sample_offset)
|
||||||
|
|
||||||
def label_and_note(self, cue_ident: int) -> Tuple[Optional[str],
|
def label_and_note(self, cue_ident: int) -> Tuple[Optional[str], Optional[str]]:
|
||||||
Optional[str]]:
|
|
||||||
"""
|
"""
|
||||||
Get the label and note (extended comment) for a cue.
|
Get the label and note (extended comment) for a cue.
|
||||||
|
|
||||||
@@ -251,10 +266,10 @@ class WavCuesReader:
|
|||||||
:returns: a tuple of the the cue's label (if present) and note (if
|
:returns: a tuple of the the cue's label (if present) and note (if
|
||||||
present)
|
present)
|
||||||
"""
|
"""
|
||||||
label = next((label.text for label in self.labels
|
label = next(
|
||||||
if label.name == cue_ident), None)
|
(label.text for label in self.labels if label.name == cue_ident), None
|
||||||
note = next((n.text for n in self.notes
|
)
|
||||||
if n.name == cue_ident), None)
|
note = next((n.text for n in self.notes if n.name == cue_ident), None)
|
||||||
return (label, note)
|
return (label, note)
|
||||||
|
|
||||||
def range(self, cue_ident: int) -> Optional[int]:
|
def range(self, cue_ident: int) -> Optional[int]:
|
||||||
@@ -264,23 +279,22 @@ class WavCuesReader:
|
|||||||
:param cue_ident: the cue's name, its unique identifying number
|
:param cue_ident: the cue's name, its unique identifying number
|
||||||
:returns: the length of the marker's range, or `None`
|
:returns: the length of the marker's range, or `None`
|
||||||
"""
|
"""
|
||||||
return next((r.length for r in self.ranges
|
return next((r.length for r in self.ranges if r.name == cue_ident), None)
|
||||||
if r.name == cue_ident), None)
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
retval = dict()
|
retval = dict()
|
||||||
|
|
||||||
for n, t in self.each_cue():
|
for n, t in self.each_cue():
|
||||||
retval[n] = dict()
|
retval[n] = dict()
|
||||||
retval[n]['frame'] = t
|
retval[n]["frame"] = t
|
||||||
label, note = self.label_and_note(n)
|
label, note = self.label_and_note(n)
|
||||||
r = self.range(n)
|
r = self.range(n)
|
||||||
|
|
||||||
if label is not None:
|
if label is not None:
|
||||||
retval[n]['label'] = label
|
retval[n]["label"] = label
|
||||||
if note is not None:
|
if note is not None:
|
||||||
retval[n]['note'] = note
|
retval[n]["note"] = note
|
||||||
if r is not None:
|
if r is not None:
|
||||||
retval[n]['length'] = r
|
retval[n]["length"] = r
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
|
|||||||
+123
-93
@@ -7,18 +7,18 @@ Unless otherwise stated, all § references here are to
|
|||||||
.. _EBU Tech 3285 Supplement 6: https://tech.ebu.ch/docs/tech/tech3285s6.pdf
|
.. _EBU Tech 3285 Supplement 6: https://tech.ebu.ch/docs/tech/tech3285s6.pdf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from enum import IntEnum, Enum
|
from dataclasses import asdict, dataclass
|
||||||
from struct import unpack
|
from enum import Enum, IntEnum
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from typing import List, Tuple, Any, Union
|
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from struct import unpack
|
||||||
|
from typing import Any, List, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
class SegmentType(IntEnum):
|
class SegmentType(IntEnum):
|
||||||
"""
|
"""
|
||||||
Metadata segment type.
|
Metadata segment type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EndMarker = 0x0
|
EndMarker = 0x0
|
||||||
DolbyE = 0x1
|
DolbyE = 0x1
|
||||||
# Reserved2 = 0x2
|
# Reserved2 = 0x2
|
||||||
@@ -29,7 +29,7 @@ class SegmentType(IntEnum):
|
|||||||
DolbyDigitalPlus = 0x7
|
DolbyDigitalPlus = 0x7
|
||||||
AudioInfo = 0x8
|
AudioInfo = 0x8
|
||||||
DolbyAtmos = 0x9
|
DolbyAtmos = 0x9
|
||||||
DolbyAtmosSupplemental = 0xa
|
DolbyAtmosSupplemental = 0xA
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _missing_(cls, val):
|
def _missing_(cls, val):
|
||||||
@@ -82,6 +82,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
"""
|
"""
|
||||||
Dolby surround endcoding mode.
|
Dolby surround endcoding mode.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RESERVED = 0b11
|
RESERVED = 0b11
|
||||||
IN_USE = 0b10
|
IN_USE = 0b10
|
||||||
NOT_IN_USE = 0b01
|
NOT_IN_USE = 0b01
|
||||||
@@ -126,6 +127,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
Dolby Digital Plus `acmod` field
|
Dolby Digital Plus `acmod` field
|
||||||
§ 4.3.2.3
|
§ 4.3.2.3
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RESERVED = 0b000
|
RESERVED = 0b000
|
||||||
CH_ORD_1_0 = 0b001
|
CH_ORD_1_0 = 0b001
|
||||||
"Mono"
|
"Mono"
|
||||||
@@ -163,6 +165,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
Dolby Digital Plus `surmixlev` field
|
Dolby Digital Plus `surmixlev` field
|
||||||
§ 4.3.3.2
|
§ 4.3.3.2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DOWN_3DB = 0b00
|
DOWN_3DB = 0b00
|
||||||
DOWN_6DB = 0b01
|
DOWN_6DB = 0b01
|
||||||
MUTE = 0b10
|
MUTE = 0b10
|
||||||
@@ -174,24 +177,22 @@ class DolbyDigitalPlusMetadata:
|
|||||||
|
|
||||||
Per ATSC/A52 § 5.4.2.12, this is not in use and always 0xFF.
|
Per ATSC/A52 § 5.4.2.12, this is not in use and always 0xFF.
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
class MixLevel(int):
|
class MixLevel(int):
|
||||||
"""
|
"""
|
||||||
§ 4.3.6.2
|
§ 4.3.6.2
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
class DialnormLevel(int):
|
class DialnormLevel(int):
|
||||||
"""
|
"""
|
||||||
§ 4.3.4.4
|
§ 4.3.4.4
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
class RoomType(Enum):
|
class RoomType(Enum):
|
||||||
"""
|
"""
|
||||||
`roomtyp` 4.3.6.3
|
`roomtyp` 4.3.6.3
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NOT_INDICATED = 0b00
|
NOT_INDICATED = 0b00
|
||||||
LARGE_ROOM_X_CURVE = 0b01
|
LARGE_ROOM_X_CURVE = 0b01
|
||||||
SMALL_ROOM_FLAT_CURVE = 0b10
|
SMALL_ROOM_FLAT_CURVE = 0b10
|
||||||
@@ -203,6 +204,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
should downmix.
|
should downmix.
|
||||||
§ 4.3.8.1
|
§ 4.3.8.1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NOT_INDICATED = 0b00
|
NOT_INDICATED = 0b00
|
||||||
PRO_LOGIC = 0b01
|
PRO_LOGIC = 0b01
|
||||||
STEREO = 0b10
|
STEREO = 0b10
|
||||||
@@ -213,6 +215,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
Dolby Surround-EX mode.
|
Dolby Surround-EX mode.
|
||||||
`dsurexmod` § 4.3.9.1
|
`dsurexmod` § 4.3.9.1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NOT_INDICATED = 0b00
|
NOT_INDICATED = 0b00
|
||||||
NOT_SEX = 0b01
|
NOT_SEX = 0b01
|
||||||
SEX = 0b10
|
SEX = 0b10
|
||||||
@@ -222,6 +225,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
"""
|
"""
|
||||||
`dheadphonmod` § 4.3.9.2
|
`dheadphonmod` § 4.3.9.2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NOT_INDICATED = 0b00
|
NOT_INDICATED = 0b00
|
||||||
NOT_DOLBY_HEADPHONE = 0b01
|
NOT_DOLBY_HEADPHONE = 0b01
|
||||||
DOLBY_HEADPHONE = 0b10
|
DOLBY_HEADPHONE = 0b10
|
||||||
@@ -246,6 +250,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
`compr1` RF compression profile
|
`compr1` RF compression profile
|
||||||
§ 4.3.10 (fig 42)
|
§ 4.3.10 (fig 42)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NONE = 0
|
NONE = 0
|
||||||
FILM_STANDARD = 1
|
FILM_STANDARD = 1
|
||||||
FILM_LIGHT = 2
|
FILM_LIGHT = 2
|
||||||
@@ -341,9 +346,11 @@ class DolbyDigitalPlusMetadata:
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
def program_info(b):
|
def program_info(b):
|
||||||
return (b & 0x40) > 0, \
|
return (
|
||||||
DolbyDigitalPlusMetadata.BitStreamMode(b & 0x38 >> 3), \
|
(b & 0x40) > 0,
|
||||||
DolbyDigitalPlusMetadata.AudioCodingMode(b & 0x7)
|
DolbyDigitalPlusMetadata.BitStreamMode(b & 0x38 >> 3),
|
||||||
|
DolbyDigitalPlusMetadata.AudioCodingMode(b & 0x7),
|
||||||
|
)
|
||||||
|
|
||||||
def ddplus_reserved1(_):
|
def ddplus_reserved1(_):
|
||||||
pass
|
pass
|
||||||
@@ -351,39 +358,49 @@ class DolbyDigitalPlusMetadata:
|
|||||||
def surround_config(b):
|
def surround_config(b):
|
||||||
return (
|
return (
|
||||||
DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4),
|
DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4),
|
||||||
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2),
|
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xC >> 2),
|
||||||
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3)
|
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3),
|
||||||
)
|
)
|
||||||
|
|
||||||
def dialnorm_info(b):
|
def dialnorm_info(b):
|
||||||
return (b & 0x80) > 0, b & 0x40 > 0, b & 0x20 > 0, \
|
return (
|
||||||
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
|
(b & 0x80) > 0,
|
||||||
|
b & 0x40 > 0,
|
||||||
|
b & 0x20 > 0,
|
||||||
|
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1F),
|
||||||
|
)
|
||||||
|
|
||||||
def langcod(b) -> int:
|
def langcod(b) -> int:
|
||||||
return b
|
return b
|
||||||
|
|
||||||
def audio_prod_info(b):
|
def audio_prod_info(b):
|
||||||
return (b & 0x80) > 0, \
|
return (
|
||||||
DolbyDigitalPlusMetadata.MixLevel(b & 0x7c >> 2), \
|
(b & 0x80) > 0,
|
||||||
DolbyDigitalPlusMetadata.RoomType(b & 0x3)
|
DolbyDigitalPlusMetadata.MixLevel(b & 0x7C >> 2),
|
||||||
|
DolbyDigitalPlusMetadata.RoomType(b & 0x3),
|
||||||
|
)
|
||||||
|
|
||||||
# loro_center_downmix_level, loro_surround_downmix_level
|
# loro_center_downmix_level, loro_surround_downmix_level
|
||||||
def ext_bsi1_word1(b):
|
def ext_bsi1_word1(b):
|
||||||
return DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
return DolbyDigitalPlusMetadata.DownMixLevelToken(
|
||||||
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
b & 0x38 >> 3
|
||||||
|
), DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
||||||
|
|
||||||
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
|
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
|
||||||
def ext_bsi1_word2(b):
|
def ext_bsi1_word2(b):
|
||||||
return DolbyDigitalPlusMetadata\
|
return (
|
||||||
.PreferredDownMixMode(b & 0xC0 >> 6), \
|
DolbyDigitalPlusMetadata.PreferredDownMixMode(b & 0xC0 >> 6),
|
||||||
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3),
|
||||||
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7),
|
||||||
|
)
|
||||||
|
|
||||||
# surround_ex_mode, dolby_headphone_encoded, ad_converter_type
|
# surround_ex_mode, dolby_headphone_encoded, ad_converter_type
|
||||||
def ext_bsi2_word1(b):
|
def ext_bsi2_word1(b):
|
||||||
return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \
|
return (
|
||||||
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \
|
DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5),
|
||||||
DolbyDigitalPlusMetadata.ADConverterType(b & 0x4 >> 2)
|
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3),
|
||||||
|
DolbyDigitalPlusMetadata.ADConverterType(b & 0x4 >> 2),
|
||||||
|
)
|
||||||
|
|
||||||
def ddplus_reserved2(_):
|
def ddplus_reserved2(_):
|
||||||
pass
|
pass
|
||||||
@@ -398,7 +415,7 @@ class DolbyDigitalPlusMetadata:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def ddplus_info1(b):
|
def ddplus_info1(b):
|
||||||
return DolbyDigitalPlusMetadata.StreamDependency(b & 0xc >> 2)
|
return DolbyDigitalPlusMetadata.StreamDependency(b & 0xC >> 2)
|
||||||
|
|
||||||
def ddplus_reserved4(_):
|
def ddplus_reserved4(_):
|
||||||
pass
|
pass
|
||||||
@@ -412,19 +429,24 @@ class DolbyDigitalPlusMetadata:
|
|||||||
pid = program_id(buffer[0])
|
pid = program_id(buffer[0])
|
||||||
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
|
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
|
||||||
ddplus_reserved1(buffer[2:2])
|
ddplus_reserved1(buffer[2:2])
|
||||||
center_downmix_level, surround_downmix_level, \
|
center_downmix_level, surround_downmix_level, dolby_surround_encoded = (
|
||||||
dolby_surround_encoded = surround_config(buffer[4])
|
surround_config(buffer[4])
|
||||||
langcode_present, copyright_bitstream, original_bitstream, \
|
)
|
||||||
dialnorm = dialnorm_info(buffer[5])
|
langcode_present, copyright_bitstream, original_bitstream, dialnorm = (
|
||||||
|
dialnorm_info(buffer[5])
|
||||||
|
)
|
||||||
langcode = langcod(buffer[6])
|
langcode = langcod(buffer[6])
|
||||||
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
|
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
|
||||||
|
|
||||||
loro_center_downmix_level, \
|
loro_center_downmix_level, loro_surround_downmix_level = ext_bsi1_word1(
|
||||||
loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
|
buffer[8]
|
||||||
downmix_mode, ltrt_center_downmix_level, \
|
)
|
||||||
ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9])
|
downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level = (
|
||||||
surround_ex_mode, dolby_headphone_encoded, \
|
ext_bsi1_word2(buffer[9])
|
||||||
ad_converter_type = ext_bsi2_word1(buffer[10])
|
)
|
||||||
|
surround_ex_mode, dolby_headphone_encoded, ad_converter_type = ext_bsi2_word1(
|
||||||
|
buffer[10]
|
||||||
|
)
|
||||||
|
|
||||||
ddplus_reserved2(buffer[11:14])
|
ddplus_reserved2(buffer[11:14])
|
||||||
compression = compr1(buffer[14])
|
compression = compr1(buffer[14])
|
||||||
@@ -436,32 +458,34 @@ class DolbyDigitalPlusMetadata:
|
|||||||
reserved(buffer[27:69])
|
reserved(buffer[27:69])
|
||||||
|
|
||||||
return DolbyDigitalPlusMetadata(
|
return DolbyDigitalPlusMetadata(
|
||||||
program_id=pid, lfe_on=lfe_on,
|
program_id=pid,
|
||||||
bitstream_mode=bitstream_mode,
|
lfe_on=lfe_on,
|
||||||
audio_coding_mode=audio_coding_mode,
|
bitstream_mode=bitstream_mode,
|
||||||
center_downmix_level=center_downmix_level,
|
audio_coding_mode=audio_coding_mode,
|
||||||
surround_downmix_level=surround_downmix_level,
|
center_downmix_level=center_downmix_level,
|
||||||
dolby_surround_encoded=dolby_surround_encoded,
|
surround_downmix_level=surround_downmix_level,
|
||||||
langcode_present=langcode_present,
|
dolby_surround_encoded=dolby_surround_encoded,
|
||||||
copyright_bitstream=copyright_bitstream,
|
langcode_present=langcode_present,
|
||||||
original_bitstream=original_bitstream,
|
copyright_bitstream=copyright_bitstream,
|
||||||
dialnorm=dialnorm,
|
original_bitstream=original_bitstream,
|
||||||
langcode=langcode,
|
dialnorm=dialnorm,
|
||||||
prod_info_exists=prod_info_exists,
|
langcode=langcode,
|
||||||
mixlevel=mixlevel,
|
prod_info_exists=prod_info_exists,
|
||||||
roomtype=roomtype,
|
mixlevel=mixlevel,
|
||||||
loro_center_downmix_level=loro_center_downmix_level,
|
roomtype=roomtype,
|
||||||
loro_surround_downmix_level=loro_surround_downmix_level,
|
loro_center_downmix_level=loro_center_downmix_level,
|
||||||
downmix_mode=downmix_mode,
|
loro_surround_downmix_level=loro_surround_downmix_level,
|
||||||
ltrt_center_downmix_level=ltrt_center_downmix_level,
|
downmix_mode=downmix_mode,
|
||||||
ltrt_surround_downmix_level=ltrt_surround_downmix_level,
|
ltrt_center_downmix_level=ltrt_center_downmix_level,
|
||||||
surround_ex_mode=surround_ex_mode,
|
ltrt_surround_downmix_level=ltrt_surround_downmix_level,
|
||||||
dolby_headphone_encoded=dolby_headphone_encoded,
|
surround_ex_mode=surround_ex_mode,
|
||||||
ad_converter_type=ad_converter_type,
|
dolby_headphone_encoded=dolby_headphone_encoded,
|
||||||
compression_profile=compression,
|
ad_converter_type=ad_converter_type,
|
||||||
dynamic_range=dynamic_range,
|
compression_profile=compression,
|
||||||
stream_dependency=stream_info,
|
dynamic_range=dynamic_range,
|
||||||
datarate_kbps=data_rate)
|
stream_dependency=stream_info,
|
||||||
|
datarate_kbps=data_rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -488,7 +512,6 @@ class DolbyAtmosMetadata:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, data: bytes):
|
def load(cls, data: bytes):
|
||||||
|
|
||||||
assert len(data) == cls.SEGMENT_LENGTH
|
assert len(data) == cls.SEGMENT_LENGTH
|
||||||
# (f"DolbyAtmosMetadata segment is incorrect length, "
|
# (f"DolbyAtmosMetadata segment is incorrect length, "
|
||||||
# f"expected {cls.SEGMENT_LENGTH} actual was {len(data)}")
|
# f"expected {cls.SEGMENT_LENGTH} actual was {len(data)}")
|
||||||
@@ -498,7 +521,7 @@ class DolbyAtmosMetadata:
|
|||||||
h.seek(32, 1)
|
h.seek(32, 1)
|
||||||
toolname = h.read(cls.TOOL_NAME_LENGTH)
|
toolname = h.read(cls.TOOL_NAME_LENGTH)
|
||||||
toolname = unpack("%is" % cls.TOOL_NAME_LENGTH, toolname)[0]
|
toolname = unpack("%is" % cls.TOOL_NAME_LENGTH, toolname)[0]
|
||||||
toolname = toolname.decode('utf-8').strip('\0')
|
toolname = toolname.decode("utf-8").strip("\0")
|
||||||
|
|
||||||
vers = h.read(3)
|
vers = h.read(3)
|
||||||
major, minor, fix = unpack("BBB", vers)
|
major, minor, fix = unpack("BBB", vers)
|
||||||
@@ -508,10 +531,11 @@ class DolbyAtmosMetadata:
|
|||||||
a_val = unpack("B", h.read(1))[0]
|
a_val = unpack("B", h.read(1))[0]
|
||||||
warp_mode = a_val & 0x7
|
warp_mode = a_val & 0x7
|
||||||
|
|
||||||
return DolbyAtmosMetadata(tool_name=toolname,
|
return DolbyAtmosMetadata(
|
||||||
tool_version=(major, minor, fix),
|
tool_name=toolname,
|
||||||
warp_mode=DolbyAtmosMetadata
|
tool_version=(major, minor, fix),
|
||||||
.WarpMode(warp_mode))
|
warp_mode=DolbyAtmosMetadata.WarpMode(warp_mode),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -531,15 +555,14 @@ class DolbyAtmosSupplementalMetadata:
|
|||||||
NOT_INDICATED = 0x04
|
NOT_INDICATED = 0x04
|
||||||
|
|
||||||
object_count: int
|
object_count: int
|
||||||
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
|
render_modes: List["DolbyAtmosSupplementalMetadata.BinauralRenderMode"]
|
||||||
trim_modes: List[int]
|
trim_modes: List[int]
|
||||||
|
|
||||||
MAGIC = 0xf8726fbd
|
MAGIC = 0xF8726FBD
|
||||||
TRIM_CONFIG_COUNT = 9
|
TRIM_CONFIG_COUNT = 9
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, data: bytes):
|
def load(cls, data: bytes):
|
||||||
|
|
||||||
trim_modes = []
|
trim_modes = []
|
||||||
render_modes = []
|
render_modes = []
|
||||||
|
|
||||||
@@ -564,9 +587,9 @@ class DolbyAtmosSupplementalMetadata:
|
|||||||
binaural_mode &= 0x7
|
binaural_mode &= 0x7
|
||||||
render_modes.append(binaural_mode)
|
render_modes.append(binaural_mode)
|
||||||
|
|
||||||
return DolbyAtmosSupplementalMetadata(object_count=object_count,
|
return DolbyAtmosSupplementalMetadata(
|
||||||
render_modes=render_modes,
|
object_count=object_count, render_modes=render_modes, trim_modes=trim_modes
|
||||||
trim_modes=trim_modes)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WavDolbyMetadataReader:
|
class WavDolbyMetadataReader:
|
||||||
@@ -589,9 +612,9 @@ class WavDolbyMetadataReader:
|
|||||||
retval = size
|
retval = size
|
||||||
for b in bs:
|
for b in bs:
|
||||||
retval += int(b)
|
retval += int(b)
|
||||||
retval &= 0xff
|
retval &= 0xFF
|
||||||
|
|
||||||
retval = ((~retval) + 1) & 0xff
|
retval = ((~retval) + 1) & 0xFF
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
@@ -614,8 +637,9 @@ class WavDolbyMetadataReader:
|
|||||||
else:
|
else:
|
||||||
seg_size = unpack("<H", h.read(2))[0]
|
seg_size = unpack("<H", h.read(2))[0]
|
||||||
seg_payload = h.read(seg_size)
|
seg_payload = h.read(seg_size)
|
||||||
expected_checksum = WavDolbyMetadataReader\
|
expected_checksum = WavDolbyMetadataReader.segment_checksum(
|
||||||
.segment_checksum(seg_payload, seg_size)
|
seg_payload, seg_size
|
||||||
|
)
|
||||||
checksum = unpack("B", h.read(1))[0]
|
checksum = unpack("B", h.read(1))[0]
|
||||||
|
|
||||||
segment = seg_payload
|
segment = seg_payload
|
||||||
@@ -626,35 +650,41 @@ class WavDolbyMetadataReader:
|
|||||||
elif stype == SegmentType.DolbyAtmosSupplemental:
|
elif stype == SegmentType.DolbyAtmosSupplemental:
|
||||||
segment = DolbyAtmosSupplementalMetadata.load(segment)
|
segment = DolbyAtmosSupplementalMetadata.load(segment)
|
||||||
|
|
||||||
self.segment_list\
|
self.segment_list.append(
|
||||||
.append((stype, checksum == expected_checksum, segment))
|
(stype, checksum == expected_checksum, segment)
|
||||||
|
)
|
||||||
|
|
||||||
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
|
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
|
||||||
"""
|
"""
|
||||||
Every valid Dolby Digital Plus metadata segment in the file.
|
Every valid Dolby Digital Plus metadata segment in the file.
|
||||||
"""
|
"""
|
||||||
return [x[2] for x in self.segment_list
|
return [
|
||||||
if x[0] == SegmentType.DolbyDigitalPlus and x[1]]
|
x[2]
|
||||||
|
for x in self.segment_list
|
||||||
|
if x[0] == SegmentType.DolbyDigitalPlus and x[1]
|
||||||
|
]
|
||||||
|
|
||||||
def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
|
def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
|
||||||
"""
|
"""
|
||||||
Every valid Dolby Atmos metadata segment in the file.
|
Every valid Dolby Atmos metadata segment in the file.
|
||||||
"""
|
"""
|
||||||
return [x[2] for x in self.segment_list
|
return [
|
||||||
if x[0] == SegmentType.DolbyAtmos and x[1]]
|
x[2] for x in self.segment_list if x[0] == SegmentType.DolbyAtmos and x[1]
|
||||||
|
]
|
||||||
|
|
||||||
def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
||||||
"""
|
"""
|
||||||
Every valid Dolby Atmos Supplemental metadata segment in the file.
|
Every valid Dolby Atmos Supplemental metadata segment in the file.
|
||||||
"""
|
"""
|
||||||
return [x[2] for x in self.segment_list
|
return [
|
||||||
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
x[2]
|
||||||
|
for x in self.segment_list
|
||||||
|
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]
|
||||||
|
]
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
|
|
||||||
ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
|
ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
|
||||||
atmos = map(lambda x: asdict(x), self.dolby_atmos())
|
atmos = map(lambda x: asdict(x), self.dolby_atmos())
|
||||||
# atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
|
# atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
|
||||||
|
|
||||||
return dict(dolby_digital_plus=list(ddp),
|
return dict(dolby_digital_plus=list(ddp), dolby_atmos=list(atmos))
|
||||||
dolby_atmos=list(atmos))
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from .riff_parser import parse_chunk, ListChunkDescriptor
|
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from .riff_parser import ListChunkDescriptor, parse_chunk
|
||||||
|
|
||||||
|
|
||||||
class WavInfoChunkReader:
|
class WavInfoChunkReader:
|
||||||
|
|
||||||
def __init__(self, f, encoding):
|
def __init__(self, f, encoding):
|
||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
|
|
||||||
@@ -12,56 +11,64 @@ class WavInfoChunkReader:
|
|||||||
parsed_chunks = parse_chunk(f)
|
parsed_chunks = parse_chunk(f)
|
||||||
assert type(parsed_chunks) is ListChunkDescriptor
|
assert type(parsed_chunks) is ListChunkDescriptor
|
||||||
|
|
||||||
list_chunks = [chunk for chunk in parsed_chunks.children
|
list_chunks = [
|
||||||
if type(chunk) is ListChunkDescriptor]
|
chunk
|
||||||
|
for chunk in parsed_chunks.children
|
||||||
|
if type(chunk) is ListChunkDescriptor
|
||||||
|
]
|
||||||
|
|
||||||
self.info_chunk = next((chunk for chunk in list_chunks
|
self.info_chunk = next(
|
||||||
if chunk.signature == b'INFO'), None)
|
(chunk for chunk in list_chunks if chunk.signature == b"INFO"), None
|
||||||
|
)
|
||||||
|
|
||||||
#: 'ICOP' Copyright
|
#: 'ICOP' Copyright
|
||||||
self.copyright: Optional[str] = self._get_field(f, b'ICOP')
|
self.copyright: Optional[str] = self._get_field(f, b"ICOP")
|
||||||
#: 'IPRD' Product
|
#: 'IPRD' Product
|
||||||
self.product: Optional[str] = self._get_field(f, b'IPRD')
|
self.product: Optional[str] = self._get_field(f, b"IPRD")
|
||||||
self.album: Optional[str] = self.product
|
self.album: Optional[str] = self.product
|
||||||
#: 'IGNR' Genre
|
#: 'IGNR' Genre
|
||||||
self.genre: Optional[str] = self._get_field(f, b'IGNR')
|
self.genre: Optional[str] = self._get_field(f, b"IGNR")
|
||||||
#: 'ISBJ' Subject
|
#: 'ISBJ' Subject
|
||||||
self.subject: Optional[str] = self._get_field(f, b'ISBJ')
|
self.subject: Optional[str] = self._get_field(f, b"ISBJ")
|
||||||
#: 'IART' Artist, composer, author
|
#: 'IART' Artist, composer, author
|
||||||
self.artist: Optional[str] = self._get_field(f, b'IART')
|
self.artist: Optional[str] = self._get_field(f, b"IART")
|
||||||
#: 'ICMT' Comment
|
#: 'ICMT' Comment
|
||||||
self.comment: Optional[str] = self._get_field(f, b'ICMT')
|
self.comment: Optional[str] = self._get_field(f, b"ICMT")
|
||||||
#: 'ISFT' Software, encoding application
|
#: 'ISFT' Software, encoding application
|
||||||
self.software: Optional[str] = self._get_field(f, b'ISFT')
|
self.software: Optional[str] = self._get_field(f, b"ISFT")
|
||||||
#: 'ICRD' Created date
|
#: 'ICRD' Created date
|
||||||
self.created_date: Optional[str] = self._get_field(f, b'ICRD')
|
self.created_date: Optional[str] = self._get_field(f, b"ICRD")
|
||||||
#: 'IENG' Engineer
|
#: 'IENG' Engineer
|
||||||
self.engineer: Optional[str] = self._get_field(f, b'IENG')
|
self.engineer: Optional[str] = self._get_field(f, b"IENG")
|
||||||
#: 'ITCH' Technician
|
#: 'ITCH' Technician
|
||||||
self.technician: Optional[str] = self._get_field(f, b'ITCH')
|
self.technician: Optional[str] = self._get_field(f, b"ITCH")
|
||||||
#: 'IKEY' Keywords, keyword list
|
#: 'IKEY' Keywords, keyword list
|
||||||
self.keywords: Optional[str] = self._get_field(f, b'IKEY')
|
self.keywords: Optional[str] = self._get_field(f, b"IKEY")
|
||||||
#: 'INAM' Name, title
|
#: 'INAM' Name, title
|
||||||
self.title: Optional[str] = self._get_field(f, b'INAM')
|
self.title: Optional[str] = self._get_field(f, b"INAM")
|
||||||
#: 'ISRC' Source
|
#: 'ISRC' Source
|
||||||
self.source: Optional[str] = self._get_field(f, b'ISRC')
|
self.source: Optional[str] = self._get_field(f, b"ISRC")
|
||||||
#: 'TAPE' Tape
|
#: 'TAPE' Tape
|
||||||
self.tape: Optional[str] = self._get_field(f, b'TAPE')
|
self.tape: Optional[str] = self._get_field(f, b"TAPE")
|
||||||
#: 'IARL' Archival Location
|
#: 'IARL' Archival Location
|
||||||
self.archival_location: Optional[str] = self._get_field(f, b'IARL')
|
self.archival_location: Optional[str] = self._get_field(f, b"IARL")
|
||||||
#: 'ICSM' Commissioned
|
#: 'ICSM' Commissioned
|
||||||
self.commissioned: Optional[str] = self._get_field(f, b'ICMS')
|
self.commissioned: Optional[str] = self._get_field(f, b"ICMS")
|
||||||
|
|
||||||
def _get_field(self, f, field_ident) -> Optional[str]:
|
def _get_field(self, f, field_ident) -> Optional[str]:
|
||||||
search = next(((chunk.start, chunk.length)
|
search = next(
|
||||||
for chunk in self.info_chunk.children
|
(
|
||||||
if chunk.ident == field_ident),
|
(chunk.start, chunk.length)
|
||||||
None)
|
for chunk in self.info_chunk.children
|
||||||
|
if chunk.ident == field_ident
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
if search is not None:
|
if search is not None:
|
||||||
f.seek(search[0])
|
f.seek(search[0])
|
||||||
data = f.read(search[1])
|
data = f.read(search[1])
|
||||||
return data.decode(self.encoding).rstrip('\0')
|
return data.decode(self.encoding).rstrip("\0")
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -69,26 +76,27 @@ class WavInfoChunkReader:
|
|||||||
"""
|
"""
|
||||||
A dictionary with all of the key/values read from the INFO scope.
|
A dictionary with all of the key/values read from the INFO scope.
|
||||||
"""
|
"""
|
||||||
return {'copyright': self.copyright,
|
return {
|
||||||
'product': self.product,
|
"copyright": self.copyright,
|
||||||
'album': self.album,
|
"product": self.product,
|
||||||
'genre': self.genre,
|
"album": self.album,
|
||||||
'artist': self.artist,
|
"genre": self.genre,
|
||||||
'comment': self.comment,
|
"artist": self.artist,
|
||||||
'software': self.software,
|
"comment": self.comment,
|
||||||
'created_date': self.created_date,
|
"software": self.software,
|
||||||
'engineer': self.engineer,
|
"created_date": self.created_date,
|
||||||
'keywords': self.keywords,
|
"engineer": self.engineer,
|
||||||
'title': self.title,
|
"keywords": self.keywords,
|
||||||
'source': self.source,
|
"title": self.title,
|
||||||
'tape': self.tape,
|
"source": self.source,
|
||||||
'commissioned': self.commissioned,
|
"tape": self.tape,
|
||||||
'archival_location': self.archival_location,
|
"commissioned": self.commissioned,
|
||||||
'subject': self.subject,
|
"archival_location": self.archival_location,
|
||||||
'technician': self.technician
|
"subject": self.subject,
|
||||||
}
|
"technician": self.technician,
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return_val = self.to_dict()
|
return_val = self.to_dict()
|
||||||
return_val.update({'encoding': self.encoding})
|
return_val.update({"encoding": self.encoding})
|
||||||
return str(return_val)
|
return str(return_val)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from lxml import etree as ET
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
# from collections import namedtuple
|
|
||||||
from typing import Optional
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
|
# from collections import namedtuple
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from lxml import etree as ET
|
||||||
|
|
||||||
|
|
||||||
class IXMLTrack(NamedTuple):
|
class IXMLTrack(NamedTuple):
|
||||||
channel_index: int
|
channel_index: int
|
||||||
@@ -22,6 +25,7 @@ class SteinbergMetadata:
|
|||||||
"""
|
"""
|
||||||
Steinberg speaker format enumeration.
|
Steinberg speaker format enumeration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MONO = 0
|
MONO = 0
|
||||||
STEREO = 1
|
STEREO = 1
|
||||||
LRC = 10
|
LRC = 10
|
||||||
@@ -74,82 +78,75 @@ class SteinbergMetadata:
|
|||||||
self.parsed = xml.find(self.Steinberg_xpath)
|
self.parsed = xml.find(self.Steinberg_xpath)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio_speaker_arrangement(self) -> Optional[AudioSpeakerArrangement]:
|
def audio_speaker_arrangement(self) -> AudioSpeakerArrangement | None:
|
||||||
"""
|
"""
|
||||||
`AudioSpeakerArrangement` property
|
`AudioSpeakerArrangement` property
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find(
|
val = self.parsed.find(
|
||||||
"./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE")
|
"./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE"
|
||||||
|
)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return type(self).AudioSpeakerArrangement(int(val.text))
|
return type(self).AudioSpeakerArrangement(int(val.text))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sample_format_size(self) -> Optional[int]:
|
def sample_format_size(self) -> int | None:
|
||||||
"""
|
"""
|
||||||
AudioSampleFormatSize
|
AudioSampleFormatSize
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find(
|
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
|
||||||
"./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
|
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return int(val.text)
|
return int(val.text)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_company(self) -> Optional[str]:
|
def media_company(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
MediaCompany
|
MediaCompany
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find(
|
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
|
||||||
"./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
|
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return val.text
|
return val.text
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_drop_frames(self) -> Optional[bool]:
|
def media_drop_frames(self) -> bool | None:
|
||||||
"""
|
"""
|
||||||
MediaDropFrames
|
MediaDropFrames
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find(
|
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
|
||||||
"./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
|
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return val.text == "1"
|
return val.text == "1"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_duration(self) -> Optional[float]:
|
def media_duration(self) -> float | None:
|
||||||
"""
|
"""
|
||||||
MediaDuration
|
MediaDuration
|
||||||
"""
|
"""
|
||||||
val = self.parsed.find(
|
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
|
||||||
"./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
|
|
||||||
if val is not None:
|
if val is not None:
|
||||||
return float(val.text)
|
return float(val.text)
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def media_start_time(self) -> Optional[float]:
|
# def media_start_time(self) -> float | None:
|
||||||
"""
|
# """
|
||||||
MediaStartTime
|
# MediaStartTime
|
||||||
"""
|
# """
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def media_track_title(self) -> Optional[str]:
|
# def media_track_title(self) -> str | None:
|
||||||
"""
|
# """
|
||||||
MediaTrackTitle
|
# MediaTrackTitle
|
||||||
"""
|
# """
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def program_name(self) -> Optional[str]:
|
# def program_name(self) -> str | None:
|
||||||
"""
|
# """
|
||||||
ProgramName
|
# ProgramName
|
||||||
"""
|
# """
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def program_version(self) -> Optional[str]:
|
# def program_version(self) -> str | None:
|
||||||
"""
|
# """
|
||||||
ProgramVersion
|
# ProgramVersion
|
||||||
"""
|
# """
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WavIXMLFormat:
|
class WavIXMLFormat:
|
||||||
@@ -167,7 +164,7 @@ class WavIXMLFormat:
|
|||||||
parser = ET.XMLParser(recover=True)
|
parser = ET.XMLParser(recover=True)
|
||||||
self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser)
|
self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser)
|
||||||
|
|
||||||
def _get_text_value(self, xpath) -> Optional[str]:
|
def _get_text_value(self, xpath) -> str | None:
|
||||||
e = self.parsed.find("./" + xpath)
|
e = self.parsed.find("./" + xpath)
|
||||||
if e is not None:
|
if e is not None:
|
||||||
return e.text
|
return e.text
|
||||||
@@ -192,45 +189,44 @@ class WavIXMLFormat:
|
|||||||
:yields: `IXMLTrack` for each track.
|
:yields: `IXMLTrack` for each track.
|
||||||
"""
|
"""
|
||||||
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":
|
||||||
yield IXMLTrack(
|
yield IXMLTrack(
|
||||||
channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
|
channel_index=track.xpath("string(CHANNEL_INDEX/text())"),
|
||||||
interleave_index=track.xpath(
|
interleave_index=track.xpath("string(INTERLEAVE_INDEX/text())"),
|
||||||
'string(INTERLEAVE_INDEX/text())'),
|
name=track.xpath("string(NAME/text())"),
|
||||||
name=track.xpath('string(NAME/text())'),
|
function=track.xpath("string(FUNCTION/text())"),
|
||||||
function=track.xpath('string(FUNCTION/text())')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project(self) -> Optional[str]:
|
def project(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
The project/film name entered for the recording.
|
The project/film name entered for the recording.
|
||||||
"""
|
"""
|
||||||
return self._get_text_value("PROJECT")
|
return self._get_text_value("PROJECT")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scene(self) -> Optional[str]:
|
def scene(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
Scene/slate.
|
Scene/slate.
|
||||||
"""
|
"""
|
||||||
return self._get_text_value("SCENE")
|
return self._get_text_value("SCENE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def take(self) -> Optional[str]:
|
def take(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
Take number.
|
Take number.
|
||||||
"""
|
"""
|
||||||
return self._get_text_value("TAKE")
|
return self._get_text_value("TAKE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tape(self) -> Optional[str]:
|
def tape(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
Tape name.
|
Tape name.
|
||||||
"""
|
"""
|
||||||
return self._get_text_value("TAPE")
|
return self._get_text_value("TAPE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family_uid(self) -> Optional[str]:
|
def family_uid(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
The globally-unique ID for this file family. This may be in the format
|
The globally-unique ID for this file family. This may be in the format
|
||||||
of a GUID, or an EBU Rec 9 source identifier, or some other dumb
|
of a GUID, or an EBU Rec 9 source identifier, or some other dumb
|
||||||
@@ -239,14 +235,14 @@ class WavIXMLFormat:
|
|||||||
return self._get_text_value("FILE_SET/FAMILY_UID")
|
return self._get_text_value("FILE_SET/FAMILY_UID")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family_name(self) -> Optional[str]:
|
def family_name(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
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")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def steinberg(self) -> Optional[SteinbergMetadata]:
|
def steinberg(self) -> SteinbergMetadata | None:
|
||||||
"""
|
"""
|
||||||
Steinberg vendor iXML metadata if present.
|
Steinberg vendor iXML metadata if present.
|
||||||
"""
|
"""
|
||||||
@@ -256,8 +252,12 @@ class WavIXMLFormat:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return dict(
|
return {
|
||||||
track_list=list(map(lambda x: x._asdict(), self.track_list)),
|
"track_list": [x._asdict() for x in self.track_list],
|
||||||
project=self.project, scene=self.scene, take=self.take,
|
"project": self.project,
|
||||||
tape=self.tape, family_uid=self.family_uid,
|
"scene": self.scene,
|
||||||
family_name=self.family_name)
|
"take": self.take,
|
||||||
|
"tape": self.tape,
|
||||||
|
"family_uid": self.family_uid,
|
||||||
|
"family_name": self.family_name,
|
||||||
|
}
|
||||||
|
|||||||
+104
-83
@@ -1,18 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
from __future__ import annotations
|
||||||
import struct
|
|
||||||
import os
|
import os
|
||||||
from typing import Optional, Generator, Any, NamedTuple
|
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import struct
|
||||||
|
from typing import Any, Generator, NamedTuple
|
||||||
|
|
||||||
|
from .riff_parser import ChunkDescriptor, ListChunkDescriptor, parse_chunk
|
||||||
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
|
|
||||||
from .wave_adm_reader import WavADMReader
|
from .wave_adm_reader import WavADMReader
|
||||||
from .wave_dbmd_reader import WavDolbyMetadataReader
|
from .wave_bext_reader import WavBextReader
|
||||||
from .wave_cues_reader import WavCuesReader
|
from .wave_cues_reader import WavCuesReader
|
||||||
|
from .wave_dbmd_reader import WavDolbyMetadataReader
|
||||||
|
from .wave_info_reader import WavInfoChunkReader
|
||||||
|
from .wave_ixml_reader import WavIXMLFormat
|
||||||
from .wave_smpl_reader import WavSmplReader
|
from .wave_smpl_reader import WavSmplReader
|
||||||
|
|
||||||
#: Calculated statistics about the audio data.
|
#: Calculated statistics about the audio data.
|
||||||
@@ -38,7 +37,7 @@ 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"):
|
||||||
"""
|
"""
|
||||||
Create a new reader object.
|
Create a new reader object.
|
||||||
|
|
||||||
@@ -60,35 +59,35 @@ class WavInfoReader:
|
|||||||
self.bext_encoding = bext_encoding
|
self.bext_encoding = bext_encoding
|
||||||
|
|
||||||
#: Wave audio data format.
|
#: Wave audio data format.
|
||||||
self.fmt: Optional[WavAudioFormat] = None
|
self.fmt: WavAudioFormat | None = None
|
||||||
|
|
||||||
#: Statistics of the `data` section.
|
#: Statistics of the `data` section.
|
||||||
self.data: Optional[WavDataDescriptor] = None
|
self.data: WavDataDescriptor | None = None
|
||||||
|
|
||||||
#: Broadcast-Wave metadata.
|
#: Broadcast-Wave metadata.
|
||||||
self.bext: Optional[WavBextReader] = None
|
self.bext: WavBextReader | None = None
|
||||||
|
|
||||||
#: iXML metadata.
|
#: iXML metadata.
|
||||||
self.ixml: Optional[WavIXMLFormat] = None
|
self.ixml: WavIXMLFormat | None = None
|
||||||
|
|
||||||
#: ADM Audio Definiton Model metadata.
|
#: ADM Audio Definiton Model metadata.
|
||||||
self.adm: Optional[WavADMReader] = None
|
self.adm: WavADMReader | None = None
|
||||||
|
|
||||||
#: Dolby bitstream metadata.
|
#: Dolby bitstream metadata.
|
||||||
self.dolby: Optional[WavDolbyMetadataReader] = None
|
self.dolby: WavDolbyMetadataReader | None = None
|
||||||
|
|
||||||
#: RIFF INFO metadata.
|
#: RIFF INFO metadata.
|
||||||
self.info: Optional[WavInfoChunkReader] = None
|
self.info: WavInfoChunkReader | None = None
|
||||||
|
|
||||||
#: RIFF cues markers, labels, and notes.
|
#: RIFF cues markers, labels, and notes.
|
||||||
self.cues: Optional[WavCuesReader] = None
|
self.cues: WavCuesReader | None = None
|
||||||
|
|
||||||
#: Sampler `smpl` metadata
|
#: Sampler `smpl` metadata
|
||||||
self.smpl: Optional[WavSmplReader] = None
|
self.smpl: WavSmplReader | None = None
|
||||||
|
|
||||||
if hasattr(path, 'read'):
|
if hasattr(path, "read"):
|
||||||
self.get_wav_info(path)
|
self.get_wav_info(path)
|
||||||
self.url = 'about:blank'
|
self.url = "about:blank"
|
||||||
self.path = repr(path)
|
self.path = repr(path)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -99,8 +98,8 @@ class WavInfoReader:
|
|||||||
|
|
||||||
self.path = absolute_path
|
self.path = absolute_path
|
||||||
|
|
||||||
with open(path, 'rb') as path:
|
with open(path, "rb") as file:
|
||||||
self.get_wav_info(path)
|
self.get_wav_info(file)
|
||||||
|
|
||||||
def get_wav_info(self, wavfile):
|
def get_wav_info(self, wavfile):
|
||||||
chunks = parse_chunk(wavfile)
|
chunks = parse_chunk(wavfile)
|
||||||
@@ -119,36 +118,41 @@ class WavInfoReader:
|
|||||||
self.smpl = self._get_sampler_loops(wavfile)
|
self.smpl = self._get_sampler_loops(wavfile)
|
||||||
self.data = self._describe_data()
|
self.data = self._describe_data()
|
||||||
|
|
||||||
def _find_chunk_data(self, ident, from_stream,
|
def _find_chunk_data(self, ident, from_stream, default_none=False) -> bytes | None:
|
||||||
default_none=False) -> Optional[bytes]:
|
top_chunks = (
|
||||||
top_chunks = (chunk for chunk in self.main_list
|
chunk
|
||||||
if type(chunk) is ChunkDescriptor and
|
for chunk in self.main_list
|
||||||
chunk.ident == ident)
|
if type(chunk) is ChunkDescriptor and chunk.ident == ident
|
||||||
|
)
|
||||||
|
|
||||||
chunk_descriptor = next(top_chunks, None) \
|
chunk_descriptor = next(top_chunks, None) if default_none else next(top_chunks)
|
||||||
if default_none else next(top_chunks)
|
|
||||||
|
|
||||||
return chunk_descriptor.read_data(from_stream) \
|
return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None
|
||||||
if chunk_descriptor else None
|
|
||||||
|
|
||||||
def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]:
|
def _find_list_chunk(self, signature) -> ListChunkDescriptor | None:
|
||||||
top_chunks = (chunk for chunk in self.main_list
|
top_chunks = (
|
||||||
if type(chunk) is ListChunkDescriptor and
|
chunk
|
||||||
chunk.signature == signature)
|
for chunk in self.main_list
|
||||||
|
if type(chunk) is ListChunkDescriptor and chunk.signature == signature
|
||||||
|
)
|
||||||
|
|
||||||
return next(top_chunks, None)
|
return next(top_chunks, None)
|
||||||
|
|
||||||
def _describe_data(self):
|
def _describe_data(self):
|
||||||
data_chunk = next(c for c in self.main_list
|
data_chunk = next(
|
||||||
if type(c) is ChunkDescriptor and c.ident == b'data')
|
c
|
||||||
|
for c in self.main_list
|
||||||
|
if type(c) is ChunkDescriptor and c.ident == b"data"
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(self.fmt, WavAudioFormat)
|
assert isinstance(self.fmt, WavAudioFormat)
|
||||||
return WavDataDescriptor(
|
return WavDataDescriptor(
|
||||||
byte_count=data_chunk.length,
|
byte_count=data_chunk.length,
|
||||||
frame_count=int(data_chunk.length / self.fmt.block_align))
|
frame_count=int(data_chunk.length / self.fmt.block_align),
|
||||||
|
)
|
||||||
|
|
||||||
def _get_format(self, f):
|
def _get_format(self, f):
|
||||||
fmt_data = self._find_chunk_data(b'fmt ', f)
|
fmt_data = self._find_chunk_data(b"fmt ", f)
|
||||||
assert fmt_data is not None, "Fmt data not found, not a valid wav file"
|
assert fmt_data is not None, "Fmt data not found, not a valid wav file"
|
||||||
|
|
||||||
packstring = "<HHIIHH"
|
packstring = "<HHIIHH"
|
||||||
@@ -156,62 +160,79 @@ class WavInfoReader:
|
|||||||
|
|
||||||
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
|
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
|
||||||
|
|
||||||
return WavAudioFormat(audio_format=unpacked[0],
|
return WavAudioFormat(
|
||||||
channel_count=unpacked[1],
|
audio_format=unpacked[0],
|
||||||
sample_rate=unpacked[2],
|
channel_count=unpacked[1],
|
||||||
byte_rate=unpacked[3],
|
sample_rate=unpacked[2],
|
||||||
block_align=unpacked[4],
|
byte_rate=unpacked[3],
|
||||||
bits_per_sample=unpacked[5]
|
block_align=unpacked[4],
|
||||||
)
|
bits_per_sample=unpacked[5],
|
||||||
|
)
|
||||||
|
|
||||||
def _get_info(self, f, encoding):
|
def _get_info(self, f, encoding):
|
||||||
finder = (chunk.signature for chunk in self.main_list
|
finder = (
|
||||||
if type(chunk) is ListChunkDescriptor)
|
chunk.signature
|
||||||
|
for chunk in self.main_list
|
||||||
|
if type(chunk) is ListChunkDescriptor
|
||||||
|
)
|
||||||
|
|
||||||
if b'INFO' in finder:
|
if b"INFO" in finder:
|
||||||
return WavInfoChunkReader(f, encoding)
|
return WavInfoChunkReader(f, encoding)
|
||||||
|
|
||||||
def _get_bext(self, f, encoding):
|
def _get_bext(self, f, encoding):
|
||||||
bext_data = self._find_chunk_data(b'bext', f, default_none=True)
|
bext_data = self._find_chunk_data(b"bext", f, default_none=True)
|
||||||
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):
|
def _get_adm(self, f):
|
||||||
axml = self._find_chunk_data(b'axml', f, default_none=True)
|
axml = self._find_chunk_data(b"axml", f, default_none=True)
|
||||||
chna = self._find_chunk_data(b'chna', f, default_none=True)
|
chna = self._find_chunk_data(b"chna", f, default_none=True)
|
||||||
return WavADMReader(axml_data=axml, chna_data=chna) \
|
return WavADMReader(axml_data=axml, chna_data=chna) if axml and chna else None
|
||||||
if axml and chna else None
|
|
||||||
|
|
||||||
def _get_dbmd(self, f):
|
def _get_dbmd(self, f):
|
||||||
dbmd_data = self._find_chunk_data(b'dbmd', f, default_none=True)
|
dbmd_data = self._find_chunk_data(b"dbmd", f, default_none=True)
|
||||||
return WavDolbyMetadataReader(dbmd_data=dbmd_data) \
|
return WavDolbyMetadataReader(dbmd_data=dbmd_data) if dbmd_data else None
|
||||||
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 WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
|
return WavIXMLFormat(ixml_data.rstrip(b"\0")) if ixml_data else None
|
||||||
|
|
||||||
def _get_cue(self, f):
|
def _get_cue(self, f):
|
||||||
cue = next((cue_chunk for cue_chunk in self.main_list if
|
cue = next(
|
||||||
type(cue_chunk) is ChunkDescriptor and
|
(
|
||||||
cue_chunk.ident == b'cue '), None)
|
cue_chunk
|
||||||
|
for cue_chunk in self.main_list
|
||||||
|
if type(cue_chunk) is ChunkDescriptor and cue_chunk.ident == b"cue "
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
adtl = self._find_list_chunk(b'adtl')
|
adtl = self._find_list_chunk(b"adtl")
|
||||||
labls = []
|
labls = []
|
||||||
ltxts = []
|
ltxts = []
|
||||||
notes = []
|
notes = []
|
||||||
if adtl is not None:
|
if adtl is not None:
|
||||||
labls = [c for c in adtl.children
|
labls = [
|
||||||
if type(c) is ChunkDescriptor and c.ident == b'labl']
|
c
|
||||||
ltxts = [c for c in adtl.children
|
for c in adtl.children
|
||||||
if type(c) is ChunkDescriptor and c.ident == b'ltxt']
|
if type(c) is ChunkDescriptor and c.ident == b"labl"
|
||||||
notes = [c for c in adtl.children
|
]
|
||||||
if type(c) is ChunkDescriptor and c.ident == b'note']
|
ltxts = [
|
||||||
|
c
|
||||||
|
for c in adtl.children
|
||||||
|
if type(c) is ChunkDescriptor and c.ident == b"ltxt"
|
||||||
|
]
|
||||||
|
notes = [
|
||||||
|
c
|
||||||
|
for c in adtl.children
|
||||||
|
if type(c) is ChunkDescriptor and c.ident == b"note"
|
||||||
|
]
|
||||||
|
|
||||||
return WavCuesReader.read_all(f, cue, labls, ltxts, notes,
|
return WavCuesReader.read_all(
|
||||||
fallback_encoding=self.info_encoding)
|
f, cue, labls, ltxts, notes, fallback_encoding=self.info_encoding
|
||||||
|
)
|
||||||
|
|
||||||
def _get_sampler_loops(self, f):
|
def _get_sampler_loops(self, f):
|
||||||
sampler_data = self._find_chunk_data(b'smpl', f, default_none=True)
|
sampler_data = self._find_chunk_data(b"smpl", f, default_none=True)
|
||||||
return WavSmplReader(sampler_data) if sampler_data else None
|
return WavSmplReader(sampler_data) if sampler_data else None
|
||||||
|
|
||||||
# FIXME: this should probably be named "iter()"
|
# FIXME: this should probably be named "iter()"
|
||||||
@@ -225,22 +246,22 @@ class WavInfoReader:
|
|||||||
"smpl".
|
"smpl".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues',
|
scopes = ("fmt", "data", "ixml", "bext", "info", "adm", "cues", "dolby", "smpl")
|
||||||
'dolby', 'smpl')
|
|
||||||
|
|
||||||
for scope in scopes:
|
for scope in scopes:
|
||||||
if scope in ['fmt', 'data']:
|
if scope in ["fmt", "data"]:
|
||||||
attr = self.__getattribute__(scope)
|
attr = self.__getattribute__(scope)
|
||||||
for field in attr._fields:
|
for field in attr._fields:
|
||||||
yield scope, field, attr.__getattribute__(field)
|
yield scope, field, attr.__getattribute__(field)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
mdict = self.__getattribute__(scope).to_dict(
|
mdict = (
|
||||||
) if self.__getattribute__(scope) else {}
|
self.__getattribute__(scope).to_dict()
|
||||||
for key in mdict.keys():
|
if self.__getattribute__(scope)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
for key in mdict:
|
||||||
yield scope, key, mdict[key]
|
yield scope, key, mdict[key]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'WavInfoReader({}, {}, {})'.format(self.path,
|
return f"WavInfoReader({self.path}, {self.info_encoding}, {self.bext_encoding})"
|
||||||
self.info_encoding,
|
|
||||||
self.bext_encoding)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import struct
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Tuple, NamedTuple, List
|
import struct
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
class WaveSmplLoop(NamedTuple):
|
class WaveSmplLoop(NamedTuple):
|
||||||
@@ -13,30 +14,29 @@ class WaveSmplLoop(NamedTuple):
|
|||||||
|
|
||||||
def loop_type_desc(self):
|
def loop_type_desc(self):
|
||||||
if self.loop_type == 0:
|
if self.loop_type == 0:
|
||||||
return 'FORWARD'
|
return "FORWARD"
|
||||||
elif self.loop_type == 1:
|
elif self.loop_type == 1:
|
||||||
return 'FORWARD_BACKWARD'
|
return "FORWARD_BACKWARD"
|
||||||
elif self.loop_type == 2:
|
elif self.loop_type == 2:
|
||||||
return 'BACKWARD'
|
return "BACKWARD"
|
||||||
elif 3 <= self.loop_type <= 31:
|
elif 3 <= self.loop_type <= 31:
|
||||||
return 'RESERVED'
|
return "RESERVED"
|
||||||
else:
|
else:
|
||||||
return 'VENDOR'
|
return "VENDOR"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'ident': self.ident,
|
"ident": self.ident,
|
||||||
'loop_type': self.loop_type,
|
"loop_type": self.loop_type,
|
||||||
'loop_type_description': self.loop_type_desc(),
|
"loop_type_description": self.loop_type_desc(),
|
||||||
'start_samples': self.start,
|
"start_samples": self.start,
|
||||||
'end_samples': self.end,
|
"end_samples": self.end,
|
||||||
'detune_cents': self.detune_cents,
|
"detune_cents": self.detune_cents,
|
||||||
'repetition_count': self.repetition_count,
|
"repetition_count": self.repetition_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WavSmplReader:
|
class WavSmplReader:
|
||||||
|
|
||||||
def __init__(self, smpl_data: bytes):
|
def __init__(self, smpl_data: bytes):
|
||||||
"""
|
"""
|
||||||
Read sampler metadata from smpl chunk.
|
Read sampler metadata from smpl chunk.
|
||||||
@@ -47,8 +47,7 @@ class WavSmplReader:
|
|||||||
header_size = struct.calcsize(header_field_fmt)
|
header_size = struct.calcsize(header_field_fmt)
|
||||||
loop_size = struct.calcsize(loop_field_fmt)
|
loop_size = struct.calcsize(loop_field_fmt)
|
||||||
|
|
||||||
unpacked_data = struct.unpack(header_field_fmt,
|
unpacked_data = struct.unpack(header_field_fmt, smpl_data[0:header_size])
|
||||||
smpl_data[0:header_size])
|
|
||||||
|
|
||||||
#: The MIDI Manufacturer's Association code for the sampler
|
#: The MIDI Manufacturer's Association code for the sampler
|
||||||
#: manufactuer, or 0 if not specific.
|
#: manufactuer, or 0 if not specific.
|
||||||
@@ -72,43 +71,47 @@ class WavSmplReader:
|
|||||||
|
|
||||||
#: The SMPTE offset to apply, as a tuple of four ints representing
|
#: The SMPTE offset to apply, as a tuple of four ints representing
|
||||||
#: hh, mm, ss, ff
|
#: hh, mm, ss, ff
|
||||||
self.smpte_offset: Tuple[int, int, int, int] = unpacked_data[6:10]
|
self.smpte_offset: tuple[int, int, int, int] = unpacked_data[6:10]
|
||||||
|
|
||||||
loop_count = unpacked_data[10]
|
loop_count = unpacked_data[10]
|
||||||
sampler_udata_length = unpacked_data[11]
|
sampler_udata_length = unpacked_data[11]
|
||||||
|
|
||||||
#: List of loops in the file.
|
#: List of loops in the file.
|
||||||
self.sample_loops: List[WaveSmplLoop] = []
|
self.sample_loops: list[WaveSmplLoop] = []
|
||||||
|
|
||||||
loop_buffer = smpl_data[header_size:
|
loop_buffer = smpl_data[header_size : header_size + loop_size * loop_count]
|
||||||
header_size + loop_size * loop_count]
|
|
||||||
|
|
||||||
for unpacked_loop in struct.iter_unpack(loop_field_fmt, loop_buffer):
|
for unpacked_loop in struct.iter_unpack(loop_field_fmt, loop_buffer):
|
||||||
self.sample_loops.append(WaveSmplLoop(
|
self.sample_loops.append(
|
||||||
ident=unpacked_loop[0],
|
WaveSmplLoop(
|
||||||
loop_type=unpacked_loop[1],
|
ident=unpacked_loop[0],
|
||||||
start=unpacked_loop[2],
|
loop_type=unpacked_loop[1],
|
||||||
end=unpacked_loop[3],
|
start=unpacked_loop[2],
|
||||||
detune_cents=unpacked_loop[4],
|
end=unpacked_loop[3],
|
||||||
repetition_count=unpacked_loop[5]))
|
detune_cents=unpacked_loop[4],
|
||||||
|
repetition_count=unpacked_loop[5],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
#: Sampler-specific user data.
|
#: Sampler-specific user data.
|
||||||
self.sampler_udata: bytes | None = None
|
self.sampler_udata: bytes | None = None
|
||||||
|
|
||||||
if sampler_udata_length > 0:
|
if sampler_udata_length > 0:
|
||||||
self.sampler_udata = smpl_data[
|
self.sampler_udata = smpl_data[
|
||||||
header_size + loop_size * loop_count:
|
header_size + loop_size * loop_count : header_size
|
||||||
header_size + loop_size * loop_count + sampler_udata_length]
|
+ loop_size * loop_count
|
||||||
|
+ sampler_udata_length
|
||||||
|
]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'manufactuer': self.manufacturer,
|
"manufactuer": self.manufacturer,
|
||||||
'product': self.product,
|
"product": self.product,
|
||||||
'sample_period_ns': self.sample_period_ns,
|
"sample_period_ns": self.sample_period_ns,
|
||||||
'midi_note': self.midi_note,
|
"midi_note": self.midi_note,
|
||||||
'midi_pitch_detune_cents': self.midi_pitch_detune_cents,
|
"midi_pitch_detune_cents": self.midi_pitch_detune_cents,
|
||||||
'smpte_format': self.smpte_format,
|
"smpte_format": self.smpte_format,
|
||||||
'smpte_offset': "%02i:%02i:%02i:%02i" % self.smpte_offset,
|
"smpte_offset": "%02i:%02i:%02i:%02i" % self.smpte_offset,
|
||||||
'loops': [x.to_dict() for x in self.sample_loops],
|
"loops": [x.to_dict() for x in self.sample_loops],
|
||||||
'sampler_user_data': self.sampler_udata,
|
"sampler_user_data": self.sampler_udata,
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-17
@@ -1,34 +1,30 @@
|
|||||||
"""
|
"""
|
||||||
|
wavfind.py
|
||||||
|
|
||||||
|
Under construction! Do Not Use!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from optparse import OptionParser, OptionGroup
|
|
||||||
import sys
|
import sys
|
||||||
|
from optparse import OptionGroup, OptionParser
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
|
||||||
parser.usage = ("wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] "
|
parser.usage = "wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] <PATH> +"
|
||||||
"<PATH> +")
|
|
||||||
|
|
||||||
primaries = OptionGroup(parser, title="Search Predicates",
|
primaries = OptionGroup(
|
||||||
description="Argument values can be globs, "
|
parser,
|
||||||
"and are logically-AND'ed.")
|
title="Search Predicates",
|
||||||
|
description="Argument values can be globs, and are logically-AND'ed.",
|
||||||
|
)
|
||||||
|
|
||||||
primaries.add_option("--scene",
|
primaries.add_option("--scene", help="Search for this scene", metavar="SCENE")
|
||||||
help='Search for this scene',
|
|
||||||
metavar='SCENE')
|
|
||||||
|
|
||||||
primaries.add_option("--take",
|
primaries.add_option("--take", help="Search for this take", metavar="TAKE")
|
||||||
help='Search for this take',
|
|
||||||
metavar='TAKE')
|
|
||||||
|
|
||||||
primaries.add_option("--desc",
|
primaries.add_option("--desc", help="Search descriptions", metavar="DESC")
|
||||||
help='Search descriptions',
|
|
||||||
metavar='DESC')
|
|
||||||
|
|
||||||
(options, args) = parser.parse_args(sys.argv)
|
(_options, _args) = parser.parse_args(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
from . import test_wave_parsing
|
|
||||||
|
|
||||||
from io import BytesIO
|
|
||||||
from typing import Generator
|
|
||||||
import zipfile as zf
|
|
||||||
import os.path
|
|
||||||
import os
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
# class TestFileLoader:
|
|
||||||
# """
|
|
||||||
# This guy manages the test_files archive.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# def __init__(self, test_file_arch_name = 'archive.zip') -> None:
|
|
||||||
# self.base_path = os.path.join(os.path.dirname(__file__), "test_files")
|
|
||||||
# self.test_file_arch_name = test_file_arch_name
|
|
||||||
# self._gather_test_files_into_archive()
|
|
||||||
|
|
||||||
# @property
|
|
||||||
# def arch_path(self):
|
|
||||||
# return os.path.join(self.base_path, self.test_file_arch_name)
|
|
||||||
|
|
||||||
# @contextmanager
|
|
||||||
# def open(self, name) -> Generator[BytesIO]:
|
|
||||||
# z = zf.ZipFile(self.arch_path, 'r')
|
|
||||||
# member = z.open(name, 'r')
|
|
||||||
# try:
|
|
||||||
# yield member
|
|
||||||
# finally:
|
|
||||||
# zf.close()
|
|
||||||
|
|
||||||
# def _gather_test_files_into_archive(self):
|
|
||||||
# with zf.ZipFile(self.arch_path, 'a') as zip:
|
|
||||||
# for root, _, files in os.walk(self.base_path):
|
|
||||||
# for name in files:
|
|
||||||
# if root == self.base_path and name == self.test_file_arch_name:
|
|
||||||
# continue
|
|
||||||
# else:
|
|
||||||
# p = os.path.join(root, name)
|
|
||||||
# zip.write(p)
|
|
||||||
# os.unlink(p)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+9
-8
@@ -2,8 +2,8 @@ from unittest import TestCase
|
|||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
class TestADMWave(TestCase):
|
|
||||||
|
|
||||||
|
class TestADMWave(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.protools_adm_wav = "tests/test_files/protools/Test_ADM_ProTools.wav"
|
self.protools_adm_wav = "tests/test_files/protools/Test_ADM_ProTools.wav"
|
||||||
return super().setUp()
|
return super().setUp()
|
||||||
@@ -24,7 +24,7 @@ class TestADMWave(TestCase):
|
|||||||
assert adm is not None
|
assert adm is not None
|
||||||
dict = adm.to_dict()
|
dict = adm.to_dict()
|
||||||
self.assertIsNotNone(dict)
|
self.assertIsNotNone(dict)
|
||||||
|
|
||||||
def test_programme(self):
|
def test_programme(self):
|
||||||
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||||
adm = info.adm
|
adm = info.adm
|
||||||
@@ -32,8 +32,8 @@ class TestADMWave(TestCase):
|
|||||||
pdict = adm.programme()
|
pdict = adm.programme()
|
||||||
self.assertIn("programme_id", pdict.keys())
|
self.assertIn("programme_id", pdict.keys())
|
||||||
self.assertIn("programme_name", pdict.keys())
|
self.assertIn("programme_name", pdict.keys())
|
||||||
self.assertEqual(pdict['programme_id'], 'APR_1001')
|
self.assertEqual(pdict["programme_id"], "APR_1001")
|
||||||
self.assertEqual(pdict['programme_name'], 'Atmos_Master')
|
self.assertEqual(pdict["programme_name"], "Atmos_Master")
|
||||||
self.assertIn("contents", pdict.keys())
|
self.assertIn("contents", pdict.keys())
|
||||||
self.assertEqual(len(pdict["contents"]), 3)
|
self.assertEqual(len(pdict["contents"]), 3)
|
||||||
|
|
||||||
@@ -42,13 +42,14 @@ class TestADMWave(TestCase):
|
|||||||
adm = info.adm
|
adm = info.adm
|
||||||
assert adm is not None
|
assert adm is not None
|
||||||
t1 = adm.track_info(0)
|
t1 = adm.track_info(0)
|
||||||
self.assertTrue("channel_format_name" in t1.keys())
|
assert t1 is not None
|
||||||
|
self.assertTrue("channel_format_name" in t1)
|
||||||
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
|
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
|
||||||
|
|
||||||
self.assertTrue("pack_format_name" in t1.keys())
|
self.assertTrue("pack_format_name" in t1)
|
||||||
self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"])
|
self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"])
|
||||||
|
|
||||||
t10 = adm.track_info(10)
|
t10 = adm.track_info(10)
|
||||||
self.assertTrue("content_name" in t10.keys())
|
assert t10 is not None
|
||||||
|
self.assertTrue("content_name" in t10)
|
||||||
self.assertEqual("Dialog", t10["content_name"])
|
self.assertEqual("Dialog", t10["content_name"])
|
||||||
|
|
||||||
|
|||||||
+15
-16
@@ -1,11 +1,12 @@
|
|||||||
from unittest import TestCase
|
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
|
|
||||||
class TestCue(TestCase):
|
class TestCue(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.test_files = glob("tests/test_files/cue_chunks/*.wav")
|
self.test_files = glob("tests/test_files/cue_chunks/*.wav")
|
||||||
return super().setUp()
|
return super().setUp()
|
||||||
|
|
||||||
def test_enumerate(self):
|
def test_enumerate(self):
|
||||||
@@ -14,7 +15,7 @@ class TestCue(TestCase):
|
|||||||
self.assertIsNotNone(w1.cues)
|
self.assertIsNotNone(w1.cues)
|
||||||
assert w1.cues is not None
|
assert w1.cues is not None
|
||||||
vals = list(w1.cues.each_cue())
|
vals = list(w1.cues.each_cue())
|
||||||
self.assertEqual(vals, [(1,29616),(2,74592),(3,121200)])
|
self.assertEqual(vals, [(1, 29616), (2, 74592), (3, 121200)])
|
||||||
|
|
||||||
def test_labels_notes(self):
|
def test_labels_notes(self):
|
||||||
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
|
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
|
||||||
@@ -23,7 +24,7 @@ class TestCue(TestCase):
|
|||||||
assert w1.cues is not None
|
assert w1.cues is not None
|
||||||
|
|
||||||
for name, _ in w1.cues.each_cue():
|
for name, _ in w1.cues.each_cue():
|
||||||
self.assertIn(name,[1,2,3])
|
self.assertIn(name, [1, 2, 3])
|
||||||
label, note = w1.cues.label_and_note(name)
|
label, note = w1.cues.label_and_note(name)
|
||||||
if name == 1:
|
if name == 1:
|
||||||
self.assertEqual("Marker 1", label)
|
self.assertEqual("Marker 1", label)
|
||||||
@@ -35,18 +36,20 @@ class TestCue(TestCase):
|
|||||||
self.assertIsNotNone(w1.cues)
|
self.assertIsNotNone(w1.cues)
|
||||||
assert w1.cues is not None
|
assert w1.cues is not None
|
||||||
|
|
||||||
self.assertEqual(w1.cues.range(3), 10000)
|
self.assertEqual(w1.cues.range(3), 10000)
|
||||||
|
|
||||||
def test_encoding_fallback(self):
|
def test_encoding_fallback(self):
|
||||||
"""
|
"""
|
||||||
Added this after I noticed that iZotope RX seems to just encode "notes"
|
Added this after I noticed that iZotope RX seems to just encode "notes"
|
||||||
as utf-8 without bothering to dump this info into the ltxt or
|
as utf-8 without bothering to dump this info into the ltxt or
|
||||||
specifying an encoding by some other means.
|
specifying an encoding by some other means.
|
||||||
"""
|
"""
|
||||||
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
|
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
|
||||||
w = wavinfo.WavInfoReader(file, info_encoding='utf-8')
|
w = wavinfo.WavInfoReader(file, info_encoding="utf-8")
|
||||||
expected = ("Лорем ипсум долор сит амет, тимеам вивендум хас ет, "
|
expected = (
|
||||||
"цу адолесценс дефинитионес еам.")
|
"Лорем ипсум долор сит амет, тимеам вивендум хас ет, "
|
||||||
|
"цу адолесценс дефинитионес еам."
|
||||||
|
)
|
||||||
|
|
||||||
assert w.cues is not None
|
assert w.cues is not None
|
||||||
note = [n for n in w.cues.notes if n.name == 3]
|
note = [n for n in w.cues.notes if n.name == 3]
|
||||||
@@ -56,20 +59,16 @@ class TestCue(TestCase):
|
|||||||
def test_label(self):
|
def test_label(self):
|
||||||
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
|
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
|
||||||
w = wavinfo.WavInfoReader(file)
|
w = wavinfo.WavInfoReader(file)
|
||||||
|
|
||||||
self.assertIsNotNone(w.cues)
|
self.assertIsNotNone(w.cues)
|
||||||
assert w.cues is not None
|
assert w.cues is not None
|
||||||
|
|
||||||
self.assertEqual(len(w.cues.labels), 3)
|
self.assertEqual(len(w.cues.labels), 3)
|
||||||
for label in w.cues.labels:
|
for label in w.cues.labels:
|
||||||
self.assertIn(label.name, [1,2,3])
|
self.assertIn(label.name, [1, 2, 3])
|
||||||
if label.name == 1:
|
if label.name == 1:
|
||||||
self.assertEqual(label.text, "Marker 1")
|
self.assertEqual(label.text, "Marker 1")
|
||||||
elif label.name == 2:
|
elif label.name == 2:
|
||||||
self.assertEqual(label.text, "Marker 2")
|
self.assertEqual(label.text, "Marker 2")
|
||||||
elif label.name == 3:
|
elif label.name == 3:
|
||||||
self.assertEqual(label.text, "Marker 3")
|
self.assertEqual(label.text, "Marker 3")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-19
@@ -1,7 +1,8 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
from wavinfo.wave_dbmd_reader import SegmentType, DolbyDigitalPlusMetadata
|
from wavinfo.wave_dbmd_reader import DolbyDigitalPlusMetadata, SegmentType
|
||||||
|
|
||||||
|
|
||||||
class TestDolby(TestCase):
|
class TestDolby(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -10,19 +11,17 @@ class TestDolby(TestCase):
|
|||||||
def test_version(self):
|
def test_version(self):
|
||||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
d = t1.dolby
|
d = t1.dolby
|
||||||
|
|
||||||
assert d is not None
|
assert d is not None
|
||||||
self.assertEqual((1,0,0,6), d.version)
|
self.assertEqual((1, 0, 0, 6), d.version)
|
||||||
|
|
||||||
def test_segments(self):
|
def test_segments(self):
|
||||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
d = t1.dolby
|
d = t1.dolby
|
||||||
assert d is not None
|
assert d is not None
|
||||||
|
|
||||||
ddp = [x for x in d.segment_list \
|
ddp = [x for x in d.segment_list if x[0] == SegmentType.DolbyDigitalPlus]
|
||||||
if x[0] == SegmentType.DolbyDigitalPlus]
|
atmos = [x for x in d.segment_list if x[0] == SegmentType.DolbyAtmos]
|
||||||
atmos = [x for x in d.segment_list \
|
|
||||||
if x[0] == SegmentType.DolbyAtmos]
|
|
||||||
|
|
||||||
self.assertEqual(len(ddp), 1)
|
self.assertEqual(len(ddp), 1)
|
||||||
self.assertEqual(len(atmos), 1)
|
self.assertEqual(len(atmos), 1)
|
||||||
@@ -40,21 +39,23 @@ class TestDolby(TestCase):
|
|||||||
d = t1.dolby
|
d = t1.dolby
|
||||||
assert d is not None
|
assert d is not None
|
||||||
ddp = d.dolby_digital_plus()
|
ddp = d.dolby_digital_plus()
|
||||||
self.assertEqual(len(ddp), 1,
|
self.assertEqual(
|
||||||
("Failed to find exactly one Dolby Digital Plus "
|
len(ddp),
|
||||||
"metadata segment")
|
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)
|
||||||
|
|
||||||
self.assertTrue( ddp[0].audio_coding_mode,
|
|
||||||
DolbyDigitalPlusMetadata.AudioCodingMode.CH_ORD_3_2 )
|
|
||||||
self.assertTrue( ddp[0].lfe_on)
|
|
||||||
|
|
||||||
def test_atmos(self):
|
def test_atmos(self):
|
||||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||||
d = t1.dolby
|
d = t1.dolby
|
||||||
assert d is not None
|
assert d is not None
|
||||||
atmos = d.dolby_atmos()
|
atmos = d.dolby_atmos()
|
||||||
self.assertEqual(len(atmos), 1,
|
self.assertEqual(
|
||||||
"Failed to find exactly one Atmos metadata segment")
|
len(atmos), 1, "Failed to find exactly one Atmos metadata segment"
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+11
-22
@@ -1,34 +1,23 @@
|
|||||||
|
import glob
|
||||||
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from wavinfo.__main__ import main
|
from wavinfo.__main__ import main
|
||||||
|
|
||||||
import sys
|
|
||||||
import glob
|
|
||||||
|
|
||||||
class MainTest(unittest.TestCase):
|
class MainTest(unittest.TestCase):
|
||||||
|
def test_empty_argv(self):
|
||||||
def test_empty_argv(self):
|
with patch.object(sys, "argv", []):
|
||||||
with patch.object(sys, 'argv', []):
|
main()
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except:
|
|
||||||
self.fail("main() throwing an exception")
|
|
||||||
|
|
||||||
def test_a_file(self):
|
def test_a_file(self):
|
||||||
for path in glob.glob("tests/test_files/**/*.wav"):
|
for path in glob.glob("tests/test_files/**/*.wav"):
|
||||||
with patch.object(sys, 'argv', ["TEST", path]):
|
with patch.object(sys, "argv", ["TEST", path]):
|
||||||
try:
|
main()
|
||||||
main()
|
|
||||||
except:
|
|
||||||
self.fail("main() throwing an exception")
|
|
||||||
|
|
||||||
def test_ixml(self):
|
def test_ixml(self):
|
||||||
with patch.object(sys, 'argv',
|
with patch.object(
|
||||||
['TEST', '--ixml',
|
sys, "argv", ["TEST", "--ixml", "tests/test_files/sounddevices/A101_1.WAV"]
|
||||||
'tests/test_files/sounddevices/A101_1.WAV']):
|
):
|
||||||
try:
|
main()
|
||||||
main()
|
|
||||||
except:
|
|
||||||
self.fail("main() throwing an exception")
|
|
||||||
|
|||||||
+2
-5
@@ -1,25 +1,22 @@
|
|||||||
# import os.path
|
# import os.path
|
||||||
import gzip
|
import gzip
|
||||||
from glob import glob
|
from glob import glob
|
||||||
# from typing import Dict, Any, cast
|
|
||||||
|
|
||||||
|
# from typing import Dict, Any, cast
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
# from .utils import all_files, ffprobe
|
# from .utils import all_files, ffprobe
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
class TestRf64(TestCase):
|
|
||||||
|
|
||||||
|
class TestRf64(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
return super().setUp()
|
return super().setUp()
|
||||||
|
|
||||||
def test_open(self):
|
def test_open(self):
|
||||||
|
|
||||||
for path in glob("tests/test_files/rf64/*.wav.gz"):
|
for path in glob("tests/test_files/rf64/*.wav.gz"):
|
||||||
gz = gzip.open(path)
|
gz = gzip.open(path)
|
||||||
wav_info = wavinfo.WavInfoReader(gz)
|
wav_info = wavinfo.WavInfoReader(gz)
|
||||||
|
|
||||||
self.assertIsNotNone(wav_info)
|
self.assertIsNotNone(wav_info)
|
||||||
# self.assertIsNotNone(wav_info.bext)
|
# self.assertIsNotNone(wav_info.bext)
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
from unittest import TestCase
|
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
|
|
||||||
class TestSmpl(TestCase):
|
class TestSmpl(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.test_files = glob("tests/test_files/smpl/*.wav")
|
self.test_files = glob("tests/test_files/smpl/*.wav")
|
||||||
|
|||||||
+14
-16
@@ -1,37 +1,35 @@
|
|||||||
|
import glob
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
import glob
|
|
||||||
|
|
||||||
class TestWalk(unittest.TestCase):
|
class TestWalk(unittest.TestCase):
|
||||||
def test_walk_metadata(self):
|
def test_walk_metadata(self):
|
||||||
test_file = 'tests/test_files/protools/PT A101_4.A1.wav'
|
test_file = "tests/test_files/protools/PT A101_4.A1.wav"
|
||||||
info = wavinfo.WavInfoReader(test_file)
|
info = wavinfo.WavInfoReader(test_file)
|
||||||
|
|
||||||
tested_data , tested_format = False, False
|
tested_data, tested_format = False, False
|
||||||
for scope, key, value in info.walk():
|
for scope, key, value in info.walk():
|
||||||
if scope == 'fmt':
|
if scope == "fmt" and key == "channel_count":
|
||||||
if key == 'channel_count':
|
|
||||||
tested_format = True
|
tested_format = True
|
||||||
self.assertEqual(value, 2)
|
self.assertEqual(value, 2)
|
||||||
if scope == 'data':
|
if scope == "data" and key == "frame_count":
|
||||||
if key == 'frame_count':
|
|
||||||
tested_data = True
|
tested_data = True
|
||||||
self.assertEqual(value, 144140)
|
self.assertEqual(value, 144140)
|
||||||
|
|
||||||
self.assertTrue(tested_data and tested_format)
|
self.assertTrue(tested_data and tested_format)
|
||||||
|
|
||||||
def test_walk_all(self):
|
def test_walk_all(self):
|
||||||
for file in glob.glob('tests/test_files/**/*.wav'):
|
for file in glob.glob("tests/test_files/**/*.wav"):
|
||||||
info = wavinfo.WavInfoReader(file)
|
info = wavinfo.WavInfoReader(file)
|
||||||
|
|
||||||
try:
|
|
||||||
for _, _, _ in info.walk():
|
|
||||||
pass
|
|
||||||
except:
|
|
||||||
self.fail(f"Failed to walk metadata in file {file}")
|
|
||||||
|
|
||||||
|
# try:
|
||||||
|
for _, _, _ in info.walk():
|
||||||
|
pass
|
||||||
|
# except Exception as e:
|
||||||
|
# self.fail(f"Failed to walk metadata in file {file}: {e}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
+116
-96
@@ -1,21 +1,21 @@
|
|||||||
import os.path
|
import os.path
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from typing import Dict, Any, cast
|
from typing import Any, Dict, cast
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from .utils import all_files, ffprobe
|
|
||||||
|
|
||||||
import wavinfo
|
import wavinfo
|
||||||
|
|
||||||
|
from .utils import all_files, ffprobe
|
||||||
|
|
||||||
|
|
||||||
class TestWaveInfo(TestCase):
|
class TestWaveInfo(TestCase):
|
||||||
def test_sanity(self):
|
def test_sanity(self):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
self.assertEqual(info.__repr__(),
|
self.assertEqual(
|
||||||
'WavInfoReader({}, latin_1, ascii)'
|
info.__repr__(),
|
||||||
.format(os.path.abspath(wav_file)))
|
f"WavInfoReader({os.path.abspath(wav_file)}, latin_1, ascii)",
|
||||||
|
)
|
||||||
self.assertIsNotNone(info)
|
self.assertIsNotNone(info)
|
||||||
|
|
||||||
def test_fmt_against_ffprobe(self):
|
def test_fmt_against_ffprobe(self):
|
||||||
@@ -26,31 +26,39 @@ class TestWaveInfo(TestCase):
|
|||||||
assert info.fmt is not None
|
assert info.fmt is not None
|
||||||
assert ffprobe_info is not None
|
assert ffprobe_info is not None
|
||||||
|
|
||||||
self.assertEqual(info.fmt.channel_count,
|
self.assertEqual(
|
||||||
ffprobe_info['streams'][0]['channels'])
|
info.fmt.channel_count, ffprobe_info["streams"][0]["channels"]
|
||||||
self.assertEqual(info.fmt.sample_rate,
|
)
|
||||||
int(ffprobe_info['streams'][0]['sample_rate']))
|
self.assertEqual(
|
||||||
self.assertEqual(info.fmt.bits_per_sample,
|
info.fmt.sample_rate, int(ffprobe_info["streams"][0]["sample_rate"])
|
||||||
int(ffprobe_info['streams'][0]['bits_per_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']\
|
self.assertTrue(
|
||||||
.startswith('pcm'))
|
ffprobe_info["streams"][0]["codec_name"].startswith("pcm")
|
||||||
streams = ffprobe_info['streams'][0]
|
)
|
||||||
byte_rate = int(streams['sample_rate']) * \
|
streams = ffprobe_info["streams"][0]
|
||||||
streams['channels'] * \
|
byte_rate = (
|
||||||
int(streams['bits_per_sample']) / 8
|
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):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
ffprobe_info = cast(Dict[str,Any], ffprobe(wav_file))
|
ffprobe_info = cast(Dict[str, Any], ffprobe(wav_file))
|
||||||
assert ffprobe_info is not None
|
assert ffprobe_info is not None
|
||||||
assert info.data is not None
|
assert info.data is not None
|
||||||
self.assertEqual(info.data.frame_count,
|
self.assertEqual(
|
||||||
int(ffprobe_info['streams'][0]['duration_ts']))
|
info.data.frame_count, int(ffprobe_info["streams"][0]["duration_ts"])
|
||||||
|
)
|
||||||
|
|
||||||
def test_bext_against_ffprobe(self):
|
def test_bext_against_ffprobe(self):
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
@@ -59,65 +67,78 @@ class TestWaveInfo(TestCase):
|
|||||||
assert ffprobe_info is not None
|
assert ffprobe_info is not None
|
||||||
|
|
||||||
if info.bext:
|
if info.bext:
|
||||||
if 'comment' in ffprobe_info['format']['tags']:
|
if "comment" in ffprobe_info["format"]["tags"]:
|
||||||
self.assertEqual(info.bext.description,
|
self.assertEqual(
|
||||||
ffprobe_info['format']['tags']\
|
info.bext.description, ffprobe_info["format"]["tags"]["comment"]
|
||||||
['comment'])
|
)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.description, '')
|
self.assertEqual(info.bext.description, "")
|
||||||
|
|
||||||
if 'encoded_by' in ffprobe_info['format']['tags']:
|
if "encoded_by" in ffprobe_info["format"]["tags"]:
|
||||||
self.assertEqual(info.bext.originator,
|
self.assertEqual(
|
||||||
ffprobe_info['format']['tags']\
|
info.bext.originator,
|
||||||
['encoded_by'])
|
ffprobe_info["format"]["tags"]["encoded_by"],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.originator, '')
|
self.assertEqual(info.bext.originator, "")
|
||||||
|
|
||||||
if 'originator_reference' in ffprobe_info['format']['tags']:
|
if "originator_reference" in ffprobe_info["format"]["tags"]:
|
||||||
self.assertEqual(info.bext.originator_ref,
|
self.assertEqual(
|
||||||
ffprobe_info['format']['tags']\
|
info.bext.originator_ref,
|
||||||
['originator_reference'])
|
ffprobe_info["format"]["tags"]["originator_reference"],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.originator_ref, '')
|
self.assertEqual(info.bext.originator_ref, "")
|
||||||
|
|
||||||
# these don't always reflect the bext info
|
# these don't always reflect the bext info
|
||||||
# self.assertEqual(info.bext.originator_date,
|
# self.assertEqual(info.bext.originator_date,
|
||||||
# ffprobe_info['format']['tags']['date'])
|
# ffprobe_info['format']['tags']['date'])
|
||||||
# self.assertEqual(info.bext.originator_time,
|
# self.assertEqual(info.bext.originator_time,
|
||||||
# ffprobe_info['format']['tags']['creation_time'])
|
# ffprobe_info['format']['tags']['creation_time'])
|
||||||
self.assertEqual(info.bext.time_reference,
|
self.assertEqual(
|
||||||
int(ffprobe_info['format']['tags']\
|
info.bext.time_reference,
|
||||||
['time_reference']))
|
int(ffprobe_info["format"]["tags"]["time_reference"]),
|
||||||
|
)
|
||||||
|
|
||||||
if 'coding_history' in ffprobe_info['format']['tags']:
|
if "coding_history" in ffprobe_info["format"]["tags"]:
|
||||||
self.assertEqual(info.bext.coding_history,
|
self.assertEqual(
|
||||||
ffprobe_info['format']['tags']\
|
info.bext.coding_history,
|
||||||
['coding_history'])
|
ffprobe_info["format"]["tags"]["coding_history"],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(info.bext.coding_history, '')
|
self.assertEqual(info.bext.coding_history, "")
|
||||||
|
|
||||||
def test_ixml(self):
|
def test_ixml(self):
|
||||||
expected = {'A101_4.WAV': {'project': 'BMH',
|
expected = {
|
||||||
'scene': 'A101', 'take': '4',
|
"A101_4.WAV": {
|
||||||
'tape': '18Y12M31',
|
"project": "BMH",
|
||||||
'family_uid':
|
"scene": "A101",
|
||||||
'USSDVGR1112089007124015008231000'},
|
"take": "4",
|
||||||
'A101_3.WAV': {'project': 'BMH',
|
"tape": "18Y12M31",
|
||||||
'scene': 'A101', 'take': '3',
|
"family_uid": "USSDVGR1112089007124015008231000",
|
||||||
'tape': '18Y12M31',
|
},
|
||||||
'family_uid':
|
"A101_3.WAV": {
|
||||||
'USSDVGR1112089007124014008228300'},
|
"project": "BMH",
|
||||||
'A101_2.WAV': {'project': 'BMH',
|
"scene": "A101",
|
||||||
'scene': 'A101', 'take': '2',
|
"take": "3",
|
||||||
'tape': '18Y12M31',
|
"tape": "18Y12M31",
|
||||||
'family_uid':
|
"family_uid": "USSDVGR1112089007124014008228300",
|
||||||
'USSDVGR1112089007124004008218600'},
|
},
|
||||||
'A101_1.WAV': {'project': 'BMH',
|
"A101_2.WAV": {
|
||||||
'scene': 'A101', 'take': '1',
|
"project": "BMH",
|
||||||
'tape': '18Y12M31',
|
"scene": "A101",
|
||||||
'family_uid':
|
"take": "2",
|
||||||
'USSDVGR1112089007124001008206300'},
|
"tape": "18Y12M31",
|
||||||
}
|
"family_uid": "USSDVGR1112089007124004008218600",
|
||||||
|
},
|
||||||
|
"A101_1.WAV": {
|
||||||
|
"project": "BMH",
|
||||||
|
"scene": "A101",
|
||||||
|
"take": "1",
|
||||||
|
"tape": "18Y12M31",
|
||||||
|
"family_uid": "USSDVGR1112089007124001008206300",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
for wav_file in all_files():
|
for wav_file in all_files():
|
||||||
basename = os.path.basename(wav_file)
|
basename = os.path.basename(wav_file)
|
||||||
@@ -125,29 +146,31 @@ class TestWaveInfo(TestCase):
|
|||||||
info = wavinfo.WavInfoReader(wav_file)
|
info = wavinfo.WavInfoReader(wav_file)
|
||||||
e = expected[basename]
|
e = expected[basename]
|
||||||
self.assertIsNotNone(info.ixml)
|
self.assertIsNotNone(info.ixml)
|
||||||
assert info.ixml is not None
|
assert info.ixml is not None
|
||||||
self.assertEqual(e['project'], info.ixml.project)
|
self.assertEqual(e["project"], info.ixml.project)
|
||||||
self.assertEqual(e['scene'], info.ixml.scene)
|
self.assertEqual(e["scene"], info.ixml.scene)
|
||||||
self.assertEqual(e['take'], info.ixml.take)
|
self.assertEqual(e["take"], info.ixml.take)
|
||||||
self.assertEqual(e['tape'], info.ixml.tape)
|
self.assertEqual(e["tape"], info.ixml.tape)
|
||||||
self.assertEqual(e['family_uid'], info.ixml.family_uid)
|
self.assertEqual(e["family_uid"], info.ixml.family_uid)
|
||||||
|
|
||||||
for track in info.ixml.track_list:
|
for track in info.ixml.track_list:
|
||||||
self.assertIsNotNone(track.channel_index)
|
self.assertIsNotNone(track.channel_index)
|
||||||
if basename == 'A101_4.WAV' and track.channel_index == '1':
|
if basename == "A101_4.WAV" and track.channel_index == "1":
|
||||||
self.assertEqual(track.name, 'MKH516 A')
|
self.assertEqual(track.name, "MKH516 A")
|
||||||
|
|
||||||
def test_steinberg_ixml(self):
|
def test_steinberg_ixml(self):
|
||||||
nuendo_files = 'tests/test_files/nuendo/*.wav'
|
nuendo_files = "tests/test_files/nuendo/*.wav"
|
||||||
for file in glob(nuendo_files):
|
for file in glob(nuendo_files):
|
||||||
info = wavinfo.WavInfoReader(file)
|
info = wavinfo.WavInfoReader(file)
|
||||||
assert info.ixml is not None
|
assert info.ixml is not None
|
||||||
self.assertIsNotNone(info.ixml.steinberg)
|
self.assertIsNotNone(info.ixml.steinberg)
|
||||||
assert info.ixml.steinberg is not None
|
assert info.ixml.steinberg is not None
|
||||||
self.assertIsNotNone(info.ixml.steinberg.audio_speaker_arrangement)
|
self.assertIsNotNone(info.ixml.steinberg.audio_speaker_arrangement)
|
||||||
self.assertEqual(info.ixml.steinberg.sample_format_size, 3)
|
self.assertEqual(info.ixml.steinberg.sample_format_size, 3)
|
||||||
self.assertEqual(info.ixml.steinberg.media_company,
|
self.assertEqual(
|
||||||
"https://github.com/iluvcapra/wavinfo")
|
info.ixml.steinberg.media_company,
|
||||||
|
"https://github.com/iluvcapra/wavinfo",
|
||||||
|
)
|
||||||
self.assertFalse(info.ixml.steinberg.media_drop_frames)
|
self.assertFalse(info.ixml.steinberg.media_drop_frames)
|
||||||
self.assertEqual(info.ixml.steinberg.media_duration, 1200.0)
|
self.assertEqual(info.ixml.steinberg.media_duration, 1200.0)
|
||||||
|
|
||||||
@@ -159,24 +182,21 @@ class TestWaveInfo(TestCase):
|
|||||||
self.assertIsNone(info.ixml.steinberg)
|
self.assertIsNone(info.ixml.steinberg)
|
||||||
|
|
||||||
def test_info_metadata(self):
|
def test_info_metadata(self):
|
||||||
file_with_metadata = \
|
file_with_metadata = "tests/test_files/sound_grinder_pro/new_camera bumb 1.wav"
|
||||||
'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
|
||||||
self.assertTrue(os.path.exists(file_with_metadata))
|
self.assertTrue(os.path.exists(file_with_metadata))
|
||||||
info = wavinfo.WavInfoReader(file_with_metadata).info
|
info = wavinfo.WavInfoReader(file_with_metadata).info
|
||||||
|
|
||||||
assert info is not None
|
assert info is not None
|
||||||
self.assertEqual(info.title, 'camera bumb 1')
|
self.assertEqual(info.title, "camera bumb 1")
|
||||||
self.assertEqual(info.artist, 'Jamie Hardt')
|
self.assertEqual(info.artist, "Jamie Hardt")
|
||||||
self.assertEqual(info.copyright, '© 2010 Jamie Hardt')
|
self.assertEqual(info.copyright, "© 2010 Jamie Hardt")
|
||||||
self.assertEqual(info.product, 'Test Sounds') # album
|
self.assertEqual(info.product, "Test Sounds") # album
|
||||||
self.assertEqual(info.album, info.product)
|
self.assertEqual(info.album, info.product)
|
||||||
self.assertEqual(info.comment, 'Comments')
|
self.assertEqual(info.comment, "Comments")
|
||||||
self.assertEqual(info.software, 'Sound Grinder Pro')
|
self.assertEqual(info.software, "Sound Grinder Pro")
|
||||||
self.assertEqual(info.created_date, '2010-12-28')
|
self.assertEqual(info.created_date, "2010-12-28")
|
||||||
self.assertEqual(info.engineer, 'JPH')
|
self.assertEqual(info.engineer, "JPH")
|
||||||
self.assertEqual(info.keywords,
|
self.assertEqual(info.keywords, "Sound Effect, movement, microphone, bump")
|
||||||
'Sound Effect, movement, microphone, bump')
|
self.assertEqual(info.title, "camera bumb 1")
|
||||||
self.assertEqual(info.title, 'camera bumb 1')
|
|
||||||
self.assertEqual(type(info.to_dict()), dict)
|
self.assertEqual(type(info.to_dict()), dict)
|
||||||
self.assertEqual(type(info.__repr__()), str)
|
self.assertEqual(type(info.__repr__()), str)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import os.path
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from subprocess import PIPE
|
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
import wavinfo
|
# import wavinfo
|
||||||
|
|
||||||
|
|
||||||
class TestZoomF8(TestCase):
|
class TestZoomF8(TestCase):
|
||||||
|
|||||||
+11
-25
@@ -1,38 +1,24 @@
|
|||||||
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from subprocess import PIPE
|
from subprocess import PIPE
|
||||||
import json
|
|
||||||
|
|
||||||
FFPROBE = 'ffprobe'
|
FFPROBE = "ffprobe"
|
||||||
|
|
||||||
|
|
||||||
def ffprobe(path):
|
def ffprobe(path):
|
||||||
arguments = [FFPROBE, "-of", "json",
|
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
|
||||||
"-show_format", "-show_streams", path]
|
process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE, check=True)
|
||||||
if int(sys.version[0]) < 3:
|
if process.returncode == 0:
|
||||||
process = subprocess.Popen(arguments, stdout=PIPE)
|
output_str = process.stdout.decode("utf-8")
|
||||||
process.wait()
|
return json.loads(output_str)
|
||||||
if process.returncode == 0:
|
else:
|
||||||
output = process.communicate()[0]
|
return None
|
||||||
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():
|
def all_files():
|
||||||
for dirpath, _, filenames in os.walk('tests/test_files'):
|
for dirpath, _, filenames in os.walk("tests/test_files"):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
_, ext = os.path.splitext(filename)
|
_, ext = os.path.splitext(filename)
|
||||||
if ext in ['.wav', '.WAV']:
|
if ext in [".wav", ".WAV"]:
|
||||||
yield os.path.join(dirpath, filename)
|
yield os.path.join(dirpath, filename)
|
||||||
|
|||||||
Reference in New Issue
Block a user