Merge branch 'feature-cues' into maint-rm-umid

This commit is contained in:
Jamie Hardt
2023-11-07 18:07:35 -08:00
13 changed files with 388 additions and 306 deletions

View File

@@ -4,7 +4,9 @@
# wavinfo
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64]
and extract extended metadata, with an emphasis on film, video and
professional music production.
## Metadata Support
@@ -13,17 +15,18 @@ The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] an
* [Broadcast-WAVE][bext] metadata, including embedded program
loudness, coding history and [SMPTE UMID][smpte_330m2011].
* [ADM][adm] track metadata and schema, including channel, pack formats, object, content and programme.
* [Audio Definition Model (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.
* [iXML][ixml] production recorder metadata, including project, scene, and
take tags, recorder notes and file family information.
* iXML `STEINBERG` sound library attributes.
* Wave embedded cue markers, cue marker labels, notes and timed ranges as used
by Zoom, iZotope RX, etc.
* Most of the common [RIFF INFO][info-tags] metadata fields.
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count
information.
In progress:
* Pro Tools __embedded regions__.
* The __wav format__ is also parsed, so you can access the basic sample rate
and channel count information.
[bext]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html
[smpte_330m2011]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html#wavinfo.wave_bext_reader.WavBextReader.umid
@@ -57,4 +60,5 @@ $ wavinfo test_files/A101_1.WAV
## Other Resources
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).
* For other file formats and ID3 decoding,
look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).

View File

@@ -17,11 +17,14 @@ instance of :class:`WaveInfoReader`.
adm_metadata = info.adm
ixml_metadata = info.ixml
WavInfoReader Class Documentation
--------------------------------------
.. module:: wavinfo
:noindex:
.. autoclass:: wavinfo.wave_reader.WavInfoReader
:members:
:special-members: __init__

View File

@@ -35,6 +35,6 @@ iXML
RIFF Metadata
-------------
* `1991. Multimedia Programming Interface and Data Specifications 1.0<https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf>`_
* `1991. Multimedia Programming Interface and Data Specifications 1.0 <https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf>`_
* `Exiftool Documentation <https://exiftool.org/TagNames/RIFF.html#Info_docs>`_

View File

@@ -4,32 +4,43 @@ Broadcast WAV Extension Metadata
Notes
-----
A WAV file produced to Broadcast-WAV specifications will have the broadcast metadata extension,
which includes a 256-character free text descrption, creating entity identifier (usually the
recording application or equipment), the date and time of recording and a time reference for
timecode synchronization.
A WAV file produced to Broadcast-WAV specifications will have the broadcast
metadata extension, which includes a 256-character free text descrption,
creating entity identifier (usually the recording application or equipment),
the date and time of recording and a time reference for timecode
synchronization.
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 :py:attr:`description<wavinfo.wave_bext_reader.WavBextReader.description>`.
Here also the :py:attr:`originator_ref<wavinfo.wave_bext_reader.WavBextReader.originator_ref>`
In this example (from a Sound Devices 702T) the bext metadata contains
scene/take slating 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
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 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
.. note::
All text fields in the Broadcast-WAV metadata structure are decoded by
default as flat ASCII. To override this and use a different encoding, pass
an string encoding name to the ``bext_encoding`` parameter of
:py:meth:`WavInfoReader()<wavinfo.wave_reader.WavInfoReader.__init__>`
Example
-------
.. code:: python

View File

@@ -0,0 +1,31 @@
Cue Marker and Range Metadata
------------------------------
Notes
=====
Cue metadata stores timed markers that clients use to mark times of interest
in a wave file, and optionally give them a name and longer comment. Markers
can also have an associated length, allowing ranges of times in a file to be
marked.
String Encoding of Cue Metadata
"""""""""""""""""""""""""""""""
Cue labels and notes will be decoded using the string encoding passed to
:py:meth:`WavInfoReader's<wavinfo.wave_reader.WaveInfoReader.__init__>`
``info_encoding=`` parameter, which by default is ``latin_1`` (ISO 8859-1).
Text associated with ``ltxt`` time ranges may specify their own encoding in
the form of a Windows codepage number. `wavinfo` will attempt to use the
encoding specified.
.. note::
``cset`` character set/locale metadata is not supported. If it is present
in the file it will be ignored by `wavinfo`.
Class Reference
===============
.. autoclass:: wavinfo.wave_cues_reader.WavCuesReader
:members:

