35 Commits

Author SHA1 Message Date
Jamie Hardt
f8ce7b9ad9 Changed indexing of tracks 2022-11-26 19:03:18 -08:00
Jamie Hardt
00728f5af3 ADM docs 2022-11-26 18:47:00 -08:00
Jamie Hardt
4e061a85f1 Renamed iter back to walk 2022-11-26 18:26:58 -08:00
Jamie Hardt
af5c83b8fc Documentation 2022-11-26 18:25:12 -08:00
Jamie Hardt
a7f77a49f7 Update README.md 2022-11-26 13:50:25 -08:00
Jamie Hardt
0acbe58f0b ADM metadata 2022-11-26 13:39:55 -08:00
Jamie Hardt
8d908a3e34 More ADM metadata 2022-11-26 12:15:11 -08:00
Jamie Hardt
c897d080bb Dolby
Removed supplemental metadata for
now
2022-11-26 11:42:37 -08:00
Jamie Hardt
710473f2aa Update wave_dbmd_reader.py 2022-11-25 13:25:06 -08:00
Jamie Hardt
cf48763b13 Update README.md 2022-11-25 13:23:29 -08:00
Jamie Hardt
5651367df7 Update wave_dbmd_reader.py 2022-11-25 13:23:03 -08:00
Jamie Hardt
6d7373391e Updated README 2022-11-25 13:08:51 -08:00
Jamie Hardt
ff60f26f78 More Dolby metadata support 2022-11-25 13:03:33 -08:00
Jamie Hardt
cc49df8f08 Bext metadata 2022-11-25 12:26:16 -08:00
Jamie Hardt
bdf5fc9349 Dolby metadata 2022-11-25 12:26:09 -08:00
Jamie Hardt
4109f77372 Update README.md 2022-11-24 23:20:46 -08:00
Jamie Hardt
7b9b64d799 Update CONTRIBUTING.md 2022-11-24 23:16:19 -08:00
Jamie Hardt
e5cd098d44 Create CONTRIBUTING.md 2022-11-24 23:15:47 -08:00
Jamie Hardt
957b23db92 Update wave_dbmd_reader.py 2022-11-24 23:04:35 -08:00
Jamie Hardt
733113819e Docs 2022-11-24 22:52:33 -08:00
Jamie Hardt
df4cc8822e Docs 2022-11-24 22:34:49 -08:00
Jamie Hardt
d5b6f15e28 Docs 2022-11-24 22:21:03 -08:00
Jamie Hardt
b830b8cdc2 Fixed shameful typo 2022-11-24 22:08:28 -08:00
Jamie Hardt
b23470ac19 Docs 2022-11-24 22:07:19 -08:00
Jamie Hardt
8fe7eefb4a Docs 2022-11-24 22:06:44 -08:00
Jamie Hardt
f0b7a0ddf6 Nudge version 2022-11-24 22:05:38 -08:00
Jamie Hardt
e83603cb47 Dolby metadata integration 2022-11-24 22:05:31 -08:00
Jamie Hardt
b6acdb1f7f More documentation 2022-11-24 21:24:59 -08:00
Jamie Hardt
faf809b8e2 Tweaking docs 2022-11-24 20:33:16 -08:00
Jamie Hardt
f7a1896f99 dbmd doc and implementation 2022-11-24 20:28:41 -08:00
Jamie Hardt
40aee91162 Documentation 2022-11-23 22:49:38 -08:00
Jamie Hardt
9f8fc87d17 Bext documentation 2022-11-23 22:44:24 -08:00
Jamie Hardt
b2323a126f Docs 2022-11-23 22:31:42 -08:00
Jamie Hardt
8fcc9787f6 Fixed typo in include 2022-11-23 19:11:57 -08:00
Jamie Hardt
52ea6fdb60 Delete metadata.py 2022-11-23 19:10:48 -08:00
24 changed files with 1116 additions and 282 deletions

15
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,15 @@
# Contributing
Contributions to this project are very welcome!
If you discover a bug or would like better support for a feature, please do the following:
1. Submit an Issue.
I'm actively developing this project and will review incoming issues.
1. Check out the source code and submit a PR.
If you're facile with Python and understand what you'd like to fix, submit a PR and I'll
review it as soon as I can. There's a `.devcontainer` available so you can creates commits
on this project in a GitHub codespace.

View File

@@ -11,8 +11,9 @@ The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] an
`wavinfo` reads:
* [__Broadcast-WAVE__][ebu] metadata, including embedded program
loudness and coding history and [__SMPTE UMID__][smpte_330m2011].
* [__ADM__][adm] track metadata, including channel, pack formats, object and content names.
loudness, coding history and [__SMPTE UMID__][smpte_330m2011].
* [__ADM__][adm] track metadata and schema, including channel, pack formats, object, content and programme.
* [__Dolby Digital Plus__][ebu3285s6] and Dolby Atmos `dbmd` metadata.
* [__iXML__][ixml] production recorder metadata, including project, scene, and take tags, recorder notes
and file family information.
* Most of the common [__RIFF INFO__][info-tags] metadata fields.
@@ -20,11 +21,9 @@ The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] an
information.
In progress:
* [__Dolby RMU__][dolby] metadata and [EBU Tech 3285 Supplement 6][ebu3285s6].
* Pro Tools __embedded regions__.
* iXML `STEINBERG` sound library attributes.
[dolby]:https://developer.dolby.com/globalassets/documentation/technology/dolby_atmos_master_adm_profile_v1.0.pdf
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
[ebu3285s6]:https://tech.ebu.ch/docs/tech/tech3285s6.pdf
[adm]:https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2076-2-201910-I!!PDF-E.pdf

View File

@@ -1,18 +1,10 @@
Other wavinfo Classes
===============
=====================
.. autoclass:: wavinfo.wave_reader.WavInfoReader
:members:
.. automethod:: __init__
.. autoclass:: wavinfo.wave_reader.WavAudioFormat
:members:
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
:members:

View File

@@ -0,0 +1,94 @@
Using `wavinfo` from the Command Line
=====================================
`wavinfo` installs a command-line entry point that will read wav files
from the command line and output metadata to stdout.
.. code-block:: shell
$ wavinfo [--ixml | --adm] INFILE +
By default, `wavinfo` will output a JSON dictionary for each file argument.
Options
-------
Two option flags will change the behavior of the command:
``--ixml``
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload
of each input wave file, or will emit an error message to stderr if iXML
metadata is not present.
``--adm``
The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata
payload of each input wave file, or will emit an error message to stderr if
ADM XML metadata is not present.
These options are mutually-exclusive, with `\-\-adm` taking precedence.
Example Output
--------------
.. code-block:: javascript
{
"filename": "tests/test_files/sounddevices/A101_1.WAV",
"run_date": "2022-11-26T17:56:38.342935",
"application": "wavinfo 2.1.0",
"scopes": {
"fmt": {
"audio_format": 1,
"channel_count": 2,
"sample_rate": 48000,
"byte_rate": 288000,
"block_align": 6,
"bits_per_sample": 24
},
"data": {
"byte_count": 1441434,
"frame_count": 240239
},
"ixml": {
"track_list": [
{
"channel_index": "1",
"interleave_index": "1",
"name": "MKH516 A",
"function": ""
},
{
"channel_index": "2",
"interleave_index": "2",
"name": "Boom",
"function": ""
}
],
"project": "BMH",
"scene": "A101",
"take": "1",
"tape": "18Y12M31",
"family_uid": "USSDVGR1112089007124001008206300",
"family_name": null
},
"bext": {
"description": "sSPEED=023.976-ND\r\nsTAKE=1\r\nsUBITS=$12311801\r\nsSWVER=2.67\r\nsPROJECT=BMH\r\nsSCENE=A101\r\nsFILENAME=A101_1.WAV\r\nsTAPE=18Y12M31\r\nsTRK1=MKH516 A\r\nsTRK2=Boom\r\nsNOTE=\r\n",
"originator": "Sound Dev: 702T S#GR1112089007",
"originator_ref": "USSDVGR1112089007124001008206301",
"originator_date": "2018-12-31",
"originator_time": "12:40:00",
"time_reference": 2190940753,
"version": 1,
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
"loudness_value": null,
"loudness_range": null,
"max_true_peak": null,
"max_momentary_loudness": null,
"max_shortterm_loudness": null
}
}
}

View File

@@ -6,15 +6,19 @@
Welcome to wavinfo's documentation!
===================================
The `wavinfo` package allows you to probe WAVE and RF64/WAVE files and
extract extended metadata, with an emphasis on film, video and professional
music production metadata.
.. toctree::
:maxdepth: 2
:caption: Notes
:maxdepth: 1
:glob:
:numbered:
quickstart
metadata_scopes/adm.rst
metadata_scopes/bext.rst
metadata_scopes/info.rst
metadata_scopes/ixml.rst
command_line
scopes/*
classes

View File

@@ -1,15 +0,0 @@
ADM (Audio Definition Model) Metadata
=====================================
Notes
-----
Class Reference
---------------
.. module:: wavinfo
.. autoclass:: wavinfo.wave_adm_reader.WavADMReader
:members:

View File

@@ -1,6 +1,11 @@
ptulsconv Quickstart
wavinfo Quickstart
====================
All metadata is read by an instance of :class:`WaveInfoReader<wavinfo.wave_reader.WavInfoReader>`.
Each type of metadata, iXML, Broadcast-WAV etc. is accessible through *scopes*, properties on an
instance of :class:`WaveInfoReader`.
.. code-block:: python
:caption: Using wavinfo
@@ -10,3 +15,10 @@ ptulsconv Quickstart
info = wavinfo.WavInfoReader(path)
.. module:: wavinfo
:noindex:
.. autoclass:: wavinfo.wave_reader.WavInfoReader
:members:

View File

@@ -0,0 +1,30 @@
ADM (Audio Definition Model) Metadata
=====================================
Notes
-----
`ADM metadata`_ is used in master recordings to describe the format and content
of the tracks. In practice on wave files, ADM tells a client which tracks are
members of multichannel stems or "beds" and their speaker assignment, and which
tracks are freely-positioned 3D objects. ADM also records the panning moves on
object tracks and their content group ("Dialogue", "Music", "Effects" etc.)
ADM wave files created with a Dolby Rendering and Mastering Unit are a common
deliverable in feature film and television production. The `Dolby Atmos ADM Profile`_
describes how the RMU translates its native Master format into ADM.
.. _ADM metadata: https://adm.ebu.io
.. _Dolby Atmos ADM Profile: https://developer.dolby.com/globalassets/documentation/technology/dolby_atmos_master_adm_profile_v1.0.pdf
Class Reference
---------------
.. module:: wavinfo
.. autoclass:: wavinfo.wave_adm_reader.WavADMReader
:members:
.. autoclass:: wavinfo.wave_adm_reader.ChannelEntry
:members:

View File

@@ -1,5 +1,5 @@
Broadcast WAV Extension
=======================
Broadcast WAV Extension Metadata
================================
Notes
@@ -9,20 +9,26 @@ which includes a 256-character free text descrption, creating entity identifier
recording application or equipment), the date and time of recording and a time reference for
timecode synchronization.
The `coding_history` is designed to contain a record of every conversion performed on the audio
file.
The :py:attr:`coding_history<wavinfo.wave_bext_reader.WavBextReader.coding_history>`
is designed to contain a record of every conversion performed on the audio file.
In this example (from a Sound Devices 702T) the bext metadata contains scene/take slating
information in the `description`. Here also the `originator_ref` is a serial number conforming
to EBU Rec 99.
information in the :py:attr:`description<wavinfo.wave_bext_reader.WavBextReader.description>`.
Here also the :py:attr:`originator_ref<wavinfo.wave_bext_reader.WavBextReader.originator_ref>`
is a serial number conforming to EBU Rec 99.
If the bext metadata conforms to EBU 3285 v1, it will contain the WAV's 32 or 64 byte SMPTE
330M UMID. The 32-byte version of the UMID is usually just a random number, while the 64-byte
If the bext metadata conforms to `EBU 3285 v1`_, it will contain the WAV's 32 or 64 byte `SMPTE
ST 330 UMID`_. The 32-byte version of the UMID is usually just a random number, while the 64-byte
UMID will also have information on the recording date and time, recording equipment and entity,
and geolocation data.
If the bext metadata conforms to EBU 3285 v2, it will hold precomputed program loudness values
as described by EBU Rec 128.
If the bext metadata conforms to `EBU 3285 v2`_, it will hold precomputed program loudness values
as described by `EBU Rec 128`_.
.. _EBU 3285 v1: https://tech.ebu.ch/publications/tech3285s1
.. _SMPTE ST 330 UMID: https://standards.globalspec.com/std/1396751/smpte-st-330
.. _EBU 3285 v2: https://tech.ebu.ch/publications/tech3285s2
.. _EBU Rec 128: https://tech.ebu.ch/publications/r128
.. code:: python

View File

@@ -0,0 +1,21 @@
Dolby Metadata
==============
Notes
-----
Dolby software and equipment creates detailed hinting metadata that can help
receiving applications decide how to present the audio content, particularly
how it should be downmixed, and dialogue normalization settings.
Class Reference
---------------
.. automodule:: wavinfo.wave_dbmd_reader
.. autoclass:: wavinfo.wave_dbmd_reader.WavDolbyMetadataReader
:members:
.. autoclass:: wavinfo.wave_dbmd_reader.DolbyDigitalPlusMetadata
:members:

View File

@@ -20,6 +20,17 @@ music library software.
print("INFO Comment:", bullet.info.comment)
On Encodings
""""""""""""
According to Microsoft, the original developers of the RIFF file and RIFF INFO
metadata, these fields are always to be interpreted as ISO Latin 1 characters,
and this is the default encoding used by `wavinfo` for these fields. You can
select a different encoding (like Shift-JIS) by passing an encoding name (as
would be used by `string.encode()`) to `WavInfoReader.__init__()`'s
`info_encoding=` parameter.
Class Reference
---------------

View File

@@ -1,3 +0,0 @@
"""
Wavinfo
"""

View File

@@ -1,5 +1,5 @@
from setuptools import setup
from .wavinfo import __author__, __license__, __version__
from wavinfo import __author__, __license__, __version__
with open("README.md", "r") as fh:
long_description = fh.read()

View File

@@ -23,6 +23,17 @@ class TestADMWave(TestCase):
dict = adm.to_dict()
self.assertIsNotNone(dict)
def test_programme(self):
info = wavinfo.WavInfoReader(self.protools_adm_wav)
adm = info.adm
pdict = adm.programme()
self.assertIn("programme_id", pdict.keys())
self.assertIn("programme_name", pdict.keys())
self.assertEqual(pdict['programme_id'], 'APR_1001')
self.assertEqual(pdict['programme_name'], 'Atmos_Master')
self.assertIn("contents", pdict.keys())
self.assertEqual(len(pdict["contents"]), 3)
def test_track_info(self):
info = wavinfo.WavInfoReader(self.protools_adm_wav)
adm = info.adm

49
tests/test_dolby.py Normal file
View File

@@ -0,0 +1,49 @@
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]
self.assertEqual(len(ddp), 1)
self.assertEqual(len(atmos), 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

@@ -1,12 +1,12 @@
"""
methods to probe a WAV file for various kinds of production metadata.
Go to the documentation for wavinfo.WavInfoReader for more information.
See the documentation for `wavinfo.WavInfoReader` for more information.
"""
from .wave_reader import WavInfoReader
from .riff_parser import WavInfoEOFError
__version__ = '2.0.1'
__version__ = '2.1.0'
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
__license__ = "MIT"

View File

@@ -1,33 +1,69 @@
from optparse import OptionParser, OptionGroup
import datetime
from . import WavInfoReader
from . import __version__
import sys
import json
from enum import Enum
class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Enum):
return o._name_
else:
return super().default(o)
class MissingDataError(RuntimeError):
pass
def main():
parser = OptionParser()
parser.usage = 'wavinfo [FILE.wav]*'
parser.usage = 'wavinfo (--adm | --ixml) [FILES]'
# 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('--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)
ret_dict = {'file_argument': arg, 'run_date': datetime.datetime.now().isoformat() , 'scopes': {}}
for scope, name, value in this_file.walk():
if scope not in ret_dict['scopes'].keys():
ret_dict['scopes'][scope] = {}
if options.adm:
if this_file.adm:
sys.stdout.write(this_file.adm.xml_str())
else:
raise MissingDataError("adm")
elif options.ixml:
if this_file.ixml:
sys.stdout.write(this_file.ixml.xml_bytes())
else:
raise MissingDataError("ixml")
else:
ret_dict = {
'filename': arg,
'run_date': datetime.datetime.now().isoformat() ,
'application': "wavinfo " + __version__,
'scopes': {}
}
for scope, name, value in this_file.walk():
if scope not in ret_dict['scopes'].keys():
ret_dict['scopes'][scope] = {}
ret_dict['scopes'][scope][name] = value
ret_dict['scopes'][scope][name] = value
json.dump(ret_dict, fp=sys.stdout, indent=2)
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)
continue
except Exception as e:
print(e)
raise e
if __name__ == "__main__":

View File

@@ -1,188 +0,0 @@
# Dolby RMU Metadata per EBU Tech 3285 Supp 6
#
# https://tech.ebu.ch/docs/tech/tech3285s6.pdf
#
from struct import unpack, calcsize
from enum import (Enum, IntEnum)
CHUNK_IDENT = "dbmd"
DOLBY_VERSION = "1.0.0.6"
class _DPPGenericDownMixLevel(Enum):
PLUS_3DB = 0b000
PLUS_1_5DB = 0b001
UNITY = 0b010
MINUS_1_5DB = 0b011
MINUS_3DB = 0b100
MINUS_4_5DB = 0b101
MINUS_6DB = 0b110
MUTE = 0b111
class DPPDolbySurroundEncodingMode(Enum):
RESERVED = 0b11
IN_USE = 0b10
NOT_IN_USE = 0b01
NOT_INDICATED = 0b00
# DPPLoRoDownMixCenterLevel
# DPPLtRtCenterMixLevel
# DPPLtRtSurroundMixLevel
class DolbyMetadataSegmentTypes(IntEnum):
END_MARKER = 0
DOLBY_E_METADATA = 1
DOLBY_DIGITAL_METADATA = 3
DOLBY_DIGITAL_PLUS_METADATA = 7
AUDIO_INFO = 8
class DDPBitStreamMode(Enum):
"""
Dolby Digital Plus `bsmod` field
§ 4.3.2.2
"""
COMPLETE_MAIN = 0b000
MUSIC_AND_EFFECTS = 0b001
VISUALLY_IMPAIRED = 0b010
HEARING_IMPAIRED = 0b011
DIALOGUE_ONLY = 0b100
COMMENTARY = 0b101
EMERGENCY = 0b110
VOICEOVER = 0b111 # if audioconfigmode is 1_0
KARAOKE = 0b1000 # if audioconfigmode is not 1_0
class DDPAudioCodingMode(Enum):
"""
Dolby Digital Plus `acmod` field
§ 4.3.2.3
"""
RESERVED = 0b000
CH_ORD_1_0 = 0b001
CH_ORD_2_0 = 0b010
CH_ORD_3_0 = 0b011
CH_ORD_2_1 = 0b100
CH_ORD_3_1 = 0b101
CH_ORD_2_2 = 0b110
CH_ORD_3_2 = 0b111
class DPPCenterDownMixLevel(Enum):
"""
§ 4.3.3.1
"""
DOWN_3DB = 0b00
DOWN_45DB = 0b01
DOWN_6DB = 0b10
RESERVED = 0b11
class DPPSurroundDownMixLevel(Enum):
"""
Dolby Digital Plus `surmixlev` field
§ 4.3.3.2
"""
DOWN_3DB = 0b00
DOWN_6DB = 0b01
MUTE = 0b10
RESERVED = 0b11
class DPPLanguageCode(Enum):
"""
§ 4.3.4.1 , 4.3.5 (always 0xFF)
"""
# this is removed in https://www.atsc.org/wp-content/uploads/2015/03/A52-201212-17.pdf § 5.4.2.12
# It should just be 0xff
pass
class DPPMixLevel(int):
pass
class DPPDialnormLevel(int):
pass
class DPPRoomTime(Enum):
"""
`roomtyp` 4.3.6.3
"""
NOT_INDICATED = 0b00
LARGE_ROOM_X_CURVE = 0b01
SMALL_ROOM_FLAT_CURVE = 0b10
RESERVED = 0b11
class DPPPreferredDownMixMode(Enum):
"""
§ 4.3.8.1
"""
NOT_INDICATED = 0b00
PRO_LOGIC = 0b01
STEREO = 0b10
PRO_LOGIC_2 = 0b11
# class DPPLtRtCenterMixLevel(_DPPGenericDownMixLevel):
# pass
#
#
# class DPPLtRtSurroundMixLevel(_DPPGenericDownMixLevel):
# pass
#
#
# class DPPSurroundEXMode(_DPPGenericInUseIndicator):
# pass
#
#
# class DPPHeadphoneMode(_DPPGenericInUseIndicator):
# pass
class DPPADConverterType(Enum):
STANDARD = 0
HDCD = 1
class DDPStreamDependency(Enum):
"""
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
"""
INDEPENDENT = 0
DEPENDENT = 1
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
RESERVED = 3
class DDPDataRate(int):
pass
class DPPRFCompressionProfile(Enum):
NONE = 0
FILM_STANDARD = 1
FILM_LIGHT = 2
MUSIC_STANDARD = 3
MUSIC_LIGHT = 4
SPEECH = 5
class DolbyDigitalPlusMetadata:
@classmethod
def parse(cls, binary_data):
binary_format = "<BBxxBBBBBBBBBBBBBxxxBxxxxxH"
assert len(binary_data >= calcsize(binary_format))
fields = unpack(binary_format, binary_data)
class WavDolbyReader:
def __init__(self, dolby_data):
version, remainder = unpack("<U", dolby_data[0]), dolby_data[1:]
## FIXME continues...

View File

@@ -14,6 +14,7 @@ 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):
@@ -26,7 +27,12 @@ class WavADMReader:
_, uid_count = unpack(header_fmt, chna_data[0:4])
#: A list of :class:`ChannelEntry` objects parsed from the
#: `chna` metadata chunk.
#: `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)
@@ -36,21 +42,68 @@ class WavADMReader:
# these values are either ascii or all null
self.channel_uids.append(ChannelEntry(track_index,
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:
"""
Extract 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):
"""
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
: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 + 1), None)
channel_info = next((x for x in self.channel_uids if x.track_index == index), None)
if channel_info is None:
return None
@@ -59,31 +112,54 @@ class WavADMReader:
nsmap = self.axml.getroot().nsmap
trackformat_elem = self.axml.find(".//audioFormatExtended/audioTrackFormat[@audioTrackFormatID='%s']" % channel_info.track_ref, namespaces=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 = self.axml.find(".//audioFormatExtended/audioStreamFormat[@audioStreamFormatID='%s']/audioChannelFormatIDRef" % stream_id, namespaces=nsmap)
channelformatref_elem = afext.find("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)
packformatref_elem = afext.find("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)
channelformat_elem = afext.find("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)
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 = self.axml.find(".//audioFormatExtended/audioObject[audioPackFormatIDRef = '%s']" % packformat_id, namespaces=nsmap)
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)
content_elem = self.axml.find(".//audioFormatExtended/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):
return dict(channel_entries=list(map(lambda z: z._asdict(), self.channel_uids)))
"""
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())

View File

@@ -17,6 +17,7 @@ class WavBextReader:
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)
trimmed = b if first_null is None else b[:first_null]
decoded = trimmed.decode(encoding)
@@ -29,9 +30,9 @@ class WavBextReader:
self.originator : str = sanitize_bytes(unpacked[1])
#: A unique identifier for the file, a serial number.
self.originator_ref : str = sanitize_bytes(unpacked[2])
#: Date of the recording, in the format YYY-MM-DD
#: Date of the recording, in the format YYYY-MM-DD in the local calendar
self.originator_date : str = sanitize_bytes(unpacked[3])
#: Time of the recording, in the format HH:MM:SS.
#: Time of the recording, in the format HH:MM:SS on the local clock
self.originator_time : str = sanitize_bytes(unpacked[4])
#: The sample offset of the start of the file relative to an
#: epoch, usually midnight the day of the recording.

661
wavinfo/wave_dbmd_reader.py Normal file
View File

@@ -0,0 +1,661 @@
"""
Reading Dolby Bitstream Metadata
Unless otherwise stated, all § references here are to
`EBU Tech 3285 Supplement 6`_.
.. _EBU Tech 3285 Supplement 6: https://tech.ebu.ch/docs/tech/tech3285s6.pdf
"""
from enum import IntEnum, Enum
from struct import unpack
from dataclasses import dataclass, asdict
from typing import List, Optional, Tuple, Any, Union
from io import BytesIO
class SegmentType(IntEnum):
"""
Metadata segment type.
"""
EndMarker = 0x0
DolbyE = 0x1
# Reserved2 = 0x2
DolbyDigital = 0x3
# Reserved4 = 0x4
# Reserved5 = 0x5
# Reserved6 = 0x6
DolbyDigitalPlus = 0x7
AudioInfo = 0x8
DolbyAtmos = 0x9
DolbyAtmosSupplemental = 0xa
@classmethod
def _missing_(cls,val):
return val
@dataclass
class DolbyDigitalPlusMetadata:
"""
*Dolby Digital Plus* is Dolby's brand for multichannel surround
on discrete formats that aren't AC-3 (Dolby Digital) or Dolby E. This
metadata segment is present in ADM wave files created with a Dolby Atmos
Production Suite.
Where an AC-3 bitstream can contain multiple programs, a Dolby Digital
Plus bitstream will only contain one program.
"""
class DownMixLevelToken(Enum):
"""
A gain coefficient used in several metadata fields for downmix
scenarios.
"""
PLUS_3DB = 0b000
"+3 dB"
PLUS_1_5DB = 0b001
"+1.5 dB"
UNITY = 0b010
"0dB"
MINUS_1_5DB = 0b011
"-1.5 dB"
MINUS_3DB = 0b100
"-3 dB"
MINUS_4_5DB = 0b101
"-4.5 dB"
MINUS_6DB = 0b110
"-6 dB"
MUTE = 0b111
"-∞ dB"
class DolbySurroundEncodingMode(Enum):
"""
Dolby surround endcoding mode.
"""
RESERVED = 0b11
IN_USE = 0b10
NOT_IN_USE = 0b01
NOT_INDICATED = 0b00
class BitStreamMode(Enum):
"""
Dolby Digital Plus `bsmod` field
§ 4.3.2.2
"""
COMPLETE_MAIN = 0b000
"main audio service: complete main"
MUSIC_AND_EFFECTS = 0b001
"main audio service: music and effects"
VISUALLY_IMPAIRED = 0b010
"associated service: visually impaired"
HEARING_IMPAIRED = 0b011
"associated service: hearing impaired"
DIALOGUE_ONLY = 0b100
"associated service: dialogue"
COMMENTARY = 0b101
"associated service: commentary"
EMERGENCY = 0b110
"associated service: emergency"
VOICEOVER_KARAOKE = 0b111
"""
associated service: voice over *OR* main audio service: karaoke.
If `acmod` is `0b001` (mono 1/0), this is voice-over, otherwise it
should be interpreted as karaoke.
"""
class AudioCodingMode(Enum):
"""
Dolby Digital Plus `acmod` field
§ 4.3.2.3
"""
RESERVED = 0b000
CH_ORD_1_0 = 0b001
"Mono"
CH_ORD_2_0 = 0b010
"L/R stereo"
CH_ORD_3_0 = 0b011
"LCR stereo"
CH_ORD_2_1 = 0b100
"LR + mono surround"
CH_ORD_3_1 = 0b101
"LCR + mono surround"
CH_ORD_2_2 = 0b110
"LR + LR surround"
CH_ORD_3_2 = 0b111
"LCR + LR surround"
class CenterDownMixLevel(Enum):
"""
§ 4.3.3.1
"""
DOWN_3DB = 0b00
"Attenuate 3 dB"
DOWN_45DB = 0b01
"Attenuate 4.5 dB"
DOWN_6DB = 0b10
"Attenuate 6 dB"
RESERVED = 0b11
class SurroundDownMixLevel(Enum):
"""
Dolby Digital Plus `surmixlev` field
§ 4.3.3.2
"""
DOWN_3DB = 0b00
DOWN_6DB = 0b01
MUTE = 0b10
RESERVED = 0b11
class LanguageCode(int):
"""
§ 4.3.4.1
Per ATSC/A52 § 5.4.2.12, this is not in use and always 0xFF.
"""
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
"""
NOT_INDICATED = 0b00
LARGE_ROOM_X_CURVE = 0b01
SMALL_ROOM_FLAT_CURVE = 0b10
RESERVED = 0b11
class PreferredDownMixMode(Enum):
"""
Indicates the creating engineer's preference of what the receiver should
downmix.
§ 4.3.8.1
"""
NOT_INDICATED = 0b00
PRO_LOGIC = 0b01
STEREO = 0b10
PRO_LOGIC_2 = 0b11
class SurroundEXMode(IntEnum):
"""
Dolby Surround-EX mode.
`dsurexmod` § 4.3.9.1
"""
NOT_INDICATED = 0b00
NOT_SEX = 0b01
SEX = 0b10
PRO_LOGIC_2 = 0b11
class HeadphoneMode(IntEnum):
"""
`dheadphonmod` § 4.3.9.2
"""
NOT_INDICATED = 0b00
NOT_DOLBY_HEADPHONE = 0b01
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
"""
INDEPENDENT = 0
DEPENDENT = 1
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
RESERVED = 3
class RFCompressionProfile(Enum):
"""
`compr1` RF compression profile
§ 4.3.10 (fig 42)
"""
NONE = 0
FILM_STANDARD = 1
FILM_LIGHT = 2
MUSIC_STANDARD = 3
MUSIC_LIGHT = 4
SPEECH = 5
#: Program ID number, this identifies the program in a multi-program
#: element. § 4.3.1
program_id: int
#: `True` if LFE is enabled. § 4.3.2.1
lfe_on: bool
#: The kind of service of this stream. `bsmod` § 4.3.2.2
bitstream_mode: BitStreamMode
#: Indicates which channels are in use. `acmod` § 4.3.2.3
audio_coding_mode: AudioCodingMode
#: When the front three channels are in use, gives the center
#: downmix level. ``
center_downmix_level: CenterDownMixLevel
#: When the surround channels are in use, gives the surround
#: downmix level.
surround_downmix_level: SurroundDownMixLevel
#: If the `acmod` is LR, this indicates if the channels
#: are encoded in Dolby Surround.
dolby_surround_encoded: DolbySurroundEncodingMode
#: `True` if there is a langcode present in the metadata.
langcode_present: bool
#: `True` if this bitstream is copyrighted.
copyright_bitstream: bool
#: `True` if this bitstream is original.
original_bitstream: bool
dialnorm: DialnormLevel
#: Language code
langcode: int
#: `True` if `mixlevel` and `roomtype` are valid
prod_info_exists: bool
#: Mix level
mixlevel: MixLevel
#: Room Type
roomtype: RoomType
#: LoRo preferred center downmix level
loro_center_downmix_level: DownMixLevelToken
#: LoRo preferred surround downmix level
loro_surround_downmix_level: DownMixLevelToken
#: Preferred downmix mode
downmix_mode: PreferredDownMixMode
#: LtRt preferred center downmix level
ltrt_center_downmix_level: DownMixLevelToken
#: LtRt preferred surround downmix level
ltrt_surround_downmix_level: DownMixLevelToken
#: Surround-EX mode
surround_ex_mode: SurroundEXMode
#: Dolby Headphone mode
dolby_headphone_encoded: HeadphoneMode
ad_converter_type: ADConverterType
compression_profile: RFCompressionProfile
dynamic_range: RFCompressionProfile
#: Indicates if this stream can be decoded independently or not
stream_dependency: StreamDependency
#: Data rate of this bitstream in kilobits per second
datarate_kbps: int
@staticmethod
def load(buffer: bytes):
assert len(buffer) == 96, "Dolby Digital Plus segment incorrect size, "
"expected 96 got %i" % len(buffer)
def program_id(b) -> int:
return b
def program_info(b):
return (b & 0x40) > 0, \
DolbyDigitalPlusMetadata.BitStreamMode(b & 0x38 >> 3), \
DolbyDigitalPlusMetadata.AudioCodingMode(b & 0x7)
def ddplus_reserved1(_):
pass
def surround_config(b):
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, \
DolbyDigitalPlusMetadata.DialnormLevel(b & 0x1f)
def langcod(b) -> int:
return b
def audio_prod_info(b):
return (b & 0x80) > 0, \
DolbyDigitalPlusMetadata.MixLevel(b & 0x7c >> 2), \
DolbyDigitalPlusMetadata.RoomType(b & 0x3)
# loro_center_downmix_level, loro_surround_downmix_level
def ext_bsi1_word1(b):
return DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
# downmix_mode, ltrt_center_downmix_level, ltrt_surround_downmix_level
def ext_bsi1_word2(b):
return DolbyDigitalPlusMetadata.PreferredDownMixMode(b & 0xC0 >> 6), \
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x38 >> 3), \
DolbyDigitalPlusMetadata.DownMixLevelToken(b & 0x7)
#surround_ex_mode, dolby_headphone_encoded, ad_converter_type
def ext_bsi2_word1(b):
return DolbyDigitalPlusMetadata.SurroundEXMode(b & 0x60 >> 5), \
DolbyDigitalPlusMetadata.HeadphoneMode(b & 0x18 >> 3), \
DolbyDigitalPlusMetadata.ADConverterType( b & 0x4 >> 2)
def ddplus_reserved2(_):
pass
def compr1(b):
return DolbyDigitalPlusMetadata.RFCompressionProfile(b)
def dynrng1(b):
DolbyDigitalPlusMetadata.RFCompressionProfile(b)
def ddplus_reserved3(_):
pass
def ddplus_info1(b):
return DolbyDigitalPlusMetadata.StreamDependency(b & 0xc >> 2)
def ddplus_reserved4(_):
pass
def datarate(b) -> int:
return unpack("<H", b)[0]
def reserved(_):
pass
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])
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])
ddplus_reserved2(buffer[11:14])
compression = compr1(buffer[14])
dynamic_range = dynrng1(buffer[15])
ddplus_reserved3(buffer[16:19])
stream_info = ddplus_info1(buffer[19])
ddplus_reserved4(buffer[20:25])
data_rate = datarate(buffer[25:27])
reserved(buffer[27:69])
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,
surround_downmix_level=surround_downmix_level,
dolby_surround_encoded=dolby_surround_encoded,
langcode_present=langcode_present,
copyright_bitstream=copyright_bitstream,
original_bitstream=original_bitstream,
dialnorm=dialnorm,
langcode=langcode,
prod_info_exists=prod_info_exists,
mixlevel=mixlevel,
roomtype=roomtype,
loro_center_downmix_level=loro_center_downmix_level,
loro_surround_downmix_level=loro_surround_downmix_level,
downmix_mode=downmix_mode,
ltrt_center_downmix_level=ltrt_center_downmix_level,
ltrt_surround_downmix_level=ltrt_surround_downmix_level,
surround_ex_mode=surround_ex_mode,
dolby_headphone_encoded=dolby_headphone_encoded,
ad_converter_type=ad_converter_type,
compression_profile=compression,
dynamic_range=dynamic_range,
stream_dependency=stream_info,
datarate_kbps=data_rate)
@dataclass
class DolbyAtmosMetadata:
"""
Dolby Atmos Metadata Segment
https://github.com/DolbyLaboratories/dbmd-atmos-parser/
"""
class WarpMode(Enum):
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 "\
"is incorrect length, expected %i actual was %i" % (cls.SEGMENT_LENGTH, len(data))
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:
"""
Dolby Atmos supplemental metadata segment.
https://github.com/DolbyLaboratories/dbmd-atmos-parser/blob/master/dbmd_atmos_parse/src/dbmd_atmos_parse.c
"""
class BinauralRenderMode(Enum):
BYPASS = 0x00
NEAR = 0x01
FAR = 0x02
MID = 0x03
NOT_INDICATED = 0x04
object_count: int
render_modes: List['DolbyAtmosSupplementalMetadata.BinauralRenderMode']
trim_modes: List[int]
MAGIC = 0xf8726fbd
TRIM_CONFIG_COUNT = 9
@classmethod
def load(cls, data: bytes):
trim_modes = []
render_modes = []
h = BytesIO(data)
magic = unpack("<I", h.read(4))
assert magic == cls.MAGIC, "Magic value was not found"
object_count = unpack("<H", h.read(2))
h.read(1) #skip 1
for _ in range(cls.TRIM_CONFIG_COUNT):
auto_trim = unpack("B", h.read(1))
trim_modes.append(auto_trim)
h.read(14) #skip 14
h.read(object_count) # skip object_count bytes
for _ in range(object_count):
binaural_mode = unpack("B", h.read(1))
binaural_mode &= 0x7
render_modes.append(binaural_mode)
return DolbyAtmosSupplementalMetadata(object_count=object_count,
render_modes=render_modes,trim_modes=trim_modes)
class WavDolbyMetadataReader:
"""
Reads Dolby bitstream metadata.
"""
#: List of the Dolby Metadata Segments.
#:
#: Each list entry is a tuple of `SegmentType`, a `bool`
#: indicating if the segment's checksum was valid, and the
#: segment's parsed dataclass (or a `bytes` array if it was
#: not recognized).
segment_list: Tuple[Union[SegmentType, int], bool, Any]
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 = []
h = BytesIO(dbmd_data)
v_vec = []
for _ in range(4):
b = h.read(1)
v_vec.insert(0, unpack("B",b)[0])
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)
# elif stype == SegmentType.DolbyAtmosSupplemental:
# segment = DolbyAtmosSupplementalMetadata.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]]
def to_dict(self) -> dict:
ddp = map(lambda x: asdict(x), self.dolby_digital_plus())
atmos = map(lambda x: asdict(x), self.dolby_atmos())
#atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental())
return dict(dolby_digital_plus=list(ddp),
dolby_atmos=list(atmos))

View File

@@ -27,6 +27,9 @@ class WavIXMLFormat:
else:
return None
def xml_bytes(self):
return ET.tostring(self.parsed).decode("utf-8")
@property
def raw_xml(self):
"""
@@ -38,7 +41,7 @@ class WavIXMLFormat:
def track_list(self):
"""
A description of each track.
:return: An Iterator
:returns: An Iterator
"""
for track in self.parsed.find("./TRACK_LIST").iter():
if track.tag == 'TRACK':

View File

@@ -3,6 +3,8 @@ import struct
import os
from collections import namedtuple
from typing import Optional, Generator, Any
import pathlib
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
@@ -10,6 +12,7 @@ from .wave_ixml_reader import WavIXMLFormat
from .wave_bext_reader import WavBextReader
from .wave_info_reader import WavInfoChunkReader
from .wave_adm_reader import WavADMReader
from .wave_dbmd_reader import WavDolbyMetadataReader
#: Calculated statistics about the audio data.
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
@@ -22,6 +25,8 @@ WavAudioFormat = namedtuple('WavAudioFormat',
class WavInfoReader:
"""
Parse a WAV audio file for metadata.
"""
def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
@@ -55,10 +60,30 @@ class WavInfoReader:
absolute_path = os.path.abspath(path)
#: `file://` url for the file.
self.url = pathlib.Path(absolute_path).as_uri()
self.url: pathlib.Path = pathlib.Path(absolute_path).as_uri()
# for __repr__()
self.path = absolute_path
#: Wave audio data format.
self.fmt :Optional[WavAudioFormat] = None
#: Statistics of the `data` section.
self.data :Optional[WavDataDescriptor] = None
#: Broadcast-Wave metadata.
self.bext :Optional[WavBextReader] = None
#: iXML metadata.
self.ixml :Optional[WavIXMLFormat] = None
#: ADM Audio Definiton Model metadata.
self.adm :Optional[WavADMReader]= None
#: Dolby bitstream metadata.
self.dolby :Optional[WavDolbyMetadataReader] = None
#: RIFF INFO metadata.
self.info :Optional[WavInfoChunkReader]= None
with open(path, 'rb') as f:
self.get_wav_info(f)
@@ -69,20 +94,12 @@ class WavInfoReader:
self.main_list = chunks.children
wavfile.seek(0)
#: :class:`wavinfo.wave_reader.WavAudioFormat`
self.fmt = self._get_format(wavfile)
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
self.bext = self._get_bext(wavfile, encoding=self.bext_encoding)
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
self.ixml = self._get_ixml(wavfile)
#: :class:`wavinfo.wave_axml_reader.WavAxmlReader` with ADM metadata
self.adm = self._get_adm(wavfile)
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
self.info = self._get_info(wavfile, encoding=self.info_encoding)
self.dolby = self._get_dbmd(wavfile)
self.data = self._describe_data()
def _find_chunk_data(self, ident, from_stream, default_none=False):
@@ -141,21 +158,24 @@ class WavInfoReader:
chna = self._find_chunk_data(b'chna', f, default_none=True)
return WavADMReader(axml_data=axml, chna_data=chna) if axml and chna else None
def _get_dbmd(self, f):
dbmd_data = self._find_chunk_data(b'dbmd', f, default_none=True)
return WavDolbyMetadataReader(dbmd_data=dbmd_data) if dbmd_data else None
def _get_ixml(self, f):
ixml_data = self._find_chunk_data(b'iXML', f, default_none=True)
return None if ixml_data is None else WavIXMLFormat(ixml_data.rstrip(b'\0'))
return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
def walk(self):
def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()"
"""
Walk all of the available metadata fields.
:yields: a string, the :scope: of the metadatum, the string :name: of the
metadata field, and the value.
:yields: tuples of the *scope*, *key*, and *value* of
each metadatum. The *scope* value will be one of
"fmt", "data", "ixml", "bext", "info", "dolby", or "adm".
"""
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm')
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'dolby')
for scope in scopes:
if scope in ['fmt', 'data']:
@@ -168,6 +188,5 @@ class WavInfoReader:
for key in dict.keys():
yield scope, key, dict[key]
def __repr__(self):
return 'WavInfoReader({}, {}, {})'.format(self.path, self.info_encoding, self.bext_encoding)