diff --git a/README.md b/README.md index 98ca450..24c1bd1 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 123e87a..1d9e7c6 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -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__ diff --git a/docs/source/references.rst b/docs/source/references.rst index 737348e..6661bf2 100644 --- a/docs/source/references.rst +++ b/docs/source/references.rst @@ -35,6 +35,6 @@ iXML RIFF Metadata ------------- -* `1991. Multimedia Programming Interface and Data Specifications 1.0`_ +* `1991. Multimedia Programming Interface and Data Specifications 1.0 `_ * `Exiftool Documentation `_ diff --git a/docs/source/scopes/bext.rst b/docs/source/scopes/bext.rst index faf17b9..2c80505 100644 --- a/docs/source/scopes/bext.rst +++ b/docs/source/scopes/bext.rst @@ -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` 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`. -Here also the :py:attr:`originator_ref` +In this example (from a Sound Devices 702T) the bext metadata contains +scene/take slating information in the +:py:attr:`description`. +Here also the +:py:attr:`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()` + + Example ------- .. code:: python diff --git a/docs/source/scopes/cue.rst b/docs/source/scopes/cue.rst new file mode 100644 index 0000000..96eed0e --- /dev/null +++ b/docs/source/scopes/cue.rst @@ -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` +``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: diff --git a/docs/source/scopes/info.rst b/docs/source/scopes/info.rst index 7288fc6..b5d000b 100644 --- a/docs/source/scopes/info.rst +++ b/docs/source/scopes/info.rst @@ -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` +``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 --------------- diff --git a/examples/demo.ipynb b/examples/demo.ipynb index 7e6de5e..f05ac2b 100644 --- a/examples/demo.ipynb +++ b/examples/demo.ipynb @@ -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" ] }, { @@ -155,12 +201,84 @@ "print(\"iXML File Family UID:\", info.ixml.family_uid)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "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": null, + "execution_count": 6, "metadata": {}, - "outputs": [], - "source": [] + "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: \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 ''}\")" + ] }, { "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 } diff --git a/examples/wavinfo.ipynb b/examples/wavinfo.ipynb deleted file mode 100644 index ba3df78..0000000 --- a/examples/wavinfo.ipynb +++ /dev/null @@ -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": [ - "\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 -} diff --git a/tests/test_cue.py b/tests/test_cue.py new file mode 100644 index 0000000..4271769 --- /dev/null +++ b/tests/test_cue.py @@ -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") + + + + diff --git a/tests/test_files/cue_chunks/izotoperx_cues_test.wav b/tests/test_files/cue_chunks/izotoperx_cues_test.wav new file mode 100644 index 0000000..a1d198d Binary files /dev/null and b/tests/test_files/cue_chunks/izotoperx_cues_test.wav differ diff --git a/tests/test_walk.py b/tests/test_walk.py index 63df1ab..361766a 100644 --- a/tests/test_walk.py +++ b/tests/test_walk.py @@ -1,6 +1,7 @@ import unittest import wavinfo +import glob class TestWalk(unittest.TestCase): def test_walk_metadata(self): @@ -20,6 +21,17 @@ def test_walk_metadata(self): 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() diff --git a/wavinfo/wave_cues_reader.py b/wavinfo/wave_cues_reader.py index 65325a7..8d47a07 100644 --- a/wavinfo/wave_cues_reader.py +++ b/wavinfo/wave_cues_reader.py @@ -12,7 +12,7 @@ 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(" 'WavCuesReader': cue_list = [] @@ -200,17 +202,71 @@ def merge(cls, f, 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]) diff --git a/wavinfo/wave_reader.py b/wavinfo/wave_reader.py index e34fbd3..56a2f77 100644 --- a/wavinfo/wave_reader.py +++ b/wavinfo/wave_reader.py @@ -38,10 +38,8 @@ def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'): 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 @@ def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'): #: 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 @@ def _describe_data(self): 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 = " Generator[str,str,Any]: #FIXME: this should probably be named "iter()" @@ -214,7 +201,8 @@ def walk(self) -> Generator[str,str,Any]: #FIXME: this should probably be named "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']: