26 Commits

Author SHA1 Message Date
Jamie Hardt
2266cc5032 Update metadata.py
Version 2.0.0
2022-11-23 14:46:19 -08:00
Jamie Hardt
ec5b796181 Fixed readme 2022-11-23 14:44:40 -08:00
Jamie Hardt
97bdb23441 Base documentation for ADM 2022-11-23 14:42:31 -08:00
Jamie Hardt
8f2fd69b00 ADM support 2022-11-23 14:23:42 -08:00
Jamie Hardt
a063fffb41 Fixing walk
And adding to_dict method to
ADM
2022-11-23 11:43:47 -08:00
Jamie Hardt
5b9d326e94 Removed some dead code 2022-11-23 11:04:00 -08:00
Jamie Hardt
85775055a9 removed settings 2022-11-23 10:52:41 -08:00
Jamie Hardt
59509e4399 Added .vscode to gitignore 2022-11-23 10:51:05 -08:00
Jamie Hardt
3a63ce9c8c Fixed mistake in call 2022-11-23 10:48:48 -08:00
Jamie Hardt
5bfe0bd95b Moved Test ADM file into folder 2022-11-23 09:03:26 -08:00
Jamie Hardt
992de72cc9 Add files via upload
Added an ADM WAV from Pro Tools
2022-11-23 08:43:48 -08:00
Jamie Hardt
ee305cebf4 Update setup.py 2022-11-22 22:42:40 -08:00
Jamie Hardt
ea4f484488 Update python-package.yml
Adding 3.11 to matrix
2022-11-22 22:41:27 -08:00
Jamie Hardt
d00e07be36 Update README.md 2022-11-22 22:38:45 -08:00
Jamie Hardt
68931348a6 Update README.md 2022-11-22 22:37:55 -08:00
Jamie Hardt
68c75fc43f ADM impl 2022-11-23 06:29:07 +00:00
Jamie Hardt
1eca249ba4 Axml implementation 2022-11-23 06:23:31 +00:00
Jamie Hardt
2052fa385a Some AXML impl 2022-11-22 19:51:08 +00:00
Jamie Hardt
3096f02971 README.md
Added dolby ADM profile link
2022-11-22 19:50:51 +00:00
Jamie Hardt
be47786439 Update devcontainer.json 2022-11-21 23:15:33 -08:00
Jamie Hardt
ecde5359f1 Update devcontainer.json 2022-11-21 22:53:25 -08:00
Jamie Hardt
8ae73213bc Dev container 2022-11-22 06:52:44 +00:00
Jamie Hardt
53217ce293 Devcontainer 2022-11-22 06:48:57 +00:00
Jamie Hardt
f9969d32cc Setting up devcontainer 2022-11-22 06:45:53 +00:00
Jamie Hardt
04c402680b Create devcontainer.json 2022-11-21 22:22:07 -08:00
Jamie Hardt
f10a546fe9 Update pythonpublish.yml 2022-11-21 20:27:58 -08:00
17 changed files with 221 additions and 29 deletions

View File

@@ -0,0 +1,26 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/anaconda:1": {},
"ghcr.io/devcontainers/features/python:1": {
"version":"3.11"
},
"ghcr.io/meaningful-ooo/devcontainer-features/fish:1": {}
},
"postCreateCommand": "pip3 install -r requirements.txt && pip3 install -r docs/requirements.txt && pip3 install pytest && conda install -y ffmpeg",
"customizations": {
"vscode": {
"extensions": [
"ms-python.isort",
"ms-toolsai.jupyter",
"ms-toolsai.vscode-jupyter-cell-tags",
"ms-toolsai.jupyter-keymap",
"ms-toolsai.jupyter-renderers",
"ms-toolsai.vscode-jupyter-slideshow",
"ms-python.python",
"ms-python.vscode-pylance",
"george-alisson.html-preview-vscode"
]
}
}
}

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10", "3.11"]
steps: steps:
- uses: actions/checkout@v2.5.0 - uses: actions/checkout@v2.5.0

View File

@@ -29,7 +29,7 @@ jobs:
with: with:
message: | message: |
I just released a new version of wavinfo, my library for reading WAVE file metadata! I just released a new version of wavinfo, my library for reading WAVE file metadata!
#sounddesign #filmmaking #audio #python
${{ github.server_url }}/${{ github.repository }} ${{ github.server_url }}/${{ github.repository }}
env: env:
MASTODON_URL: ${{ secrets.MASTODON_URL }} MASTODON_URL: ${{ secrets.MASTODON_URL }}