View File

@@ -20,16 +20,16 @@ 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.
String Encoding of INFO Metadata
""""""""""""""""""""""""""""""""
Info metadata fields will be decoded using the string encoding passed to
:py:meth:`WavInfoReader's<wavinfo.wave_reader.WaveInfoReader.__init__>`
``info_encoding=`` parameter, which by default is ``latin_1`` (ISO 8859-1).
.. note::
``cset`` character set/locale metadata is not supported. If it is present
in the file it will be ignored by `wavinfo`.
Class Reference
---------------

View File

@@ -6,7 +6,16 @@
"source": [
"# `wavinfo` Demonstration\n",
"\n",
"The entry point for wavinfo is the WavInfoReader class."
"The `wavinfo` module allows you to read most of the metadata formats that are available for WAV files."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Opening a WAV file for reading metadata\n",
"\n",
"The entry point for wavinfo is the `WavInfoReader` class:"
]
},
{
@@ -26,7 +35,35 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basic WAV Data\n",
"Once you have a `WavInfoReader`, you can access different metadata systems or \"scopes.\"\n",
"\n",
"The scopes that are presently supported are: \n",
" * `fmt`: sample format, sample rate, bit depth, block alignment, etc.\n",
" * `data`: data chunk description, bytes length and frames length.\n",
" * `ixml`: Gallery Software's iXML metadata, used by production sound recorder equipment and DAWs.\n",
" * `bext`: Broacast-WAV metadata as used by DAWs.\n",
" * `info`: title, artist and description metadata tags, among other items.\n",
" * `adm`: EBU Audio Defintion Model metadata, as used by Dolby Atmos.\n",
" * `cues`: Cue marker metadata, including labels and notes \n",
" * `dolby`: Dolby recorder and playback metadata\n",
"\n",
"Each of these is an attribute of a `WavInfoReader` object.\n",
"\n",
"Each scope corresponds to a vendor-defined metadata system. Many scopes directly represent a specific file *chunk*, like `fmt` or `ixml`, and some may involve data read from many chunks. Examples of this would include `cues` or `adm`.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Metadata Scopes"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### `data` and `fmt`: Basic WAV Data\n",
"\n",
"The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk."
]
@@ -51,6 +88,13 @@
"(info.data.frame_count, info.data.byte_count)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `fmt` scope allows the client to read metadata from the WAVE format description."
]
},
{
"cell_type": "code",
"execution_count": 3,
@@ -75,7 +119,9 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Broadcast WAV Extension"
"### `bext`: Broadcast WAV Extension\n",
"\n",
"The `bext` scope allows the client to access Broadcast-WAV metadata. "
]
},
{
@@ -87,17 +133,17 @@
"name": "stdout",
"output_type": "stream",
"text": [
"sSPEED=023.976-ND\r\n",
"sTAKE=1\r\n",
"sUBITS=$12311801\r\n",
"sSWVER=2.67\r\n",
"sPROJECT=BMH\r\n",
"sSCENE=A101\r\n",
"sFILENAME=A101_1.WAV\r\n",
"sTAPE=18Y12M31\r\n",
"sTRK1=MKH516 A\r\n",
"sTRK2=Boom\r\n",
"sNOTE=\r\n",
"sSPEED=023.976-ND\n",
"sTAKE=1\n",
"sUBITS=$12311801\n",
"sSWVER=2.67\n",
"sPROJECT=BMH\n",
"sSCENE=A101\n",
"sFILENAME=A101_1.WAV\n",
"sTAPE=18Y12M31\n",
"sTRK1=MKH516 A\n",
"sTRK2=Boom\n",
"sNOTE=\n",
"\n",
"----------\n",
"Originator: Sound Dev: 702T S#GR1112089007\n",
@@ -105,7 +151,7 @@
"Originator Date: 2018-12-31\n",
"Originator Time: 12:40:00\n",
"Time Reference: 2190940753\n",
"A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
"A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\n",
"\n"
]
}
@@ -125,7 +171,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## iXML Production Recorder Metadata"
"### `ixml`: iXML Production Recorder Metadata"
]
},
{
@@ -156,11 +202,83 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"metadata": {},
"outputs": [],
"source": []
"source": [
"### `cues`: Cues Metadata\n",
"\n",
"Cue time markers are accessible through the `cues` scope. The `each_cue` method returns an iterator that yields a tuple of each cue \"name\" or integer UID, and sample location. "
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Cue ID: 1\n",
"Cue Offset: 29616\n",
"Cue ID: 2\n",
"Cue Offset: 74592\n",
"Cue ID: 3\n",
"Cue Offset: 121200\n"
]
}
],
"source": [
"path = \"../tests/test_files/cue_chunks/STE-000.wav\"\n",
"info = WavInfoReader(path)\n",
"\n",
"for cue in info.cues.each_cue():\n",
" print(f\"Cue ID: {cue[0]}\")\n",
" print(f\"Cue Offset: {cue[1]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There is also a convenience method to get the appropriate label and note for a given marker. (Note here also `WavInfoReader`'s facility for overriding default text encodings.)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Cue ID: 1\n",
" Label: Marker 1\n",
" At: 1000\n",
" Note: <NO NOTE>\n",
"Cue ID: 2\n",
" Label: Marker 2\n",
" At: 5000\n",
" Note: Marker Comment 1\n",
"Cue ID: 3\n",
" Label: Marker 3\n",
" At: 10000\n",
" Note: Лорем ипсум долор сит амет, тимеам вивендум хас ет, цу адолесценс дефинитионес еам.\n"
]
}
],
"source": [
"path = \"../tests/test_files/cue_chunks/izotoperx_cues_test.wav\"\n",
"info = WavInfoReader(path, info_encoding=\"utf-8\") # iZotope RX seems to encode marker text as UTF-8\n",
"\n",
"for cue in info.cues.each_cue():\n",
" print(f\"Cue ID: {cue[0]}\")\n",
" label, note = info.cues.label_and_note(cue[0])\n",
" print(f\" Label: {label}\")\n",
" print(f\" At: {cue[1]}\")\n",
" print(f\" Note: {note or '<NO NOTE>'}\")"
]
},
{
"cell_type": "code",
@@ -172,7 +290,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
@@ -186,9 +304,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
"version": "3.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}

View File

@@ -1,215 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import wavinfo\n",
"import pprint"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"pp = pprint.PrettyPrinter(indent=4)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"path = '../tests/test_files/protools/PT A101_4.A1.wav'\n",
"\n",
"info = wavinfo.WavInfoReader(path)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[ ChunkDescriptor(ident=b'bext', start=20, length=858),\n",
" ChunkDescriptor(ident=b'iXML', start=886, length=5226),\n",
" ChunkDescriptor(ident=b'fmt ', start=6120, length=16),\n",
" ChunkDescriptor(ident=b'data', start=6144, length=864840),\n",
" ChunkDescriptor(ident=b'umid', start=870992, length=24),\n",
" ChunkDescriptor(ident=b'minf', start=871024, length=16),\n",
" ChunkDescriptor(ident=b'regn', start=871048, length=92)]\n"
]
}
],
"source": [
"import wavinfo.wave_parser\n",
"\n",
"with open(path,'rb') as f:\n",
" chunk_tree = wavinfo.wave_parser.parse_chunk(f)\n",
"\n",
"pp.pprint(chunk_tree.children)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00*\\xfd\\xf5\\x0c$\\xe4s\\x80\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'\n",
"000000000000002afdf50c24e47380000000000000000000\n",
"24\n"
]
}
],
"source": [
"with open(path,'rb') as f:\n",
" f.seek( chunk_tree.children[4].start )\n",
" umid_bin = f.read(chunk_tree.children[4].length)\n",
" f.seek( chunk_tree.children[6].start )\n",
" regn_bin = f.read(chunk_tree.children[6].length)\n",
" \n",
"print(umid_bin)\n",
"print(umid_bin.hex())\n",
"print(len(umid_bin))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<wavinfo.wave_bext_reader.WavBextReader object at 0x10d5f8ac8>\n"
]
}
],
"source": [
"print(info.bext)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00*\\xfd\\xf5\\x0c$\\xe4s\\x80\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c3\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00T\\xd5\\xa2\\x82\\x00\\x00\\x00\\x00\\x10PT A101_4.A1.wavGK\\xaa\\xaf\\x7f\\x00\\x00@ }\\x06\\x00`\\x00\\x00'\n",
"01000000000000000000002afdf50c24e473800000000000000000000c330200000000000000000000000000000000000000000054d5a2820000000010505420413130315f342e41312e776176474baaaf7f000040207d0600600000\n",
"92\n"
]
}
],
"source": [
"\n",
"print(regn_bin)\n",
"print(regn_bin.hex())\n",
"print(len(regn_bin))"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{ 'artist': 'Frank Bry',\n",
" 'comment': 'BULLET Impact Plastic LCD TV Screen Shatter Debris 2x',\n",
" 'copyright': '2018 Creative Sound Design, LLC (The Recordist Christmas '\n",
" '2018) www.therecordist.com',\n",
" 'created_date': '2018-11-15',\n",
" 'engineer': None,\n",
" 'genre': 'Bullets',\n",
" 'keywords': None,\n",
" 'product': 'The Recordist Christmas 2018',\n",
" 'software': 'Soundminer',\n",
" 'source': None,\n",
" 'tape': None,\n",
" 'title': None}\n",
"{ 'coding_history': '',\n",
" 'description': 'BULLET Impact Plastic LCD TV Screen Shatter Debris 2x',\n",
" 'loudness_range': None,\n",
" 'loudness_value': None,\n",
" 'max_momentary_loudness': None,\n",
" 'max_shortterm_loudness': None,\n",
" 'max_true_peak': None,\n",
" 'originator': 'TheRecordist',\n",
" 'originator_date': '2018-12-20',\n",
" 'originator_ref': 'aaiAKt3fCGTk',\n",
" 'originator_time': '12:15:37',\n",
" 'time_reference': 57882,\n",
" 'version': 0}\n"
]
}
],
"source": [
"path = '../tests/test_files/BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'\n",
"\n",
"info = wavinfo.WavInfoReader(path)\n",
"\n",
"with open(path,'rb') as f:\n",
" chunk_tree = wavinfo.wave_parser.parse_chunk(f)\n",
" \n",
"pp.pprint(info.info.to_dict())\n",
"pp.pprint(info.bext.to_dict())"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

74
tests/test_cue.py Normal file
View File

@@ -0,0 +1,74 @@
from unittest import TestCase
from glob import glob
import wavinfo
class TestCue(TestCase):
def setUp(self) -> None:
self.test_files = glob("tests/test_files/cue_chunks/*.wav")
return super().setUp()
def test_enumerate(self):
file1 = "tests/test_files/cue_chunks/STE-000.wav"
w1 = wavinfo.WavInfoReader(file1)
self.assertIsNotNone(w1.cues)
vals = list(w1.cues.each_cue())
self.assertEqual(vals, [(1,29616),(2,74592),(3,121200)])
def test_labels_notes(self):
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
w1 = wavinfo.WavInfoReader(file)
self.assertIsNotNone(w1.cues)
assert w1.cues is not None
for name, _ in w1.cues.each_cue():
self.assertIn(name,[1,2,3])
label, note = w1.cues.label_and_note(name)
if name == 1:
self.assertEqual("Marker 1", label)
self.assertIsNone(note)
def test_range(self):
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
w1 = wavinfo.WavInfoReader(file)
self.assertIsNotNone(w1.cues)
assert w1.cues is not None
self.assertEqual(w1.cues.range(3), 10000)
def test_encoding_fallback(self):
"""
Added this after I noticed that iZotope RX seems to just encode "notes"
as utf-8 without bothering to dump this info into the ltxt or
specifying an encoding by some other means.
"""
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
w = wavinfo.WavInfoReader(file, info_encoding='utf-8')
expected = ("Лорем ипсум долор сит амет, тимеам вивендум хас ет, "
"цу адолесценс дефинитионес еам.")
assert w.cues is not None
note = [n for n in w.cues.notes if n.name == 3]
self.assertEqual(len(note), 1)
self.assertEqual(note[0].text, expected)
def test_label(self):
file = "tests/test_files/cue_chunks/izotoperx_cues_test.wav"
w = wavinfo.WavInfoReader(file)
self.assertIsNotNone(w.cues)
assert w.cues is not None
self.assertEqual(len(w.cues.labels), 3)
for label in w.cues.labels:
self.assertIn(label.name, [1,2,3])
if label.name == 1:
self.assertEqual(label.text, "Marker 1")
elif label.name == 2:
self.assertEqual(label.text, "Marker 2")
elif label.name == 3:
self.assertEqual(label.text, "Marker 3")

Binary file not shown.

View File

@@ -1,6 +1,7 @@
import unittest
import wavinfo
import glob
class TestWalk(unittest.TestCase):
def test_walk_metadata(self):
@@ -20,6 +21,17 @@ class TestWalk(unittest.TestCase):
self.assertTrue(tested_data and tested_format)
def test_walk_all(self):
for file in glob.glob('tests/test_files/**/*.wav'):
info = wavinfo.WavInfoReader(file)
try:
for _, _, _ in info.walk():
pass
except:
self.fail(f"Failed to walk metadata in file {file}")
if __name__ == '__main__':
unittest.main()

View File

@@ -12,7 +12,7 @@ import encodings
from .riff_parser import ChunkDescriptor
from struct import unpack, calcsize
from typing import Optional, NamedTuple, List, Dict, Any
from typing import Optional, Tuple, NamedTuple, List, Dict, Any, Generator
#: Country Codes used in the RIFF standard to resolve locale. These codes
#: appear in CSET and LTXT metadata.
@@ -130,7 +130,7 @@ class LabelEntry(NamedTuple):
@classmethod
def read(cls, data: bytes, encoding: str):
return cls(name=unpack("<I", data[0:4])[0],
text=data[4:].decode(encoding))
text=data[4:].decode(encoding).rstrip("\0"))
NoteEntry = LabelEntry
@@ -166,12 +166,14 @@ class WavCuesReader:
cues: List[CueEntry]
labels: List[LabelEntry]
ranges: List[RangeLabel]
notes: List[NoteEntry]
@classmethod
def merge(cls, f,
def read_all(cls, f,
cues: Optional[ChunkDescriptor],
labls: List[ChunkDescriptor],
ltxts: List[ChunkDescriptor],
notes: List[ChunkDescriptor],
fallback_encoding: str) -> 'WavCuesReader':
cue_list = []
@@ -200,17 +202,71 @@ class WavCuesReader:
fallback_encoding=fallback_encoding)
)
note_list = []
for note in notes:
note_list.append(
NoteEntry.read(note.read_data(f),
encoding=fallback_encoding)
)
return WavCuesReader(cues=cue_list, labels=label_list,
ranges=range_list)
ranges=range_list, notes=note_list)
def each_cue(self) -> Generator[Tuple[int, int], None, None]:
"""
Iterate through each cue.
:yields: the cue's ``name`` and ``sample_offset``
"""
for cue in self.cues:
yield (cue.name, cue.sample_offset)
def label_and_note(self, cue_ident: int) -> Tuple[Optional[str],
Optional[str]]:
"""
Get the label and note (extended comment) for a cue.
:param cue_ident: the cue's name, its unique identifying number
:returns: a tuple of the the cue's label (if present) and note (if
present)
"""
label = next((l.text for l in self.labels
if l.name == cue_ident), None)
note = next((n.text for n in self.notes
if n.name == cue_ident), None)
return (label, note)
def range(self, cue_ident: int) -> Optional[int]:
"""
Get the length of the time range for a cue, if it has one.
:param cue_ident: the cue's name, its unique identifying number
:returns: the length of the marker's range, or `None`
"""
return next((r.length for r in self.ranges
if r.name == cue_ident), None)
def to_dict(self) -> Dict[str, Any]:
return dict(cues=[c.__dict__ for c in self.cues],
labels=[l.__dict__ for l in self.labels],
ranges=[r.__dict__ for r in self.ranges])
retval = dict()
for n, t in self.each_cue():
retval[n] = dict()
retval[n]['frame'] = t
label, note = self.label_and_note(n)
r = self.range(n)
if label is not None:
retval[n]['label'] = label
if note is not None:
retval[n]['note'] = note
if r is not None:
retval[n]['length'] = r
return retval
# return dict(cues=[c._asdict() for c in self.cues],
# labels=[l._asdict() for l in self.labels],
# ranges=[r._asdict() for r in self.ranges],
# notes=[n._asdict() for n in self.notes])

