Dolby metadata

This commit is contained in:
Jamie Hardt
2022-11-25 12:26:09 -08:00
parent 4109f77372
commit bdf5fc9349
2 changed files with 180 additions and 19 deletions

53
tests/test_dolby.py Normal file
View File

@@ -0,0 +1,53 @@
from unittest import TestCase
import wavinfo
from wavinfo.wave_dbmd_reader import SegmentType, DolbyAtmosMetadata, DolbyDigitalPlusMetadata
class TestDolby(TestCase):
def setUp(self):
self.test_file = "tests/test_files/protools/Test_ADM_ProTools.wav"
def test_version(self):
t1 = wavinfo.WavInfoReader(self.test_file)
d = t1.dolby
self.assertEqual((1,0,0,6), d.version)
def test_segments(self):
t1 = wavinfo.WavInfoReader(self.test_file)
d = t1.dolby
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]
atmos_sup = [x for x in d.segment_list if x[0] == SegmentType.DolbyAtmosSupplemental]
self.assertEqual(len(ddp), 1)
self.assertEqual(len(atmos), 1)
self.assertEqual(len(atmos_sup), 1)
def test_checksums(self):
t1 = wavinfo.WavInfoReader(self.test_file)
d = t1.dolby
for seg in d.segment_list:
self.assertTrue(seg[1])
def test_ddp(self):
t1 = wavinfo.WavInfoReader(self.test_file)
d = t1.dolby
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.assertTrue( ddp[0].lfe_on)
def test_atmos(self):
t1 = wavinfo.WavInfoReader(self.test_file)
d = t1.dolby
atmos = d.dolby_atmos()
self.assertEqual(len(atmos), 1, "Failed to find exactly one Atmos metadata segment")

View File

@@ -10,7 +10,7 @@ Unless otherwise stated, all § references here are to
from enum import IntEnum, Enum
from struct import unpack
from dataclasses import dataclass
from typing import Optional, Tuple, Any, Union
from typing import List, Optional, Tuple, Any, Union
from io import BytesIO
@@ -20,11 +20,11 @@ class SegmentType(IntEnum):
"""
EndMarker = 0x0
DolbyE = 0x1
Reserved2 = 0x2
# Reserved2 = 0x2
DolbyDigital = 0x3
Reserved4 = 0x4
Reserved5 = 0x5
Reserved6 = 0x6
# Reserved4 = 0x4
# Reserved5 = 0x5
# Reserved6 = 0x6
DolbyDigitalPlus = 0x7
AudioInfo = 0x8
DolbyAtmos = 0x9
@@ -351,14 +351,12 @@ class DolbyDigitalPlusMetadata:
datarate_kbps: int
@staticmethod
def parse_dolby_digital_plus(buffer: bytes):
def load(buffer: bytes):
assert len(buffer) == 96, "Dolby Digital Plus segment incorrect size, "
"expected 96 got %i" % len(buffer)
retval = DolbyDigitalPlusMetadata()
def program_id(b) -> int:
return unpack("<B",b)
return b
def program_info(b):
return (b & 0x40) > 0, \
@@ -378,7 +376,7 @@ class DolbyDigitalPlusMetadata:
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
def langcod(b) -> int:
return unpack("B", b)
return b
def audio_prod_info(b):
return (b & 0x80) > 0, \
@@ -406,10 +404,10 @@ class DolbyDigitalPlusMetadata:
pass
def compr1(b):
return DolbyDigitalPlusMetadata.RFCompressionProfile(unpack("B", b))
return DolbyDigitalPlusMetadata.RFCompressionProfile(b)
def dynrng1(b):
DolbyDigitalPlusMetadata.RFCompressionProfile(unpack("B",b))
DolbyDigitalPlusMetadata.RFCompressionProfile(b)
def ddplus_reserved3(_):
pass
@@ -421,7 +419,7 @@ class DolbyDigitalPlusMetadata:
pass
def datarate(b) -> int:
return unpack("<H", b)
return b
def reserved(_):
pass
@@ -476,6 +474,72 @@ class DolbyDigitalPlusMetadata:
datarate_kbps=data_rate)
@dataclass
class DolbyAtmosMetadata:
"""
https://github.com/DolbyLaboratories/dbmd-atmos-parser/
"""
class WarpMode(IntEnum):
NORMAL = 0x00
WARPING = 0x01
DOWNMIX_PLIIX = 0x02
DOWNMIX_LORO = 0x03
NOT_INDICATED = 0x04
tool_name: str
tool_version: Tuple[int,int,int]
warp_mode: WarpMode
SEGMENT_LENGTH = 248
TOOL_NAME_LENGTH = 64
@classmethod
def load(cls, data: bytes):
assert(len(data) == cls.SEGMENT_LENGTH,
"DolbyAtmosMetadata segment present in file is incorrect length")
h = BytesIO(data)
h.seek(32, 1)
toolname = h.read(cls.TOOL_NAME_LENGTH)
toolname = unpack("%is" % cls.TOOL_NAME_LENGTH, toolname)[0]
toolname = toolname.decode('utf-8').strip('\0')
vers = h.read(3)
major, minor, fix = unpack("BBB", vers)
h.seek(53, 1)
a_val = unpack("B", h.read(1))[0]
warp_mode = a_val & 0x7
return DolbyAtmosMetadata(tool_name=toolname,
tool_version=(major, minor, fix), warp_mode=DolbyAtmosMetadata.WarpMode(warp_mode))
@dataclass
class DolbyAtmosSupplementalMetadata:
class BinauralRenderMode(Enum):
BYPASS = 0x00
NEAR = 0x01
FAR = 0x02
MID = 0x03
NOT_INDICATED = 0x04
object_count: int
render_modes: List['DolbyAtmosMetadata.BinauralRenderMode']
trim_modes: List[int]
class WavDolbyMetadataReader:
"""
Reads Dolby bitstream metadata.
@@ -489,7 +553,19 @@ class WavDolbyMetadataReader:
#: not recognized).
segment_list: Tuple[Union[SegmentType, int], bool, Any]
version: str
version: Tuple[int,int,int,int]
@staticmethod
def segment_checksum(bs: bytes, size: int):
retval = size
for b in bs:
retval += int(b)
retval &= 0xff
retval = ((~retval) + 1) & 0xff
return retval
def __init__(self, dbmd_data) -> None:
self.segment_list = []
@@ -499,13 +575,45 @@ class WavDolbyMetadataReader:
v_vec = []
for _ in range(4):
b = h.read(1)
v_vec.insert(0, str(unpack("B", b[0:1])))
v_vec.insert(0, unpack("B",b)[0])
self.version = ".".join(v_vec)
self.version = tuple(v_vec)
while True:
stype= SegmentType(unpack("B", h.read(1))[0])
if stype == SegmentType.EndMarker:
break
else:
seg_size = unpack("<H", h.read(2))[0]
seg_payload = h.read(seg_size)
expected_checksum = WavDolbyMetadataReader.segment_checksum(seg_payload, seg_size)
checksum = unpack("B", h.read(1))[0]
segment = seg_payload
if stype == SegmentType.DolbyDigitalPlus:
segment = DolbyDigitalPlusMetadata.load(segment)
elif stype == SegmentType.DolbyAtmos:
segment = DolbyAtmosMetadata.load(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 \
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 \
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 \
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]