3
.gitignore vendored
View File

@@ -90,6 +90,7 @@ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
docs_venv/ docs_venv/
venv_docs/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@@ -107,3 +108,5 @@ docs_venv/
# vim swap # vim swap
*.swp *.swp
.DS_Store .DS_Store
.vscode/

View File

@@ -1,3 +0,0 @@
{
"esbonio.sphinx.confDir": ""
}

View File

@@ -9,19 +9,23 @@ The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] an
* __Broadcast-WAVE__ metadata<sup>[1][ebu]</sup>, including embedded program * __Broadcast-WAVE__ metadata<sup>[1][ebu]</sup>, including embedded program
loudness and coding history, if extant. This also includes the SMPTE UMID<sup>[2][smpte_330m2011]</sup>. loudness and coding history, if extant. This also includes the SMPTE UMID<sup>[2][smpte_330m2011]</sup>.
* __iXML__ production recorder metadata<sup>[3][ixml]</sup>, including project, scene, and take tags, recorder notes * ADM track metadata<sup>[3][adm]</sup>, including channel, pack formats, object and content names.
* __iXML__ production recorder metadata<sup>[4][ixml]</sup>, including project, scene, and take tags, recorder notes
and file family information. and file family information.
* Most of the common __RIFF INFO__<sup>[4][info-tags]</sup> metadata fields. * Most of the common __RIFF INFO__<sup>[5][info-tags]</sup> metadata fields.
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count * The __wav format__ is also parsed, so you can access the basic sample rate and channel count
information. information.
In progress: In progress:
* ADM metadata consilient with the output of the __Dolby RMU__, perhaps later fully complaint with [ITU BS.2076-2][adm]. * [Dolby RMU][dolby] metadata and [EBU Tech 3285 Supplement 6][ebu3285s6].
* iXML `STEINBERG` sound library attributes. * iXML `STEINBERG` sound library attributes.
* __NetMix__ library attributes. * __NetMix__ library attributes.
* Pro Tools __embedded regions__. * Pro Tools __embedded regions__.
[dolby]:https://developer.dolby.com/globalassets/documentation/technology/dolby_atmos_master_adm_profile_v1.0.pdf
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf [ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
[ebu3285s6]:https://tech.ebu.ch/docs/tech/tech3285s6.pdf
[adm]:https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2076-2-201910-I!!PDF-E.pdf [adm]:https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2076-2-201910-I!!PDF-E.pdf
[smpte_330m2011]:http://standards.smpte.org/content/978-1-61482-678-1/st-330-2011/SEC1.abstract [smpte_330m2011]:http://standards.smpte.org/content/978-1-61482-678-1/st-330-2011/SEC1.abstract
[ixml]:http://www.ixml.info [ixml]:http://www.ixml.info

View File

@@ -10,9 +10,10 @@ Welcome to wavinfo's documentation!
:maxdepth: 2 :maxdepth: 2
:caption: Notes :caption: Notes
metadata_scopes/adm.rst
metadata_scopes/bext.rst metadata_scopes/bext.rst
metadata_scopes/ixml.rst
metadata_scopes/info.rst metadata_scopes/info.rst
metadata_scopes/ixml.rst
classes classes

View File

@@ -0,0 +1,15 @@
ADM (Audio Definition Model) Metadata
=====================================
Notes
-----
Class Reference
---------------
.. module:: wavinfo
.. autoclass:: wavinfo.wave_adm_reader.WavADMReader
:members:

View File

@@ -1,6 +1,6 @@
""" """
Wavinfo Wavinfo
""" """
__version__ = '1.7.2' __version__ = '2.0.0'
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>' __author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
__license__ = "MIT" __license__ = "MIT"

View File

@@ -28,7 +28,8 @@ setup(name='wavinfo',
'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10"], "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11"],
keywords='waveform metadata audio ebu smpte avi library film tv editing editorial', keywords='waveform metadata audio ebu smpte avi library film tv editing editorial',
install_requires=['lxml'], install_requires=['lxml'],
entry_points={ entry_points={

40
tests/test_adm.py Normal file
View File

@@ -0,0 +1,40 @@
from unittest import TestCase
import wavinfo
class TestADMWave(TestCase):
def setUp(self) -> None:
self.protools_adm_wav = "tests/test_files/protools/Test_ADM_ProTools.wav"
return super().setUp()
def test_chna(self):
info = wavinfo.WavInfoReader(self.protools_adm_wav)
self.assertIsNotNone(info)
adm = info.adm
self.assertIsNotNone(adm)
self.assertEqual(len(adm.channel_uids), 14)
def test_to_dict(self):
info = wavinfo.WavInfoReader(self.protools_adm_wav)
adm = info.adm
dict = adm.to_dict()
self.assertIsNotNone(dict)
def test_track_info(self):
info = wavinfo.WavInfoReader(self.protools_adm_wav)
adm = info.adm
t1 = adm.track_info(0)
self.assertTrue("channel_format_name" in t1.keys())
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
self.assertTrue("pack_format_name" in t1.keys())
self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"])
t10 = adm.track_info(10)
self.assertTrue("content_name" in t10.keys())
self.assertEqual("Dialog", t10["content_name"])

Binary file not shown.

View File

@@ -1,5 +1,4 @@
import os.path import os.path
import sys
from unittest import TestCase from unittest import TestCase

View File

@@ -0,0 +1,89 @@
"""
ADM Reader
"""
from struct import unpack, unpack_from, calcsize
from io import BytesIO
from collections import namedtuple
from typing import Iterable, Tuple
from lxml import etree as ET
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
class WavADMReader:
"""
Reads XML data from an EBU ADM (Audio Definiton Model) WAV File.
"""
def __init__(self, axml_data: bytes, chna_data: bytes):
header_fmt = "<HH"
uid_fmt = "<H12s14s11sx"
#: An :mod:`lxml.etree` of the ADM XML document
self.axml = ET.parse(BytesIO(axml_data))
_, uid_count = unpack(header_fmt, chna_data[0:4])
#: A list of :class:`ChannelEntry` objects parsed from the
#: `chna` metadata chunk.
self.channel_uids = []
offset = calcsize(header_fmt)
for _ in range(uid_count):
track_index, uid, track_ref, pack_ref = unpack_from(uid_fmt, chna_data, offset)
# these values are either ascii or all null
self.channel_uids.append(ChannelEntry(track_index,
uid.decode('ascii') , track_ref.decode('ascii'), pack_ref.decode('ascii')))
offset += calcsize(uid_fmt)
def track_info(self, index):
"""
Information about a track in the WAV file.
:param index: index of audio track (indexed from zero)
:returns: a dictionary with content_name, object_name, pack_format_name, pack_type,
channel_format_name
"""
channel_info = next((x for x in self.channel_uids if x.track_index == index + 1), None)
if channel_info is None:
return None
ret_dict = {}
nsmap = self.axml.getroot().nsmap
trackformat_elem = self.axml.find(".//audioFormatExtended/audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref, namespaces=nsmap)
stream_id = trackformat_elem[0].text
channelformatref_elem = self.axml.find(".//audioFormatExtended/audioStreamFormat[@audioStreamFormatID='%s']/audioChannelFormatIDRef" % stream_id, namespaces=nsmap)
channelformat_id = channelformatref_elem.text
packformatref_elem = self.axml.find(".//audioFormatExtended/audioStreamFormat[@audioStreamFormatID='%s']/audioPackFormatIDRef" % stream_id, namespaces=nsmap)
packformat_id = packformatref_elem.text
channelformat_elem = self.axml.find(".//audioFormatExtended/audioChannelFormat[@audioChannelFormatID='%s']" % channelformat_id, namespaces=nsmap)
ret_dict['channel_format_name'] = channelformat_elem.get("audioChannelFormatName")
packformat_elem = self.axml.find(".//audioFormatExtended/audioPackFormat[@audioPackFormatID='%s']" % packformat_id, namespaces=nsmap)
ret_dict['pack_type'] = packformat_elem.get("typeDefinition")
ret_dict['pack_format_name'] = packformat_elem.get("audioPackFormatName")
object_elem = self.axml.find(".//audioFormatExtended/audioObject[audioPackFormatIDRef = '%s']" % packformat_id, namespaces=nsmap)
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
object_id = object_elem.get("audioObjectID")
content_elem = self.axml.find(".//audioFormatExtended/audioContent/[audioObjectIDRef = '%s']" % object_id, namespaces=nsmap)
ret_dict['content_name'] = content_elem.get("audioContentName")
return ret_dict
def to_dict(self):
return dict(channel_entries=list(map(lambda z: z._asdict(), self.channel_uids)))

View File

@@ -1,9 +0,0 @@
from lxml import etree as ET
from collections import namedtuple
from ear.fileio.bw64 import Bw64Reader

View File

@@ -89,3 +89,13 @@ class WavIXMLFormat:
The name of this file's file family. The name of this file's file family.
""" """
return self._get_text_value("FILE_SET/FAMILY_NAME") return self._get_text_value("FILE_SET/FAMILY_NAME")
def to_dict(self):
return dict(track_list=list(map(lambda x: x._asdict(), self.track_list)),
project=self.project,
scene=self.scene,
take=self.take,
tape=self.tape,
family_uid=self.family_uid,
family_name=self.family_name
)

View File

@@ -9,6 +9,7 @@ from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
from .wave_ixml_reader import WavIXMLFormat from .wave_ixml_reader import WavIXMLFormat
from .wave_bext_reader import WavBextReader from .wave_bext_reader import WavBextReader
from .wave_info_reader import WavInfoChunkReader from .wave_info_reader import WavInfoChunkReader
from .wave_adm_reader import WavADMReader
#: Calculated statistics about the audio data. #: Calculated statistics about the audio data.
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count') WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
@@ -75,6 +76,9 @@ class WavInfoReader:
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata #: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
self.ixml = self._get_ixml(wavfile) self.ixml = self._get_ixml(wavfile)
#: :class:`wavinfo.wave_axml_reader.WavAxmlReader` with ADM metadata
self.adm = self._get_adm(wavfile)
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata #: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
self.info = self._get_info(wavfile, encoding=self.info_encoding) self.info = self._get_info(wavfile, encoding=self.info_encoding)
self.data = self._describe_data() self.data = self._describe_data()
@@ -130,6 +134,11 @@ class WavInfoReader:
bext_data = self._find_chunk_data(b'bext', f, default_none=True) bext_data = self._find_chunk_data(b'bext', f, default_none=True)
return WavBextReader(bext_data, encoding) if bext_data else None return WavBextReader(bext_data, encoding) if bext_data else None
def _get_adm(self, f):
axml = self._find_chunk_data(b'axml', f, default_none=True)
chna = self._find_chunk_data(b'chna', f, default_none=True)
return WavADMReader(axml_data=axml, chna_data=chna) if axml and chna else None
def _get_ixml(self, f): def _get_ixml(self, f):
ixml_data = self._find_chunk_data(b'iXML', f, default_none=True) ixml_data = self._find_chunk_data(b'iXML', f, default_none=True)
return None if ixml_data is None else WavIXMLFormat(ixml_data.rstrip(b'\0')) return None if ixml_data is None else WavIXMLFormat(ixml_data.rstrip(b'\0'))
@@ -142,22 +151,29 @@ class WavInfoReader:
metadata field, and the value. metadata field, and the value.
""" """
scopes = ('fmt', 'data') # 'bext', 'ixml', 'info') scopes = ('fmt', 'data', 'ixml', 'bext', 'info')
for scope in scopes: for scope in scopes:
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)
if self.bext is not None: if scope in ['bext']:
bext_dict = (self.bext or {}).to_dict() bext_dict = self.bext.to_dict() if self.bext else {}
for key in bext_dict.keys(): for key in bext_dict.keys():
yield 'bext', key, bext_dict[key] yield 'bext', key, bext_dict[key]
if self.info is not None: if scope in ['info']:
info_dict = self.info.to_dict() info_dict = self.info.to_dict() if self.info else {}
for key in info_dict.keys(): for key in info_dict.keys():
yield 'info', key, info_dict[key] yield 'info', key, info_dict[key]
if scope in ['ixml']:
ixml_dict = self.ixml.to_dict() if self.ixml else {}
for key in ixml_dict.keys():
yield 'ixml', key, ixml_dict[key]
def __repr__(self): def __repr__(self):
return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding) return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)