mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 08:50:41 +00:00
Merge branch 'feature-cues' into maint-rm-umid
This commit is contained in:
24
README.md
24
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).
|
||||
|
||||
@@ -18,10 +18,13 @@ 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__
|
||||
|
||||
|
||||
@@ -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>`_
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
31
docs/source/scopes/cue.rst
Normal file
31
docs/source/scopes/cue.rst
Normal 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:
|
||||
@@ -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
|
||||
---------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
74
tests/test_cue.py
Normal 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")
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
tests/test_files/cue_chunks/izotoperx_cues_test.wav
Normal file
BIN
tests/test_files/cue_chunks/izotoperx_cues_test.wav
Normal file
Binary file not shown.
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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']:
|
||||
|
||||
Reference in New Issue
Block a user