mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 08:50:41 +00:00
3
.flake8
Normal file
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
per-file-ignores =
|
||||
wavinfo/__init__.py: F401
|
||||
1
.github/workflows/python-package.yml
vendored
1
.github/workflows/python-package.yml
vendored
@@ -40,3 +40,4 @@ jobs:
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
flake8 wavinfo
|
||||
|
||||
@@ -15,17 +15,20 @@ class TestADMWave(TestCase):
|
||||
adm = info.adm
|
||||
self.assertIsNotNone(adm)
|
||||
|
||||
assert adm is not None
|
||||
self.assertEqual(len(adm.channel_uids), 14)
|
||||
|
||||
def test_to_dict(self):
|
||||
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||
adm = info.adm
|
||||
assert adm is not None
|
||||
dict = adm.to_dict()
|
||||
self.assertIsNotNone(dict)
|
||||
|
||||
def test_programme(self):
|
||||
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||
adm = info.adm
|
||||
assert adm is not None
|
||||
pdict = adm.programme()
|
||||
self.assertIn("programme_id", pdict.keys())
|
||||
self.assertIn("programme_name", pdict.keys())
|
||||
@@ -37,7 +40,7 @@ class TestADMWave(TestCase):
|
||||
def test_track_info(self):
|
||||
info = wavinfo.WavInfoReader(self.protools_adm_wav)
|
||||
adm = info.adm
|
||||
|
||||
assert adm is not None
|
||||
t1 = adm.track_info(0)
|
||||
self.assertTrue("channel_format_name" in t1.keys())
|
||||
self.assertEqual("RoomCentricLeft", t1["channel_format_name"])
|
||||
|
||||
@@ -12,6 +12,7 @@ class TestCue(TestCase):
|
||||
file1 = "tests/test_files/cue_chunks/STE-000.wav"
|
||||
w1 = wavinfo.WavInfoReader(file1)
|
||||
self.assertIsNotNone(w1.cues)
|
||||
assert w1.cues is not None
|
||||
vals = list(w1.cues.each_cue())
|
||||
self.assertEqual(vals, [(1,29616),(2,74592),(3,121200)])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import wavinfo
|
||||
from wavinfo.wave_dbmd_reader import SegmentType, DolbyAtmosMetadata, DolbyDigitalPlusMetadata
|
||||
from wavinfo.wave_dbmd_reader import SegmentType, DolbyDigitalPlusMetadata
|
||||
|
||||
class TestDolby(TestCase):
|
||||
def setUp(self):
|
||||
@@ -19,8 +19,10 @@ class TestDolby(TestCase):
|
||||
d = t1.dolby
|
||||
assert d is not None
|
||||
|
||||
ddp = [x for x in d.segment_list if x[0] == SegmentType.DolbyDigitalPlus]
|
||||
atmos = [x for x in d.segment_list if x[0] == SegmentType.DolbyAtmos]
|
||||
ddp = [x for x in d.segment_list \
|
||||
if x[0] == SegmentType.DolbyDigitalPlus]
|
||||
atmos = [x for x in d.segment_list \
|
||||
if x[0] == SegmentType.DolbyAtmos]
|
||||
|
||||
self.assertEqual(len(ddp), 1)
|
||||
self.assertEqual(len(atmos), 1)
|
||||
@@ -38,8 +40,13 @@ class TestDolby(TestCase):
|
||||
d = t1.dolby
|
||||
assert d is not None
|
||||
ddp = d.dolby_digital_plus()
|
||||
self.assertEqual(len(ddp), 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.assertEqual(len(ddp), 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)
|
||||
|
||||
def test_atmos(self):
|
||||
@@ -47,6 +54,7 @@ class TestDolby(TestCase):
|
||||
d = t1.dolby
|
||||
assert d is not None
|
||||
atmos = d.dolby_atmos()
|
||||
self.assertEqual(len(atmos), 1, "Failed to find exactly one Atmos metadata segment")
|
||||
self.assertEqual(len(atmos), 1,
|
||||
"Failed to find exactly one Atmos metadata segment")
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ class MainTest(unittest.TestCase):
|
||||
|
||||
def test_ixml(self):
|
||||
with patch.object(sys, 'argv',
|
||||
['TEST', '--ixml', 'tests/test_files/sounddevices/A101_1.WAV']):
|
||||
['TEST', '--ixml',
|
||||
'tests/test_files/sounddevices/A101_1.WAV']):
|
||||
try:
|
||||
main()
|
||||
except:
|
||||
|
||||
@@ -13,7 +13,9 @@ class TestWaveInfo(TestCase):
|
||||
def test_sanity(self):
|
||||
for wav_file in all_files():
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
self.assertEqual(info.__repr__(), 'WavInfoReader({}, latin_1, ascii)'.format(os.path.abspath(wav_file)))
|
||||
self.assertEqual(info.__repr__(),
|
||||
'WavInfoReader({}, latin_1, ascii)'
|
||||
.format(os.path.abspath(wav_file)))
|
||||
self.assertIsNotNone(info)
|
||||
|
||||
def test_fmt_against_ffprobe(self):
|
||||
@@ -24,14 +26,21 @@ class TestWaveInfo(TestCase):
|
||||
assert info.fmt is not None
|
||||
assert ffprobe_info is not None
|
||||
|
||||
self.assertEqual(info.fmt.channel_count, ffprobe_info['streams'][0]['channels'])
|
||||
self.assertEqual(info.fmt.sample_rate, int(ffprobe_info['streams'][0]['sample_rate']))
|
||||
self.assertEqual(info.fmt.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_sample']))
|
||||
self.assertEqual(info.fmt.channel_count,
|
||||
ffprobe_info['streams'][0]['channels'])
|
||||
self.assertEqual(info.fmt.sample_rate,
|
||||
int(ffprobe_info['streams'][0]['sample_rate']))
|
||||
self.assertEqual(info.fmt.bits_per_sample,
|
||||
int(ffprobe_info['streams'][0]['bits_per_sample']
|
||||
))
|
||||
|
||||
if info.fmt.audio_format == 1:
|
||||
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm'))
|
||||
self.assertTrue(ffprobe_info['streams'][0]['codec_name']\
|
||||
.startswith('pcm'))
|
||||
streams = ffprobe_info['streams'][0]
|
||||
byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_sample']) / 8
|
||||
byte_rate = int(streams['sample_rate']) * \
|
||||
streams['channels'] * \
|
||||
int(streams['bits_per_sample']) / 8
|
||||
self.assertEqual(info.fmt.byte_rate, byte_rate)
|
||||
|
||||
def test_data_against_ffprobe(self):
|
||||
@@ -40,7 +49,8 @@ class TestWaveInfo(TestCase):
|
||||
ffprobe_info = cast(Dict[str,Any], ffprobe(wav_file))
|
||||
assert ffprobe_info is not None
|
||||
assert info.data is not None
|
||||
self.assertEqual(info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts']))
|
||||
self.assertEqual(info.data.frame_count,
|
||||
int(ffprobe_info['streams'][0]['duration_ts']))
|
||||
|
||||
def test_bext_against_ffprobe(self):
|
||||
for wav_file in all_files():
|
||||
@@ -50,39 +60,63 @@ class TestWaveInfo(TestCase):
|
||||
|
||||
if info.bext:
|
||||
if 'comment' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual(info.bext.description, ffprobe_info['format']['tags']['comment'])
|
||||
self.assertEqual(info.bext.description,
|
||||
ffprobe_info['format']['tags']\
|
||||
['comment'])
|
||||
else:
|
||||
self.assertEqual(info.bext.description, '')
|
||||
|
||||
if 'encoded_by' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual(info.bext.originator, ffprobe_info['format']['tags']['encoded_by'])
|
||||
self.assertEqual(info.bext.originator,
|
||||
ffprobe_info['format']['tags']\
|
||||
['encoded_by'])
|
||||
else:
|
||||
self.assertEqual(info.bext.originator, '')
|
||||
|
||||
if 'originator_reference' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual(info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'])
|
||||
self.assertEqual(info.bext.originator_ref,
|
||||
ffprobe_info['format']['tags']\
|
||||
['originator_reference'])
|
||||
else:
|
||||
self.assertEqual(info.bext.originator_ref, '')
|
||||
|
||||
# these don't always reflect the bext info
|
||||
# self.assertEqual(info.bext.originator_date, ffprobe_info['format']['tags']['date'])
|
||||
# self.assertEqual(info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'])
|
||||
self.assertEqual(info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']))
|
||||
# self.assertEqual(info.bext.originator_date,
|
||||
# ffprobe_info['format']['tags']['date'])
|
||||
# self.assertEqual(info.bext.originator_time,
|
||||
# ffprobe_info['format']['tags']['creation_time'])
|
||||
self.assertEqual(info.bext.time_reference,
|
||||
int(ffprobe_info['format']['tags']\
|
||||
['time_reference']))
|
||||
|
||||
if 'coding_history' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual(info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'])
|
||||
self.assertEqual(info.bext.coding_history,
|
||||
ffprobe_info['format']['tags']\
|
||||
['coding_history'])
|
||||
else:
|
||||
self.assertEqual(info.bext.coding_history, '')
|
||||
|
||||
def test_ixml(self):
|
||||
expected = {'A101_4.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '4',
|
||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
|
||||
'A101_3.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '3',
|
||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
|
||||
'A101_2.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '2',
|
||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
|
||||
'A101_1.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '1',
|
||||
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
|
||||
expected = {'A101_4.WAV': {'project': 'BMH',
|
||||
'scene': 'A101', 'take': '4',
|
||||
'tape': '18Y12M31',
|
||||
'family_uid':
|
||||
'USSDVGR1112089007124015008231000'},
|
||||
'A101_3.WAV': {'project': 'BMH',
|
||||
'scene': 'A101', 'take': '3',
|
||||
'tape': '18Y12M31',
|
||||
'family_uid':
|
||||
'USSDVGR1112089007124014008228300'},
|
||||
'A101_2.WAV': {'project': 'BMH',
|
||||
'scene': 'A101', 'take': '2',
|
||||
'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():
|
||||
@@ -112,7 +146,8 @@ class TestWaveInfo(TestCase):
|
||||
assert info.ixml.steinberg is not None
|
||||
self.assertIsNotNone(info.ixml.steinberg.audio_speaker_arrangement)
|
||||
self.assertEqual(info.ixml.steinberg.sample_format_size, 3)
|
||||
self.assertEqual(info.ixml.steinberg.media_company, "https://github.com/iluvcapra/wavinfo")
|
||||
self.assertEqual(info.ixml.steinberg.media_company,
|
||||
"https://github.com/iluvcapra/wavinfo")
|
||||
self.assertFalse(info.ixml.steinberg.media_drop_frames)
|
||||
self.assertEqual(info.ixml.steinberg.media_duration, 1200.0)
|
||||
|
||||
@@ -124,7 +159,8 @@ class TestWaveInfo(TestCase):
|
||||
self.assertIsNone(info.ixml.steinberg)
|
||||
|
||||
def test_info_metadata(self):
|
||||
file_with_metadata = 'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
||||
file_with_metadata = \
|
||||
'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
||||
self.assertTrue(os.path.exists(file_with_metadata))
|
||||
info = wavinfo.WavInfoReader(file_with_metadata).info
|
||||
|
||||
@@ -138,7 +174,8 @@ class TestWaveInfo(TestCase):
|
||||
self.assertEqual(info.software, 'Sound Grinder Pro')
|
||||
self.assertEqual(info.created_date, '2010-12-28')
|
||||
self.assertEqual(info.engineer, 'JPH')
|
||||
self.assertEqual(info.keywords, 'Sound Effect, movement, microphone, bump')
|
||||
self.assertEqual(info.keywords,
|
||||
'Sound Effect, movement, microphone, bump')
|
||||
self.assertEqual(info.title, 'camera bumb 1')
|
||||
self.assertEqual(type(info.to_dict()), dict)
|
||||
self.assertEqual(type(info.__repr__()), str)
|
||||
|
||||
@@ -8,7 +8,8 @@ FFPROBE = 'ffprobe'
|
||||
|
||||
|
||||
def ffprobe(path):
|
||||
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
|
||||
arguments = [FFPROBE, "-of", "json",
|
||||
"-show_format", "-show_streams", path]
|
||||
if int(sys.version[0]) < 3:
|
||||
process = subprocess.Popen(arguments, stdout=PIPE)
|
||||
process.wait()
|
||||
@@ -20,7 +21,8 @@ def ffprobe(path):
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE)
|
||||
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)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from optparse import OptionParser, OptionGroup
|
||||
import datetime
|
||||
from . import WavInfoReader
|
||||
from . import __version__
|
||||
|
||||
from optparse import OptionParser
|
||||
import sys
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MyJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, Enum):
|
||||
@@ -13,28 +15,30 @@ class MyJSONEncoder(json.JSONEncoder):
|
||||
else:
|
||||
return super().default(o)
|
||||
|
||||
|
||||
class MissingDataError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
parser = OptionParser()
|
||||
|
||||
parser.usage = 'wavinfo (--adm | --ixml) <FILE> +'
|
||||
|
||||
# parser.add_option('-f', dest='output_format', help='Set the output format',
|
||||
# default='json',
|
||||
# metavar='FORMAT')
|
||||
parser.add_option('--adm', dest='adm',
|
||||
help='Output ADM XML',
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
parser.add_option('--adm', dest='adm', help='Output ADM XML',
|
||||
default=False, action='store_true')
|
||||
|
||||
parser.add_option('--ixml', dest='ixml', help='Output iXML',
|
||||
default=False, action='store_true')
|
||||
parser.add_option('--ixml', dest='ixml',
|
||||
help='Output iXML',
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
(options, args) = parser.parse_args(sys.argv)
|
||||
for arg in args[1:]:
|
||||
try:
|
||||
this_file = WavInfoReader(path=arg)
|
||||
this_file = WavInfoReader(f=arg)
|
||||
if options.adm:
|
||||
if this_file.adm:
|
||||
sys.stdout.write(this_file.adm.xml_str())
|
||||
@@ -60,7 +64,8 @@ def main():
|
||||
|
||||
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
|
||||
except MissingDataError as e:
|
||||
print("MissingDataError: Missing metadata (%s) in file %s" % (e, arg), file=sys.stderr)
|
||||
print("MissingDataError: Missing metadata (%s) in file %s" %
|
||||
(e, arg), file=sys.stderr)
|
||||
continue
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import struct
|
||||
from collections import namedtuple
|
||||
# from collections import namedtuple
|
||||
from typing import NamedTuple, Dict
|
||||
|
||||
from . import riff_parser
|
||||
|
||||
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
|
||||
|
||||
class RF64Context(NamedTuple):
|
||||
sample_count: int
|
||||
bigchunk_table: Dict[str, int]
|
||||
|
||||
|
||||
def parse_rf64(stream, signature=b'RF64') -> RF64Context:
|
||||
start = stream.tell()
|
||||
assert( stream.read(4) == b'WAVE' )
|
||||
assert stream.read(4) == b'WAVE'
|
||||
|
||||
ds64_chunk = riff_parser.parse_chunk(stream)
|
||||
assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \
|
||||
@@ -16,20 +20,22 @@ def parse_rf64(stream, signature = b'RF64') -> RF64Context:
|
||||
|
||||
ds64_field_spec = "<QQQI"
|
||||
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)
|
||||
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(
|
||||
ds64_field_spec, ds64_data[0:ds64_fields_size])
|
||||
ds64_field_spec, ds64_data[0:ds64_fields_size]
|
||||
)
|
||||
|
||||
bigchunk_table = {}
|
||||
chunksize64format = "<4sL"
|
||||
# chunksize64size = struct.calcsize(chunksize64format)
|
||||
|
||||
for _ in range(length_lookup_table):
|
||||
bigname, bigsize = struct.unpack_from(chunksize64format, ds64_data,
|
||||
bigname, bigsize = struct.unpack_from(chunksize64format,
|
||||
ds64_data,
|
||||
offset=ds64_fields_size)
|
||||
bigchunk_table[bigname] = bigsize
|
||||
|
||||
@@ -39,4 +45,3 @@ def parse_rf64(stream, signature = b'RF64') -> RF64Context:
|
||||
stream.seek(start, 0)
|
||||
return RF64Context(sample_count=sample_count,
|
||||
bigchunk_table=bigchunk_table)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
# from optparse import Option
|
||||
import struct
|
||||
from collections import namedtuple
|
||||
from .rf64_parser import parse_rf64
|
||||
from .rf64_parser import parse_rf64, RF64Context
|
||||
from typing import NamedTuple, Union, List, Optional
|
||||
|
||||
|
||||
class WavInfoEOFError(EOFError):
|
||||
@@ -10,11 +10,17 @@ class WavInfoEOFError(EOFError):
|
||||
self.chunk_start = chunk_start
|
||||
|
||||
|
||||
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
|
||||
pass
|
||||
class ListChunkDescriptor(NamedTuple):
|
||||
signature: bytes
|
||||
children: List[Union['ChunkDescriptor', 'ListChunkDescriptor']]
|
||||
|
||||
|
||||
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
|
||||
class ChunkDescriptor(NamedTuple):
|
||||
ident: bytes
|
||||
start: int
|
||||
length: int
|
||||
rf64_context: Optional[RF64Context]
|
||||
|
||||
def read_data(self, from_stream) -> bytes:
|
||||
from_stream.seek(self.start)
|
||||
return from_stream.read(self.length)
|
||||
@@ -49,7 +55,7 @@ def parse_chunk(stream, rf64_context=None):
|
||||
rf64_context = parse_rf64(stream=stream, signature=ident)
|
||||
|
||||
assert rf64_context is not None, \
|
||||
f"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]
|
||||
|
||||
@@ -64,5 +70,7 @@ def parse_chunk(stream, rf64_context=None):
|
||||
else:
|
||||
data_start = stream.tell()
|
||||
stream.seek(displacement, 1)
|
||||
return ChunkDescriptor(ident=ident, start=data_start, length=data_size,
|
||||
return ChunkDescriptor(ident=ident,
|
||||
start=data_start,
|
||||
length=data_size,
|
||||
rf64_context=rf64_context)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
|
||||
# def binary_to_string(binary_value):
|
||||
# return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
|
||||
|
||||
# return reduce(lambda val, el: val + "{:02x}".format(el),
|
||||
# binary_value, '')
|
||||
|
||||
# class UMIDParser:
|
||||
# """
|
||||
|
||||
@@ -5,12 +5,14 @@ ADM Reader
|
||||
from struct import unpack, unpack_from, calcsize
|
||||
from io import BytesIO
|
||||
from collections import namedtuple
|
||||
from typing import Iterable, Tuple
|
||||
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.
|
||||
@@ -26,24 +28,24 @@ class WavADMReader:
|
||||
|
||||
_, uid_count = unpack(header_fmt, chna_data[0:4])
|
||||
|
||||
#: A list of :class:`ChannelEntry` objects parsed from the
|
||||
#: `chna` metadata chunk.
|
||||
#:
|
||||
#: .. note::
|
||||
#: In-file, the `chna` track indexes start at 1. However, this interface
|
||||
#: numbers the first track 0, in order to maintain consistency with other
|
||||
#: libraries.
|
||||
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)
|
||||
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')))
|
||||
self.channel_uids.append(
|
||||
ChannelEntry(track_index - 1,
|
||||
uid.decode('ascii'),
|
||||
track_ref.decode('ascii'),
|
||||
pack_ref.decode('ascii')
|
||||
)
|
||||
)
|
||||
|
||||
offset += calcsize(uid_fmt)
|
||||
|
||||
@@ -53,7 +55,8 @@ class WavADMReader:
|
||||
|
||||
def programme(self) -> dict:
|
||||
"""
|
||||
Read the ADM `audioProgramme` data structure and some of its reference properties.
|
||||
Read the ADM `audioProgramme` data structure and some of its reference
|
||||
properties.
|
||||
"""
|
||||
ret_dict = dict()
|
||||
|
||||
@@ -68,17 +71,21 @@ class WavADMReader:
|
||||
ret_dict['programme_end'] = program.get("end")
|
||||
ret_dict['contents'] = []
|
||||
|
||||
for content_ref in program.findall("audioContentIDRef", namespaces=nsmap):
|
||||
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 = 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):
|
||||
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)
|
||||
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")
|
||||
@@ -95,15 +102,17 @@ class WavADMReader:
|
||||
|
||||
return ret_dict
|
||||
|
||||
def track_info(self, index) -> 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*,
|
||||
: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)
|
||||
channel_info = next((x for x in self.channel_uids
|
||||
if x.track_index == index), None)
|
||||
|
||||
if channel_info is None:
|
||||
return None
|
||||
@@ -112,38 +121,52 @@ class WavADMReader:
|
||||
|
||||
nsmap = self.axml.getroot().nsmap
|
||||
|
||||
afext = self.axml.find(".//audioFormatExtended", namespaces=nsmap)
|
||||
|
||||
trackformat_elem = afext.find("audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
channelformat_elem = afext\
|
||||
.find("audioChannelFormat[@audioChannelFormatID='%s']"
|
||||
% channelformat_id,
|
||||
namespaces=nsmap)
|
||||
ret_dict['channel_format_name'] = channelformat_elem.get("audioChannelFormatName")
|
||||
ret_dict['channel_format_name'] = channelformat_elem.get(
|
||||
"audioChannelFormatName")
|
||||
|
||||
packformat_elem = afext.find("audioPackFormat[@audioPackFormatID='%s']" % packformat_id,
|
||||
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")
|
||||
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,
|
||||
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,
|
||||
content_elem = afext.find("audioContent/[audioObjectIDRef = '%s']"
|
||||
% object_id,
|
||||
namespaces=nsmap)
|
||||
|
||||
ret_dict['content_name'] = content_elem.get("audioContentName")
|
||||
@@ -161,5 +184,6 @@ class WavADMReader:
|
||||
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)),
|
||||
return dict(channel_entries=list(map(lambda z: make_entry(z),
|
||||
self.channel_uids)),
|
||||
programme=self.programme())
|
||||
@@ -3,22 +3,26 @@ import struct
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class WavBextReader:
|
||||
def __init__(self, bext_data, encoding):
|
||||
"""
|
||||
Read Broadcast-WAV extended metadata.
|
||||
:param bext_data: The bytes-like data.
|
||||
:param encoding: The encoding to use when decoding the text fields of the
|
||||
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
|
||||
:param encoding: The encoding to use when decoding the text fields of
|
||||
the BEXT metadata scope. According to EBU Rec 3285 this shall be
|
||||
ASCII.
|
||||
"""
|
||||
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
||||
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + \
|
||||
"hhhhh" + "180s"
|
||||
|
||||
rest_starts = struct.calcsize(packstring)
|
||||
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
|
||||
|
||||
def sanitize_bytes(b: bytes) -> str:
|
||||
# honestly can't remember why I'm stripping nulls this way
|
||||
first_null = next((index for index, byte in enumerate(b) if byte == 0), None)
|
||||
first_null = next((index for index, byte in enumerate(b)
|
||||
if byte == 0), None)
|
||||
trimmed = b if first_null is None else b[:first_null]
|
||||
decoded = trimmed.decode(encoding)
|
||||
return decoded
|
||||
@@ -50,8 +54,8 @@ class WavBextReader:
|
||||
#: BEXT version.
|
||||
self.version: int = unpacked[6]
|
||||
|
||||
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
|
||||
#: may only be 32 bytes long.
|
||||
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though
|
||||
#: the UMID may only be 32 bytes long.
|
||||
self.umid: Optional[bytes] = None
|
||||
|
||||
#: EBU R128 Integrated loudness, in LUFS.
|
||||
|
||||
@@ -8,7 +8,6 @@ IBM Corporation and Microsoft Corporation
|
||||
https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import encodings
|
||||
from .riff_parser import ChunkDescriptor
|
||||
|
||||
from struct import unpack, calcsize
|
||||
@@ -252,8 +251,8 @@ class WavCuesReader:
|
||||
:returns: a tuple of the the cue's label (if present) and note (if
|
||||
present)
|
||||
"""
|
||||
label = next((l.text for l in self.labels
|
||||
if l.name == cue_ident), None)
|
||||
label = next((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)
|
||||
return (label, note)
|
||||
@@ -285,10 +284,3 @@ class WavCuesReader:
|
||||
retval[n]['length'] = r
|
||||
|
||||
return retval
|
||||
# return dict(cues=[c._asdict() for c in self.cues],
|
||||
# labels=[l._asdict() for l in self.labels],
|
||||
# ranges=[r._asdict() for r in self.ranges],
|
||||
# notes=[n._asdict() for n in self.notes])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ Unless otherwise stated, all § references here are to
|
||||
from enum import IntEnum, Enum
|
||||
from struct import unpack
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Optional, Tuple, Any, Union
|
||||
from typing import List, Tuple, Any, Union
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class SegmentType(IntEnum):
|
||||
"""
|
||||
Metadata segment type.
|
||||
@@ -77,7 +78,6 @@ class DolbyDigitalPlusMetadata:
|
||||
MUTE = 0b111
|
||||
"-∞ dB"
|
||||
|
||||
|
||||
class DolbySurroundEncodingMode(Enum):
|
||||
"""
|
||||
Dolby surround endcoding mode.
|
||||
@@ -87,7 +87,6 @@ class DolbyDigitalPlusMetadata:
|
||||
NOT_IN_USE = 0b01
|
||||
NOT_INDICATED = 0b00
|
||||
|
||||
|
||||
class BitStreamMode(Enum):
|
||||
"""
|
||||
Dolby Digital Plus `bsmod` field
|
||||
@@ -122,7 +121,6 @@ class DolbyDigitalPlusMetadata:
|
||||
should be interpreted as karaoke.
|
||||
"""
|
||||
|
||||
|
||||
class AudioCodingMode(Enum):
|
||||
"""
|
||||
Dolby Digital Plus `acmod` field
|
||||
@@ -144,7 +142,6 @@ class DolbyDigitalPlusMetadata:
|
||||
CH_ORD_3_2 = 0b111
|
||||
"LCR + LR surround"
|
||||
|
||||
|
||||
class CenterDownMixLevel(Enum):
|
||||
"""
|
||||
§ 4.3.3.1
|
||||
@@ -161,7 +158,6 @@ class DolbyDigitalPlusMetadata:
|
||||
|
||||
RESERVED = 0b11
|
||||
|
||||
|
||||
class SurroundDownMixLevel(Enum):
|
||||
"""
|
||||
Dolby Digital Plus `surmixlev` field
|
||||
@@ -172,7 +168,6 @@ class DolbyDigitalPlusMetadata:
|
||||
MUTE = 0b10
|
||||
RESERVED = 0b11
|
||||
|
||||
|
||||
class LanguageCode(int):
|
||||
"""
|
||||
§ 4.3.4.1
|
||||
@@ -181,21 +176,18 @@ class DolbyDigitalPlusMetadata:
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MixLevel(int):
|
||||
"""
|
||||
§ 4.3.6.2
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DialnormLevel(int):
|
||||
"""
|
||||
§ 4.3.4.4
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RoomType(Enum):
|
||||
"""
|
||||
`roomtyp` 4.3.6.3
|
||||
@@ -205,11 +197,10 @@ class DolbyDigitalPlusMetadata:
|
||||
SMALL_ROOM_FLAT_CURVE = 0b10
|
||||
RESERVED = 0b11
|
||||
|
||||
|
||||
class PreferredDownMixMode(Enum):
|
||||
"""
|
||||
Indicates the creating engineer's preference of what the receiver should
|
||||
downmix.
|
||||
Indicates the creating engineer's preference of what the receiver
|
||||
should downmix.
|
||||
§ 4.3.8.1
|
||||
"""
|
||||
NOT_INDICATED = 0b00
|
||||
@@ -217,7 +208,6 @@ class DolbyDigitalPlusMetadata:
|
||||
STEREO = 0b10
|
||||
PRO_LOGIC_2 = 0b11
|
||||
|
||||
|
||||
class SurroundEXMode(IntEnum):
|
||||
"""
|
||||
Dolby Surround-EX mode.
|
||||
@@ -228,7 +218,6 @@ class DolbyDigitalPlusMetadata:
|
||||
SEX = 0b10
|
||||
PRO_LOGIC_2 = 0b11
|
||||
|
||||
|
||||
class HeadphoneMode(IntEnum):
|
||||
"""
|
||||
`dheadphonmod` § 4.3.9.2
|
||||
@@ -238,12 +227,10 @@ class DolbyDigitalPlusMetadata:
|
||||
DOLBY_HEADPHONE = 0b10
|
||||
RESERVED = 0b11
|
||||
|
||||
|
||||
class ADConverterType(Enum):
|
||||
STANDARD = 0
|
||||
HDCD = 1
|
||||
|
||||
|
||||
class StreamDependency(Enum):
|
||||
"""
|
||||
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
|
||||
@@ -254,7 +241,6 @@ class DolbyDigitalPlusMetadata:
|
||||
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
|
||||
RESERVED = 3
|
||||
|
||||
|
||||
class RFCompressionProfile(Enum):
|
||||
"""
|
||||
`compr1` RF compression profile
|
||||
@@ -363,9 +349,11 @@ class DolbyDigitalPlusMetadata:
|
||||
pass
|
||||
|
||||
def surround_config(b):
|
||||
return DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4), \
|
||||
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2), \
|
||||
return (
|
||||
DolbyDigitalPlusMetadata.CenterDownMixLevel(b & 0x30 >> 4),
|
||||
DolbyDigitalPlusMetadata.SurroundDownMixLevel(b & 0xc >> 2),
|
||||
DolbyDigitalPlusMetadata.DolbySurroundEncodingMode(b & 0x3)
|
||||
)
|
||||
|
||||
def dialnorm_info(b):
|
||||
return (b & 0x80) > 0, b & 0x40 > 0, b & 0x20 > 0, \
|
||||
@@ -386,7 +374,8 @@ class DolbyDigitalPlusMetadata:
|
||||
|
||||
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
|
||||
def ext_bsi1_word2(b):
|
||||
return DolbyDigitalPlusMetadata.PreferredDownMixMode(b & 0xC0 >> 6), \
|
||||
return DolbyDigitalPlusMetadata\
|
||||
.PreferredDownMixMode(b & 0xC0 >> 6), \
|
||||
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
|
||||
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
|
||||
|
||||
@@ -423,14 +412,19 @@ class DolbyDigitalPlusMetadata:
|
||||
pid = program_id(buffer[0])
|
||||
lfe_on, bitstream_mode, audio_coding_mode = program_info(buffer[1])
|
||||
ddplus_reserved1(buffer[2:2])
|
||||
center_downmix_level, surround_downmix_level, dolby_surround_encoded = surround_config(buffer[4])
|
||||
langcode_present, copyright_bitstream, original_bitstream, dialnorm = dialnorm_info(buffer[5])
|
||||
center_downmix_level, surround_downmix_level, \
|
||||
dolby_surround_encoded = surround_config(buffer[4])
|
||||
langcode_present, copyright_bitstream, original_bitstream, \
|
||||
dialnorm = dialnorm_info(buffer[5])
|
||||
langcode = langcod(buffer[6])
|
||||
prod_info_exists, mixlevel, roomtype = audio_prod_info(buffer[7])
|
||||
|
||||
loro_center_downmix_level, loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
|
||||
downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9])
|
||||
surround_ex_mode, dolby_headphone_encoded, ad_converter_type = ext_bsi2_word1(buffer[10])
|
||||
loro_center_downmix_level, \
|
||||
loro_surround_downmix_level = ext_bsi1_word1(buffer[8])
|
||||
downmix_mode, ltrt_center_downmix_level, \
|
||||
ltrt_surround_downmix_level = ext_bsi1_word2(buffer[9])
|
||||
surround_ex_mode, dolby_headphone_encoded, \
|
||||
ad_converter_type = ext_bsi2_word1(buffer[10])
|
||||
|
||||
ddplus_reserved2(buffer[11:14])
|
||||
compression = compr1(buffer[14])
|
||||
@@ -441,8 +435,8 @@ class DolbyDigitalPlusMetadata:
|
||||
data_rate = datarate(buffer[25:27])
|
||||
reserved(buffer[27:69])
|
||||
|
||||
return DolbyDigitalPlusMetadata(program_id=pid,
|
||||
lfe_on=lfe_on,
|
||||
return DolbyDigitalPlusMetadata(
|
||||
program_id=pid, lfe_on=lfe_on,
|
||||
bitstream_mode=bitstream_mode,
|
||||
audio_coding_mode=audio_coding_mode,
|
||||
center_downmix_level=center_downmix_level,
|
||||
@@ -494,8 +488,10 @@ class DolbyAtmosMetadata:
|
||||
|
||||
@classmethod
|
||||
def load(cls, data: bytes):
|
||||
assert len(data) == cls.SEGMENT_LENGTH, "DolbyAtmosMetadata segment "\
|
||||
"is incorrect length, expected %i actual was %i" % (cls.SEGMENT_LENGTH, len(data))
|
||||
|
||||
assert len(data) == cls.SEGMENT_LENGTH
|
||||
# (f"DolbyAtmosMetadata segment is incorrect length, "
|
||||
# f"expected {cls.SEGMENT_LENGTH} actual was {len(data)}")
|
||||
|
||||
h = BytesIO(data)
|
||||
|
||||
@@ -513,7 +509,9 @@ class DolbyAtmosMetadata:
|
||||
warp_mode = a_val & 0x7
|
||||
|
||||
return DolbyAtmosMetadata(tool_name=toolname,
|
||||
tool_version=(major, minor, fix), warp_mode=DolbyAtmosMetadata.WarpMode(warp_mode))
|
||||
tool_version=(major, minor, fix),
|
||||
warp_mode=DolbyAtmosMetadata
|
||||
.WarpMode(warp_mode))
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -521,7 +519,8 @@ class DolbyAtmosSupplementalMetadata:
|
||||
"""
|
||||
Dolby Atmos supplemental metadata segment.
|
||||
|
||||
https://github.com/DolbyLaboratories/dbmd-atmos-parser/blob/master/dbmd_atmos_parse/src/dbmd_atmos_parse.c
|
||||
https://github.com/DolbyLaboratories/dbmd-atmos-parser/blob/
|
||||
master/dbmd_atmos_parse/src/dbmd_atmos_parse.c
|
||||
"""
|
||||
|
||||
class BinauralRenderMode(Enum):
|
||||
@@ -531,12 +530,10 @@ class DolbyAtmosSupplementalMetadata:
|
||||
MID = 0x03
|
||||
NOT_INDICATED = 0x04
|
||||
|
||||
|
||||
object_count: int
|
||||
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
|
||||
trim_modes: List[int]
|
||||
|
||||
|
||||
MAGIC = 0xf8726fbd
|
||||
TRIM_CONFIG_COUNT = 9
|
||||
|
||||
@@ -568,7 +565,8 @@ class DolbyAtmosSupplementalMetadata:
|
||||
render_modes.append(binaural_mode)
|
||||
|
||||
return DolbyAtmosSupplementalMetadata(object_count=object_count,
|
||||
render_modes=render_modes,trim_modes=trim_modes)
|
||||
render_modes=render_modes,
|
||||
trim_modes=trim_modes)
|
||||
|
||||
|
||||
class WavDolbyMetadataReader:
|
||||
@@ -597,7 +595,6 @@ class WavDolbyMetadataReader:
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
def __init__(self, dbmd_data):
|
||||
self.segment_list = []
|
||||
|
||||
@@ -617,7 +614,8 @@ class WavDolbyMetadataReader:
|
||||
else:
|
||||
seg_size = unpack("<H", h.read(2))[0]
|
||||
seg_payload = h.read(seg_size)
|
||||
expected_checksum = WavDolbyMetadataReader.segment_checksum(seg_payload, seg_size)
|
||||
expected_checksum = WavDolbyMetadataReader\
|
||||
.segment_checksum(seg_payload, seg_size)
|
||||
checksum = unpack("B", h.read(1))[0]
|
||||
|
||||
segment = seg_payload
|
||||
@@ -628,27 +626,28 @@ class WavDolbyMetadataReader:
|
||||
elif stype == SegmentType.DolbyAtmosSupplemental:
|
||||
segment = DolbyAtmosSupplementalMetadata.load(segment)
|
||||
|
||||
self.segment_list.append( (stype, checksum == expected_checksum, segment) )
|
||||
self.segment_list\
|
||||
.append((stype, checksum == expected_checksum, segment))
|
||||
|
||||
def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]:
|
||||
"""
|
||||
Every valid Dolby Digital Plus metadata segment in the file.
|
||||
"""
|
||||
return [x[2] for x in self.segment_list \
|
||||
return [x[2] for x in self.segment_list
|
||||
if x[0] == SegmentType.DolbyDigitalPlus and x[1]]
|
||||
|
||||
def dolby_atmos(self) -> List[DolbyAtmosMetadata]:
|
||||
"""
|
||||
Every valid Dolby Atmos metadata segment in the file.
|
||||
"""
|
||||
return [x[2] for x in self.segment_list \
|
||||
return [x[2] for x in self.segment_list
|
||||
if x[0] == SegmentType.DolbyAtmos and x[1]]
|
||||
|
||||
def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
||||
"""
|
||||
Every valid Dolby Atmos Supplemental metadata segment in the file.
|
||||
"""
|
||||
return [x[2] for x in self.segment_list \
|
||||
return [x[2] for x in self.segment_list
|
||||
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
|
||||
@@ -2,6 +2,7 @@ from .riff_parser import parse_chunk, ListChunkDescriptor
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class WavInfoChunkReader:
|
||||
|
||||
def __init__(self, f, encoding):
|
||||
@@ -9,10 +10,13 @@ class WavInfoChunkReader:
|
||||
|
||||
f.seek(0)
|
||||
parsed_chunks = parse_chunk(f)
|
||||
assert type(parsed_chunks) is ListChunkDescriptor
|
||||
|
||||
list_chunks = [chunk for chunk in parsed_chunks.children if type(chunk) is ListChunkDescriptor]
|
||||
list_chunks = [chunk for chunk in parsed_chunks.children
|
||||
if type(chunk) is ListChunkDescriptor]
|
||||
|
||||
self.info_chunk = next((chunk for chunk in list_chunks if chunk.signature == b'INFO'), None)
|
||||
self.info_chunk = next((chunk for chunk in list_chunks
|
||||
if chunk.signature == b'INFO'), None)
|
||||
|
||||
#: 'ICOP' Copyright
|
||||
self.copyright: Optional[str] = self._get_field(f, b'ICOP')
|
||||
@@ -49,7 +53,9 @@ class WavInfoChunkReader:
|
||||
self.commissioned: Optional[str] = self._get_field(f, b'ICMS')
|
||||
|
||||
def _get_field(self, f, field_ident) -> Optional[str]:
|
||||
search = next(((chunk.start, chunk.length) for chunk in self.info_chunk.children if chunk.ident == field_ident),
|
||||
search = next(((chunk.start, chunk.length)
|
||||
for chunk in self.info_chunk.children
|
||||
if chunk.ident == field_ident),
|
||||
None)
|
||||
|
||||
if search is not None:
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
from lxml import etree as ET
|
||||
import io
|
||||
from collections import namedtuple
|
||||
# from collections import namedtuple
|
||||
from typing import Optional
|
||||
from enum import IntEnum
|
||||
from typing import NamedTuple
|
||||
|
||||
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
|
||||
|
||||
class IXMLTrack(NamedTuple):
|
||||
channel_index: int
|
||||
interleave_index: int
|
||||
name: str
|
||||
function: str
|
||||
|
||||
|
||||
class SteinbergMetadata:
|
||||
@@ -72,7 +78,8 @@ class SteinbergMetadata:
|
||||
"""
|
||||
`AudioSpeakerArrangement` property
|
||||
"""
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE")
|
||||
val = self.parsed.find(
|
||||
"./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE")
|
||||
if val is not None:
|
||||
return type(self).AudioSpeakerArrangement(int(val.text))
|
||||
|
||||
@@ -81,7 +88,8 @@ class SteinbergMetadata:
|
||||
"""
|
||||
AudioSampleFormatSize
|
||||
"""
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
|
||||
val = self.parsed.find(
|
||||
"./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
|
||||
if val is not None:
|
||||
return int(val.text)
|
||||
|
||||
@@ -90,7 +98,8 @@ class SteinbergMetadata:
|
||||
"""
|
||||
MediaCompany
|
||||
"""
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
|
||||
val = self.parsed.find(
|
||||
"./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
|
||||
if val is not None:
|
||||
return val.text
|
||||
|
||||
@@ -99,7 +108,8 @@ class SteinbergMetadata:
|
||||
"""
|
||||
MediaDropFrames
|
||||
"""
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
|
||||
val = self.parsed.find(
|
||||
"./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
|
||||
if val is not None:
|
||||
return val.text == "1"
|
||||
|
||||
@@ -108,7 +118,8 @@ class SteinbergMetadata:
|
||||
"""
|
||||
MediaDuration
|
||||
"""
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
|
||||
val = self.parsed.find(
|
||||
"./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
|
||||
if val is not None:
|
||||
return float(val.text)
|
||||
|
||||
@@ -145,6 +156,7 @@ class WavIXMLFormat:
|
||||
"""
|
||||
iXML recorder metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, xml):
|
||||
"""
|
||||
Parse iXML.
|
||||
@@ -181,10 +193,13 @@ class WavIXMLFormat:
|
||||
"""
|
||||
for track in self.parsed.find("./TRACK_LIST").iter():
|
||||
if track.tag == 'TRACK':
|
||||
yield IXMLTrack(channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
|
||||
interleave_index=track.xpath('string(INTERLEAVE_INDEX/text())'),
|
||||
yield IXMLTrack(
|
||||
channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
|
||||
interleave_index=track.xpath(
|
||||
'string(INTERLEAVE_INDEX/text())'),
|
||||
name=track.xpath('string(NAME/text())'),
|
||||
function=track.xpath('string(FUNCTION/text())'))
|
||||
function=track.xpath('string(FUNCTION/text())')
|
||||
)
|
||||
|
||||
@property
|
||||
def project(self) -> Optional[str]:
|
||||
@@ -218,7 +233,8 @@ class WavIXMLFormat:
|
||||
def family_uid(self) -> Optional[str]:
|
||||
"""
|
||||
The globally-unique ID for this file family. This may be in the format
|
||||
of a GUID, or an EBU Rec 9 source identifier, or some other dumb number.
|
||||
of a GUID, or an EBU Rec 9 source identifier, or some other dumb
|
||||
number.
|
||||
"""
|
||||
return self._get_text_value("FILE_SET/FAMILY_UID")
|
||||
|
||||
@@ -240,11 +256,8 @@ class WavIXMLFormat:
|
||||
return None
|
||||
|
||||
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
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import struct
|
||||
import os
|
||||
from collections import namedtuple
|
||||
|
||||
from typing import Optional, Generator, Any
|
||||
from typing import Optional, Generator, Any, NamedTuple
|
||||
|
||||
import pathlib
|
||||
|
||||
@@ -16,12 +14,21 @@ from .wave_dbmd_reader import WavDolbyMetadataReader
|
||||
from .wave_cues_reader import WavCuesReader
|
||||
|
||||
#: Calculated statistics about the audio data.
|
||||
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
|
||||
|
||||
|
||||
class WavDataDescriptor(NamedTuple):
|
||||
byte_count: int
|
||||
frame_count: int
|
||||
|
||||
|
||||
#: The format of the audio samples.
|
||||
WavAudioFormat = namedtuple('WavAudioFormat',
|
||||
['audio_format', 'channel_count', 'sample_rate',
|
||||
'byte_rate', 'block_align', 'bits_per_sample'])
|
||||
class WavAudioFormat(NamedTuple):
|
||||
audio_format: int
|
||||
channel_count: int
|
||||
sample_rate: int
|
||||
byte_rate: int
|
||||
block_align: int
|
||||
bits_per_sample: int
|
||||
|
||||
|
||||
class WavInfoReader:
|
||||
@@ -29,7 +36,7 @@ class WavInfoReader:
|
||||
Parse a WAV audio file for metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
|
||||
def __init__(self, f, info_encoding='latin_1', bext_encoding='ascii'):
|
||||
"""
|
||||
Create a new reader object.
|
||||
|
||||
@@ -74,20 +81,20 @@ class WavInfoReader:
|
||||
#: RIFF cues markers, labels, and notes.
|
||||
self.cues: Optional[WavCuesReader] = None
|
||||
|
||||
if hasattr(path, 'read'):
|
||||
self.get_wav_info(path)
|
||||
if hasattr(f, 'read'):
|
||||
self.get_wav_info(f)
|
||||
self.url = 'about:blank'
|
||||
self.path = repr(path)
|
||||
self.path = repr(f)
|
||||
|
||||
else:
|
||||
absolute_path = os.path.abspath(path)
|
||||
absolute_path = os.path.abspath(f)
|
||||
|
||||
#: `file://` url for the file.
|
||||
self.url: str = pathlib.Path(absolute_path).as_uri()
|
||||
|
||||
self.path = absolute_path
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
with open(f, 'rb') as f:
|
||||
self.get_wav_info(f)
|
||||
|
||||
def get_wav_info(self, wavfile):
|
||||
@@ -106,9 +113,11 @@ class WavInfoReader:
|
||||
self.cues = self._get_cue(wavfile)
|
||||
self.data = self._describe_data()
|
||||
|
||||
def _find_chunk_data(self, ident, from_stream, default_none=False) -> Optional[bytes]:
|
||||
top_chunks = (chunk for chunk in self.main_list \
|
||||
if type(chunk) is ChunkDescriptor and chunk.ident == ident)
|
||||
def _find_chunk_data(self, ident, from_stream,
|
||||
default_none=False) -> Optional[bytes]:
|
||||
top_chunks = (chunk for chunk in self.main_list
|
||||
if type(chunk) is ChunkDescriptor and
|
||||
chunk.ident == ident)
|
||||
|
||||
chunk_descriptor = next(top_chunks, None) \
|
||||
if default_none else next(top_chunks)
|
||||
@@ -117,14 +126,14 @@ class WavInfoReader:
|
||||
if chunk_descriptor else None
|
||||
|
||||
def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]:
|
||||
top_chunks = (chunk for chunk in self.main_list \
|
||||
if type(chunk) is ListChunkDescriptor and \
|
||||
top_chunks = (chunk for chunk in self.main_list
|
||||
if type(chunk) is ListChunkDescriptor and
|
||||
chunk.signature == signature)
|
||||
|
||||
return next(top_chunks, None)
|
||||
|
||||
def _describe_data(self):
|
||||
data_chunk = next(c for c in self.main_list \
|
||||
data_chunk = next(c for c in self.main_list
|
||||
if type(c) is ChunkDescriptor and c.ident == b'data')
|
||||
|
||||
assert isinstance(self.fmt, WavAudioFormat)
|
||||
@@ -150,7 +159,7 @@ class WavInfoReader:
|
||||
)
|
||||
|
||||
def _get_info(self, f, encoding):
|
||||
finder = (chunk.signature for chunk in self.main_list \
|
||||
finder = (chunk.signature for chunk in self.main_list
|
||||
if type(chunk) is ListChunkDescriptor)
|
||||
|
||||
if b'INFO' in finder:
|
||||
@@ -176,8 +185,8 @@ class WavInfoReader:
|
||||
return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
|
||||
|
||||
def _get_cue(self, f):
|
||||
cue = next((cue_chunk for cue_chunk in self.main_list if \
|
||||
type(cue_chunk) is ChunkDescriptor and \
|
||||
cue = next((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')
|
||||
@@ -192,7 +201,8 @@ class WavInfoReader:
|
||||
return WavCuesReader.read_all(f, cue, labls, ltxts, notes,
|
||||
fallback_encoding=self.info_encoding)
|
||||
|
||||
def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()"
|
||||
# FIXME: this should probably be named "iter()"
|
||||
def walk(self) -> Generator[str, str, Any]:
|
||||
"""
|
||||
Walk all of the available metadata fields.
|
||||
|
||||
@@ -211,9 +221,12 @@ class WavInfoReader:
|
||||
yield scope, field, attr.__getattribute__(field)
|
||||
|
||||
else:
|
||||
dict = self.__getattribute__(scope).to_dict() if self.__getattribute__(scope) else {}
|
||||
dict = self.__getattribute__(scope).to_dict(
|
||||
) if self.__getattribute__(scope) else {}
|
||||
for key in dict.keys():
|
||||
yield scope, key, dict[key]
|
||||
|
||||
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)
|
||||
|
||||
@@ -9,10 +9,12 @@ import sys
|
||||
def main():
|
||||
parser = OptionParser()
|
||||
|
||||
parser.usage = "wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] <PATH> +"
|
||||
parser.usage = ("wavfind [--scene=SCENE] [--take=TAKE] [--desc=DESC] "
|
||||
"<PATH> +")
|
||||
|
||||
primaries = OptionGroup(parser, title="Search Predicates",
|
||||
description="Argument values can be globs, and are logically-AND'ed.")
|
||||
description="Argument values can be globs, "
|
||||
"and are logically-AND'ed.")
|
||||
|
||||
primaries.add_option("--scene",
|
||||
help='Search for this scene',
|
||||
@@ -26,7 +28,6 @@ def main():
|
||||
help='Search descriptions',
|
||||
metavar='DESC')
|
||||
|
||||
|
||||
(options, args) = parser.parse_args(sys.argv)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user