View File

@@ -38,10 +38,8 @@ class WavInfoReader:
file handle to an open file.
:param info_encoding:
The text encoding of the INFO, LABL and other RIFF-defined metadata
fields. latin_1/ISO 8859-1/Win CP819 is the safest assumption for
this; chunks that define their own encoding explicitly (like LTXT)
will override this setting.
The text encoding of the ``INFO``, ``LABL`` and other RIFF-defined
metadata fields.
:param bext_encoding:
The text encoding to use when decoding the string
@@ -73,7 +71,7 @@ class WavInfoReader:
#: RIFF INFO metadata.
self.info :Optional[WavInfoChunkReader]= None
#: RIFF CUE, LABL and LTXT metadata.
#: RIFF cues markers, labels, and notes.
self.cues :Optional[WavCuesReader] = None
if hasattr(path, 'read'):
@@ -137,25 +135,12 @@ class WavInfoReader:
def _get_format(self, f):
fmt_data = self._find_chunk_data(b'fmt ', f)
assert fmt_data is not None, "Fmt data not found, not a valid wav file"
# The format chunk is
# audio_format U16
# channel_count U16
# sample_rate U32 Note an integer
# byte_rate U32 == SampleRate * NumChannels * BitsPerSample/8
# block_align U16 == NumChannels * BitsPerSample/8
# bits_per_sampl U16
packstring = "<HHIIHH"
rest_starts = struct.calcsize(packstring)
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
# 0x0001 WAVE_FORMAT_PCM PCM
# 0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
# 0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
# 0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
# 0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
# https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html
return WavAudioFormat(audio_format=unpacked[0],
channel_count=unpacked[1],
sample_rate=unpacked[2],
@@ -198,11 +183,13 @@ class WavInfoReader:
adtl = self._find_list_chunk(b'adtl')
labls = []
ltxts = []
notes = []
if adtl is not None:
labls = [child for child in adtl.children if child.ident == b'labl']
ltxts = [child for child in adtl.children if child.ident == b'ltxt']
labls = [c for c in adtl.children if c.ident == b'labl']
ltxts = [c for c in adtl.children if c.ident == b'ltxt']
notes = [c for c in adtl.children if c.ident == b'note']
return WavCuesReader.merge(f, cue, labls, ltxts,
return WavCuesReader.read_all(f, cue, labls, ltxts, notes,
fallback_encoding=self.info_encoding)
def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()"
@@ -214,7 +201,8 @@ class WavInfoReader:
"fmt", "data", "ixml", "bext", "info", "dolby", "cues" or "adm".
"""
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues', 'dolby')
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues',
'dolby')
for scope in scopes:
if scope in ['fmt', 'data']: