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 # 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 ## 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 * [Broadcast-WAVE][bext] metadata, including embedded program
loudness, coding history and [SMPTE UMID][smpte_330m2011]. 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. * [Dolby Digital Plus][ebu3285s6] and Dolby Atmos `dbmd` metadata.
* [iXML][ixml] production recorder metadata, including project, scene, and take tags, recorder notes * [iXML][ixml] production recorder metadata, including project, scene, and
and file family information. take tags, recorder notes and file family information.
* iXML `STEINBERG` sound library attributes. * 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. * 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 * The __wav format__ is also parsed, so you can access the basic sample rate
information. and channel count information.
In progress:
* Pro Tools __embedded regions__.
[bext]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html [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 [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 ## 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

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

View File

@@ -35,6 +35,6 @@ iXML
RIFF Metadata 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>`_ * `Exiftool Documentation <https://exiftool.org/TagNames/RIFF.html#Info_docs>`_

View File

@@ -4,32 +4,43 @@ Broadcast WAV Extension Metadata
Notes Notes
----- -----
A WAV file produced to Broadcast-WAV specifications will have the broadcast metadata extension, A WAV file produced to Broadcast-WAV specifications will have the broadcast
which includes a 256-character free text descrption, creating entity identifier (usually the metadata extension, which includes a 256-character free text descrption,
recording application or equipment), the date and time of recording and a time reference for creating entity identifier (usually the recording application or equipment),
timecode synchronization. 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>` 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. 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 In this example (from a Sound Devices 702T) the bext metadata contains
information in the :py:attr:`description<wavinfo.wave_bext_reader.WavBextReader.description>`. scene/take slating information in the
Here also the :py:attr:`originator_ref<wavinfo.wave_bext_reader.WavBextReader.originator_ref>` :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. 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 If the bext metadata conforms to `EBU 3285 v1`_, it will contain the WAV's 32
ST 330 UMID`_. The 32-byte version of the UMID is usually just a random number, while the 64-byte or 64 byte `SMPTE ST 330 UMID`_. The 32-byte version of the UMID is usually
UMID will also have information on the recording date and time, recording equipment and entity, just a random number, while the 64-byte UMID will also have information on the
and geolocation data. 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 If the bext metadata conforms to `EBU 3285 v2`_, it will hold precomputed
as described by `EBU Rec 128`_. program loudness values as described by `EBU Rec 128`_.
.. _EBU 3285 v1: https://tech.ebu.ch/publications/tech3285s1 .. _EBU 3285 v1: https://tech.ebu.ch/publications/tech3285s1
.. _SMPTE ST 330 UMID: https://standards.globalspec.com/std/1396751/smpte-st-330 .. _SMPTE ST 330 UMID: https://standards.globalspec.com/std/1396751/smpte-st-330
.. _EBU 3285 v2: https://tech.ebu.ch/publications/tech3285s2 .. _EBU 3285 v2: https://tech.ebu.ch/publications/tech3285s2
.. _EBU Rec 128: https://tech.ebu.ch/publications/r128 .. _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 Example
------- -------
.. code:: python .. 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) print("INFO Comment:", bullet.info.comment)
On Encodings String Encoding of INFO Metadata
"""""""""""" """"""""""""""""""""""""""""""""
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.
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 Class Reference
--------------- ---------------

View File

@@ -6,7 +6,16 @@
"source": [ "source": [
"# `wavinfo` Demonstration\n", "# `wavinfo` Demonstration\n",
"\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", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "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", "\n",
"The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk." "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)" "(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", "cell_type": "code",
"execution_count": 3, "execution_count": 3,
@@ -75,7 +119,9 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "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", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"sSPEED=023.976-ND\r\n", "sSPEED=023.976-ND\n",
"sTAKE=1\r\n", "sTAKE=1\n",
"sUBITS=$12311801\r\n", "sUBITS=$12311801\n",
"sSWVER=2.67\r\n", "sSWVER=2.67\n",
"sPROJECT=BMH\r\n", "sPROJECT=BMH\n",
"sSCENE=A101\r\n", "sSCENE=A101\n",
"sFILENAME=A101_1.WAV\r\n", "sFILENAME=A101_1.WAV\n",
"sTAPE=18Y12M31\r\n", "sTAPE=18Y12M31\n",
"sTRK1=MKH516 A\r\n", "sTRK1=MKH516 A\n",
"sTRK2=Boom\r\n", "sTRK2=Boom\n",
"sNOTE=\r\n", "sNOTE=\n",
"\n", "\n",
"----------\n", "----------\n",
"Originator: Sound Dev: 702T S#GR1112089007\n", "Originator: Sound Dev: 702T S#GR1112089007\n",
@@ -105,7 +151,7 @@
"Originator Date: 2018-12-31\n", "Originator Date: 2018-12-31\n",
"Originator Time: 12:40:00\n", "Originator Time: 12:40:00\n",
"Time Reference: 2190940753\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" "\n"
] ]
} }
@@ -125,7 +171,7 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## iXML Production Recorder Metadata" "### `ixml`: iXML Production Recorder Metadata"
] ]
}, },
{ {
@@ -156,11 +202,83 @@
] ]
}, },
{ {
"cell_type": "code", "cell_type": "markdown",
"execution_count": null,
"metadata": {}, "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", "cell_type": "code",
@@ -172,7 +290,7 @@
], ],
"metadata": { "metadata": {
"kernelspec": { "kernelspec": {
"display_name": "Python 3", "display_name": "Python 3 (ipykernel)",
"language": "python", "language": "python",
"name": "python3" "name": "python3"
}, },
@@ -186,9 +304,9 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.7.2" "version": "3.11.5"
} }
}, },
"nbformat": 4, "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 unittest
import wavinfo import wavinfo
import glob
class TestWalk(unittest.TestCase): class TestWalk(unittest.TestCase):
def test_walk_metadata(self): def test_walk_metadata(self):
@@ -20,6 +21,17 @@ class TestWalk(unittest.TestCase):
self.assertTrue(tested_data and tested_format) 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -12,7 +12,7 @@ import encodings
from .riff_parser import ChunkDescriptor from .riff_parser import ChunkDescriptor
from struct import unpack, calcsize 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 #: Country Codes used in the RIFF standard to resolve locale. These codes
#: appear in CSET and LTXT metadata. #: appear in CSET and LTXT metadata.
@@ -130,7 +130,7 @@ class LabelEntry(NamedTuple):
@classmethod @classmethod
def read(cls, data: bytes, encoding: str): def read(cls, data: bytes, encoding: str):
return cls(name=unpack("<I", data[0:4])[0], return cls(name=unpack("<I", data[0:4])[0],
text=data[4:].decode(encoding)) text=data[4:].decode(encoding).rstrip("\0"))
NoteEntry = LabelEntry NoteEntry = LabelEntry
@@ -166,12 +166,14 @@ class WavCuesReader:
cues: List[CueEntry] cues: List[CueEntry]
labels: List[LabelEntry] labels: List[LabelEntry]
ranges: List[RangeLabel] ranges: List[RangeLabel]
notes: List[NoteEntry]
@classmethod @classmethod
def merge(cls, f, def read_all(cls, f,
cues: Optional[ChunkDescriptor], cues: Optional[ChunkDescriptor],
labls: List[ChunkDescriptor], labls: List[ChunkDescriptor],
ltxts: List[ChunkDescriptor], ltxts: List[ChunkDescriptor],
notes: List[ChunkDescriptor],
fallback_encoding: str) -> 'WavCuesReader': fallback_encoding: str) -> 'WavCuesReader':
cue_list = [] cue_list = []
@@ -200,17 +202,71 @@ class WavCuesReader:
fallback_encoding=fallback_encoding) 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, 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]: def to_dict(self) -> Dict[str, Any]:
return dict(cues=[c.__dict__ for c in self.cues], retval = dict()
labels=[l.__dict__ for l in self.labels],
ranges=[r.__dict__ for r in self.ranges]) 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. file handle to an open file.
:param info_encoding: :param info_encoding:
The text encoding of the INFO, LABL and other RIFF-defined metadata The text encoding of the ``INFO``, ``LABL`` and other RIFF-defined
fields. latin_1/ISO 8859-1/Win CP819 is the safest assumption for metadata fields.
this; chunks that define their own encoding explicitly (like LTXT)
will override this setting.
:param bext_encoding: :param bext_encoding:
The text encoding to use when decoding the string The text encoding to use when decoding the string
@@ -73,7 +71,7 @@ class WavInfoReader:
#: RIFF INFO metadata. #: RIFF INFO metadata.
self.info :Optional[WavInfoChunkReader]= None self.info :Optional[WavInfoChunkReader]= None
#: RIFF CUE, LABL and LTXT metadata. #: RIFF cues markers, labels, and notes.
self.cues :Optional[WavCuesReader] = None self.cues :Optional[WavCuesReader] = None
if hasattr(path, 'read'): if hasattr(path, 'read'):
@@ -137,25 +135,12 @@ class WavInfoReader:
def _get_format(self, f): def _get_format(self, f):
fmt_data = self._find_chunk_data(b'fmt ', 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" 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" packstring = "<HHIIHH"
rest_starts = struct.calcsize(packstring) rest_starts = struct.calcsize(packstring)
unpacked = struct.unpack(packstring, fmt_data[:rest_starts]) 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], return WavAudioFormat(audio_format=unpacked[0],
channel_count=unpacked[1], channel_count=unpacked[1],
sample_rate=unpacked[2], sample_rate=unpacked[2],
@@ -198,11 +183,13 @@ class WavInfoReader:
adtl = self._find_list_chunk(b'adtl') adtl = self._find_list_chunk(b'adtl')
labls = [] labls = []
ltxts = [] ltxts = []
notes = []
if adtl is not None: if adtl is not None:
labls = [child for child in adtl.children if child.ident == b'labl'] labls = [c for c in adtl.children if c.ident == b'labl']
ltxts = [child for child in adtl.children if child.ident == b'ltxt'] 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) fallback_encoding=self.info_encoding)
def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "iter()" 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". "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: for scope in scopes:
if scope in ['fmt', 'data']: if scope in ['fmt', 'data']: