mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 08:50:41 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ce7b9ad9 | ||
|
|
00728f5af3 | ||
|
|
4e061a85f1 | ||
|
|
af5c83b8fc | ||
|
|
a7f77a49f7 | ||
|
|
0acbe58f0b | ||
|
|
8d908a3e34 | ||
|
|
c897d080bb | ||
|
|
710473f2aa | ||
|
|
cf48763b13 | ||
|
|
5651367df7 | ||
|
|
6d7373391e | ||
|
|
ff60f26f78 | ||
|
|
cc49df8f08 | ||
|
|
bdf5fc9349 | ||
|
|
4109f77372 | ||
|
|
7b9b64d799 | ||
|
|
e5cd098d44 | ||
|
|
957b23db92 | ||
|
|
733113819e | ||
|
|
df4cc8822e | ||
|
|
d5b6f15e28 | ||
|
|
b830b8cdc2 | ||
|
|
b23470ac19 | ||
|
|
8fe7eefb4a | ||
|
|
f0b7a0ddf6 | ||
|
|
e83603cb47 | ||
|
|
b6acdb1f7f | ||
|
|
faf809b8e2 | ||
|
|
f7a1896f99 | ||
|
|
40aee91162 | ||
|
|
9f8fc87d17 | ||
|
|
b2323a126f |
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
|
||||
|
||||
94
docs/source/command_line.rst
Normal file
94
docs/source/command_line.rst
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
ADM (Audio Definition Model) Metadata
|
||||
=====================================
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
|
||||
|
||||
Class Reference
|
||||
---------------
|
||||
|
||||
.. module:: wavinfo
|
||||
|
||||
.. autoclass:: wavinfo.wave_adm_reader.WavADMReader
|
||||
:members:
|
||||
@@ -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:
|
||||
|
||||
|
||||
30
docs/source/scopes/adm.rst
Normal file
30
docs/source/scopes/adm.rst
Normal 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:
|
||||
@@ -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
|
||||
|
||||
21
docs/source/scopes/dolby.rst
Normal file
21
docs/source/scopes/dolby.rst
Normal 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:
|
||||
@@ -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
|
||||
---------------
|
||||
|
||||
@@ -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
49
tests/test_dolby.py
Normal 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")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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...
|
||||
@@ -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())
|
||||
@@ -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
661
wavinfo/wave_dbmd_reader.py
Normal 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))
|
||||
@@ -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':
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user