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 enum import IntEnum, Enum
from struct import unpack from struct import unpack
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Tuple, Any, Union from typing import List, Optional, Tuple, Any, Union
from io import BytesIO from io import BytesIO
@@ -20,11 +20,11 @@ class SegmentType(IntEnum):
""" """
EndMarker = 0x0 EndMarker = 0x0
DolbyE = 0x1 DolbyE = 0x1
Reserved2 = 0x2 # Reserved2 = 0x2
DolbyDigital = 0x3 DolbyDigital = 0x3
Reserved4 = 0x4 # Reserved4 = 0x4
Reserved5 = 0x5 # Reserved5 = 0x5
Reserved6 = 0x6 # Reserved6 = 0x6
DolbyDigitalPlus = 0x7 DolbyDigitalPlus = 0x7
AudioInfo = 0x8 AudioInfo = 0x8
DolbyAtmos = 0x9 DolbyAtmos = 0x9
@@ -351,14 +351,12 @@ class DolbyDigitalPlusMetadata:
datarate_kbps: int datarate_kbps: int
@staticmethod @staticmethod
def parse_dolby_digital_plus(buffer: bytes): def load(buffer: bytes):
assert len(buffer) == 96, "Dolby Digital Plus segment incorrect size, " assert len(buffer) == 96, "Dolby Digital Plus segment incorrect size, "
"expected 96 got %i" % len(buffer) "expected 96 got %i" % len(buffer)
retval = DolbyDigitalPlusMetadata()
def program_id(b) -> int: def program_id(b) -> int:
return unpack("<B",b) return b
def program_info(b): def program_info(b):
return (b & 0x40) > 0, \ return (b & 0x40) > 0, \
@@ -378,7 +376,7 @@ class DolbyDigitalPlusMetadata:
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f) DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
def langcod(b) -> int: def langcod(b) -> int:
return unpack("B", b) return b
def audio_prod_info(b): def audio_prod_info(b):
return (b & 0x80) > 0, \ return (b & 0x80) > 0, \
@@ -406,10 +404,10 @@ class DolbyDigitalPlusMetadata:
pass pass
def compr1(b): def compr1(b):
return DolbyDigitalPlusMetadata.RFCompressionProfile(unpack("B", b)) return DolbyDigitalPlusMetadata.RFCompressionProfile(b)
def dynrng1(b): def dynrng1(b):
DolbyDigitalPlusMetadata.RFCompressionProfile(unpack("B",b)) DolbyDigitalPlusMetadata.RFCompressionProfile(b)
def ddplus_reserved3(_): def ddplus_reserved3(_):
pass pass
@@ -421,7 +419,7 @@ class DolbyDigitalPlusMetadata:
pass pass
def datarate(b) -> int: def datarate(b) -> int:
return unpack("<H", b) return b
def reserved(_): def reserved(_):
pass pass
@@ -476,6 +474,72 @@ class DolbyDigitalPlusMetadata:
datarate_kbps=data_rate) 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: class WavDolbyMetadataReader:
""" """
Reads Dolby bitstream metadata. Reads Dolby bitstream metadata.
@@ -489,7 +553,19 @@ class WavDolbyMetadataReader:
#: not recognized). #: not recognized).
segment_list: Tuple[Union[SegmentType, int], bool, Any] 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: def __init__(self, dbmd_data) -> None:
self.segment_list = [] self.segment_list = []
@@ -499,13 +575,45 @@ class WavDolbyMetadataReader:
v_vec = [] v_vec = []
for _ in range(4): for _ in range(4):
b = h.read(1) 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]]