mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2026-01-01 01:10:40 +00:00
190 lines
6.7 KiB
Python
190 lines
6.7 KiB
Python
"""
|
|
ADM Reader
|
|
"""
|
|
|
|
from struct import unpack, unpack_from, calcsize
|
|
from io import BytesIO
|
|
from collections import namedtuple
|
|
from typing import Optional
|
|
|
|
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])
|
|
|
|
self.channel_uids = []
|
|
|
|
offset = calcsize(header_fmt)
|
|
for _ in range(uid_count):
|
|
|
|
track_index, uid, track_ref, pack_ref = unpack_from(uid_fmt,
|
|
chna_data,
|
|
offset)
|
|
|
|
# these values are either ascii or all null
|
|
|
|
self.channel_uids.append(
|
|
ChannelEntry(track_index - 1,
|
|
uid.decode('ascii'),
|
|
track_ref.decode('ascii'),
|
|
pack_ref.decode('ascii')
|
|
)
|
|
)
|
|
|
|
offset += calcsize(uid_fmt)
|
|
|
|
def xml_str(self) -> str:
|
|
"""ADM XML as a string"""
|
|
return ET.tostring(self.axml).decode("utf-8")
|
|
|
|
def programme(self) -> dict:
|
|
"""
|
|
Read the ADM `audioProgramme` data structure and some of its reference
|
|
properties.
|
|
"""
|
|
ret_dict = dict()
|
|
|
|
nsmap = self.axml.getroot().nsmap
|
|
|
|
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
|
|
|
program = afext.find("audioProgramme", namespaces=nsmap)
|
|
ret_dict['programme_id'] = program.get("audioProgrammeID")
|
|
ret_dict['programme_name'] = program.get("audioProgrammeName")
|
|
ret_dict['programme_start'] = program.get("start")
|
|
ret_dict['programme_end'] = program.get("end")
|
|
ret_dict['contents'] = []
|
|
|
|
for content_ref in program.findall("audioContentIDRef",
|
|
namespaces=nsmap):
|
|
content_dict = dict()
|
|
content_dict['content_id'] = cid = content_ref.text
|
|
content = afext.find("audioContent[@audioContentID='%s']" % cid,
|
|
namespaces=nsmap)
|
|
content_dict['content_name'] = content.get("audioContentName")
|
|
content_dict['objects'] = []
|
|
|
|
for object_ref in content.findall("audioObjectIDRef",
|
|
namespaces=nsmap):
|
|
object_dict = dict()
|
|
object_dict['object_id'] = oid = object_ref.text
|
|
object = afext.find("audioObject[@audioObjectID='%s']" % oid,
|
|
namespaces=nsmap)
|
|
pack = object.find("audioPackFormatIDRef", namespaces=nsmap)
|
|
object_dict['object_name'] = object.get("audioObjectName")
|
|
object_dict['object_start'] = object.get("start")
|
|
object_dict['object_duration'] = object.get("duration")
|
|
object_dict['pack_id'] = pack.text
|
|
track_uid_list = []
|
|
for t in object.findall("audioTrackUIDRef", namespaces=nsmap):
|
|
track_uid_list.append(t.text)
|
|
|
|
object_dict['track_uids'] = track_uid_list
|
|
content_dict['objects'].append(object_dict)
|
|
|
|
ret_dict['contents'].append(content_dict)
|
|
|
|
return ret_dict
|
|
|
|
def track_info(self, index) -> Optional[dict]:
|
|
"""
|
|
Information about a track in the WAV file.
|
|
|
|
:param index: index of audio track (indexed from zero)
|
|
:returns: a dictionary with *content_name*, *content_id*,
|
|
*object_name*, *object_id*,
|
|
*pack_format_name*, *pack_type*, *channel_format_name*
|
|
"""
|
|
channel_info = next((x for x in self.channel_uids
|
|
if x.track_index == index), None)
|
|
|
|
if channel_info is None:
|
|
return None
|
|
|
|
ret_dict = {}
|
|
|
|
nsmap = self.axml.getroot().nsmap
|
|
|
|
afext = self.axml.find(".//audioFormatExtended",
|
|
namespaces=nsmap)
|
|
|
|
trackformat_elem = afext.find(
|
|
"audioTrackFormat[@audioTrackFormatID='%s']"
|
|
% channel_info.track_ref, namespaces=nsmap)
|
|
|
|
stream_id = trackformat_elem[0].text
|
|
|
|
channelformatref_elem = afext.find(
|
|
("audioStreamFormat[@audioStreamFormatID='%s']"
|
|
"/audioChannelFormatIDRef") % stream_id,
|
|
namespaces=nsmap)
|
|
channelformat_id = channelformatref_elem.text
|
|
|
|
packformatref_elem = afext.find(
|
|
("audioStreamFormat[@audioStreamFormatID='%s']"
|
|
"/audioPackFormatIDRef") % stream_id,
|
|
namespaces=nsmap)
|
|
packformat_id = packformatref_elem.text
|
|
|
|
channelformat_elem = afext\
|
|
.find("audioChannelFormat[@audioChannelFormatID='%s']"
|
|
% channelformat_id,
|
|
namespaces=nsmap)
|
|
ret_dict['channel_format_name'] = channelformat_elem.get(
|
|
"audioChannelFormatName")
|
|
|
|
packformat_elem = afext.find(
|
|
"audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
|
|
namespaces=nsmap)
|
|
ret_dict['pack_type'] = packformat_elem.get(
|
|
"typeDefinition")
|
|
ret_dict['pack_format_name'] = packformat_elem.get(
|
|
"audioPackFormatName")
|
|
|
|
object_elem = afext.find("audioObject[audioPackFormatIDRef = '%s']"
|
|
% packformat_id,
|
|
namespaces=nsmap)
|
|
|
|
ret_dict['audio_object_name'] = object_elem.get("audioObjectName")
|
|
object_id = object_elem.get("audioObjectID")
|
|
ret_dict['object_id'] = object_id
|
|
|
|
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']"
|
|
% object_id,
|
|
namespaces=nsmap)
|
|
|
|
ret_dict['content_name'] = content_elem.get("audioContentName")
|
|
ret_dict['content_id'] = content_elem.get("audioContentID")
|
|
|
|
return ret_dict
|
|
|
|
def to_dict(self) -> dict: # FIXME should be "asdict"
|
|
"""
|
|
Get ADM metadata as a dictionary.
|
|
"""
|
|
|
|
def make_entry(channel_uid_rec):
|
|
rd = channel_uid_rec._asdict()
|
|
rd.update(self.track_info(channel_uid_rec.track_index))
|
|
return rd
|
|
|
|
return dict(channel_entries=list(map(lambda z: make_entry(z),
|
|
self.channel_uids)),
|
|
programme=self.programme())
|