ADM support

This commit is contained in:
Jamie Hardt
2022-11-23 14:23:42 -08:00
parent a063fffb41
commit 8f2fd69b00
3 changed files with 74 additions and 26 deletions

View File

@@ -14,9 +14,10 @@ The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] an
* Most of the common __RIFF INFO__<sup>[4][info-tags]</sup> metadata fields. * Most of the common __RIFF INFO__<sup>[4][info-tags]</sup> metadata fields.
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count * The __wav format__ is also parsed, so you can access the basic sample rate and channel count
information. information.
* EBU ADM track metadata<sup>[5][adm]</sup>, including track, channel and pack formats, object and content names.
In progress: In progress:
* ADM metadata consilient with the output of the [__Dolby RMU__][dolby], and [EBU Tech 3285 Supplement 6][ebu3285s6]. * [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__.

View File

@@ -17,8 +17,24 @@ class TestADMWave(TestCase):
self.assertEqual(len(adm.channel_uids), 14) self.assertEqual(len(adm.channel_uids), 14)
# def test_to_dict(self): def test_to_dict(self):
# info = wavinfo.WavInfoReader(self.protools_adm_wav) info = wavinfo.WavInfoReader(self.protools_adm_wav)
# adm = info.adm adm = info.adm
# dict = adm.to_dict() 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"])

View File

@@ -3,35 +3,19 @@ ADM Reader
""" """
from struct import unpack, unpack_from, calcsize from struct import unpack, unpack_from, calcsize
from dataclasses import dataclass
from io import BytesIO from io import BytesIO
from collections import namedtuple
from typing import Iterable, Tuple
from lxml import etree as ET from lxml import etree as ET
@dataclass
class ChannelEntry:
"""
A `chna` chunk table entry.
"""
track_index: int
"Track index (indexed from 1)"
uid: str
"audioTrackUID"
track_ref: str
"audioTrackFormatID"
pack_ref: str
"audioPackFormatID"
class WavADMReader: class WavADMReader:
""" """
Reads XML data from an EBU ADM (Audio Definiton Model) WAV File. Reads XML data from an EBU ADM (Audio Definiton Model) WAV File.
""" """
ChannelEntry = namedtuple('ChannelEntry', "track_index uid track_ref pack_ref")
def __init__(self, axml_data: bytes, chna_data: bytes): def __init__(self, axml_data: bytes, chna_data: bytes):
header_fmt = "<HH" header_fmt = "<HH"
uid_fmt = "<H12s14s11sx" uid_fmt = "<H12s14s11sx"
@@ -49,7 +33,54 @@ class WavADMReader:
# these values are either ascii or all null # these values are either ascii or all null
self.channel_uids.append(ChannelEntry(track_index, self.channel_uids.append(WavADMReader.ChannelEntry(track_index,
uid.decode('ascii') , track_ref.decode('ascii'), pack_ref.decode('ascii'))) uid.decode('ascii') , track_ref.decode('ascii'), pack_ref.decode('ascii')))
offset += calcsize(uid_fmt) 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)))