mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6654a194ba | ||
|
|
af5b538115 | ||
|
|
069666e9f9 | ||
|
|
13fdb147b5 | ||
|
|
8df6c52a9e | ||
|
|
408771c2e5 | ||
|
|
b0a4454f0d | ||
|
|
0952337a47 | ||
|
|
0de314d0ac | ||
|
|
8d7597c0df | ||
|
|
e9bebcd022 | ||
|
|
0138387d27 | ||
|
|
d1b42bd836 | ||
|
|
3323aef36c | ||
|
|
7cbdd3dab6 | ||
|
|
c392f48819 | ||
|
|
2cfb88a59c | ||
|
|
267befc0b0 | ||
|
|
26a9104dd9 | ||
|
|
f1089a7e08 | ||
|
|
ab7bd66f13 | ||
|
|
f1ce4888af | ||
|
|
7ca3721ab8 | ||
|
|
5aa34dfbe4 | ||
|
|
208edd8bdc | ||
|
|
96f79b5dc7 | ||
|
|
6f6a90a262 | ||
|
|
8aad9ae9b9 | ||
|
|
1a6349bdd8 | ||
|
|
9f0b1f1106 | ||
|
|
ec01f699fc | ||
|
|
2cc95b6f24 | ||
|
|
e35a5aa736 | ||
|
|
8ad03e34bb | ||
|
|
f5ee41c8d5 | ||
|
|
f00a338cee | ||
|
|
d0e45a2d90 | ||
|
|
ee1a0b9ac0 | ||
|
|
4401745c96 | ||
|
|
2c760a9c68 | ||
|
|
df15428260 | ||
|
|
43666de976 | ||
|
|
2ca21cd316 | ||
|
|
f963daa8a7 | ||
|
|
b87f4e135f | ||
|
|
a42e9d1bbf | ||
|
|
77517db653 | ||
|
|
16d2609558 | ||
|
|
18eda82ebd | ||
|
|
c8add89bc2 | ||
|
|
553b9d4790 | ||
|
|
0792388871 | ||
|
|
4384d8f575 | ||
|
|
e41eadad95 | ||
|
|
0933c7f580 | ||
|
|
538449bd9c | ||
|
|
a38c79d985 | ||
|
|
c0ab22115a | ||
|
|
75228830cb | ||
|
|
0c418cecdd | ||
|
|
156568488e | ||
|
|
5f2c16bd35 | ||
|
|
f63d8d8ef8 | ||
|
|
83500944eb | ||
|
|
cc29bfd801 | ||
|
|
c2ebaa8141 | ||
|
|
48c4b1565d | ||
|
|
f95a1ac652 | ||
|
|
c1e52ddba1 | ||
|
|
ef5078cc0d | ||
|
|
64b69f9341 | ||
|
|
1e7a4f6218 | ||
|
|
e47a7dbb89 | ||
|
|
3e3dd6d5bf |
39
.github/workflows/coverage.yml
vendored
Normal file
39
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Test Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.5.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
python -m pip install -e .
|
||||
- name: Setup FFmpeg
|
||||
uses: FedericoCarboni/setup-ffmpeg@v2
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
pip install pytest
|
||||
pip install pytest-cov
|
||||
pytest --cov=./ --cov-report=xml
|
||||
- name: Codecov
|
||||
# You may pin to the exact commit or the version.
|
||||
# uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d
|
||||
uses: codecov/codecov-action@v3.1.4
|
||||
|
||||
2
.github/workflows/python-package.yml
vendored
2
.github/workflows/python-package.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.5.0
|
||||
|
||||
@@ -29,4 +29,4 @@ python:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- doc
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Jamie Hardt
|
||||
Copyright (c) 2018-2023 Jamie Hardt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
25
README.md
25
README.md
@@ -1,9 +1,12 @@
|
||||
[](https://wavinfo.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/wavinfo/) 
|
||||
[](https://github.com/iluvcapra/wavinfo/actions/workflows/python-package.yml)
|
||||
[](https://codecov.io/gh/iluvcapra/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
|
||||
@@ -12,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
|
||||
@@ -56,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).
|
||||
|
||||
74
data/share/man/man1/wavinfo.1
Normal file
74
data/share/man/man1/wavinfo.1
Normal file
@@ -0,0 +1,74 @@
|
||||
.TH wavinfo 1 "2023-11-07" "Jamie Hardt" "User Manuals"
|
||||
.SH NAME
|
||||
wavinfo \- probe wave files for metadata
|
||||
.SH SYNOPSIS
|
||||
.SY wavinfo
|
||||
.I "[\-\-adm]"
|
||||
.I "[\-\-ixml]"
|
||||
.I FILE ...
|
||||
.SH DESCRIPTION
|
||||
.B wavinfo
|
||||
extracts embedded metadata from WAVE and RF64/WAVE sound files, with an
|
||||
emphasis on film, video and professional music production metadata.
|
||||
.SH OPTIONS
|
||||
.IP "(no options)"
|
||||
With no options,
|
||||
.B wavinfo
|
||||
will emit a JSON (Javascript Object Notation) object containing all
|
||||
detected metadata.
|
||||
.IP "\-\-adm"
|
||||
Output any Audio Definition Model (ADM) metadata in
|
||||
.BR FILE .
|
||||
.IP "\-\-ixml"
|
||||
Output any iXML metdata in
|
||||
.BR FILE .
|
||||
.IP "\-h, \-\-help"
|
||||
Print brief help.
|
||||
.SH DETAILED DESCRIPTION
|
||||
.B wavinfo
|
||||
collects metadata according to different
|
||||
.IR scopes .
|
||||
.SS METADATA SCOPES
|
||||
.IP fmt
|
||||
Basic audio properties: sample format, sample rate, channel count, etc.
|
||||
.IP data
|
||||
Size and frame count of the WAVE file's
|
||||
.I data
|
||||
segment.
|
||||
.IP cues
|
||||
Timed cue points, labels, notes and time ranges.
|
||||
.IP bext
|
||||
Broadcast-WAV metadata: description, originator, date and time, UMID.
|
||||
.IP ixml
|
||||
A selection of parsed iXML fields: track list, project, scene, take, tape,
|
||||
family name, family uid. For the full iXML document use the
|
||||
.IR \-\-ixml
|
||||
command option.
|
||||
.IP adm
|
||||
EBU Audio Definition Model (ADM) metadata: Programme, channels. For the full
|
||||
ADM
|
||||
.I <axml>
|
||||
document use the
|
||||
.IR \-\-adm
|
||||
command option.
|
||||
.IP dolby
|
||||
Dolby bitstream and Atmos metadata.
|
||||
.IP info
|
||||
INFO metadata fields: IART (artist), ICMT (comment), etc.
|
||||
.SH EXIT STATUS
|
||||
.IP 0
|
||||
On user quit.
|
||||
.SH AUTHOR
|
||||
Jamie Hardt
|
||||
.UR https://github.com/iluvcapra
|
||||
.UE
|
||||
.SH BUGS
|
||||
Issue submissions, feature requests, pull requests and other contributions
|
||||
are welcome and should be directed at
|
||||
.BR wavinfo 's
|
||||
home page on GitHub:
|
||||
.RS 4
|
||||
.UR https://github.com/iluvcapra/wavinfo
|
||||
.UE
|
||||
.\" .SH SEE ALSO
|
||||
.\" .BR "ffmpeg" "(1),"
|
||||
190
data/share/man/man7/wavinfo.7
Normal file
190
data/share/man/man7/wavinfo.7
Normal file
@@ -0,0 +1,190 @@
|
||||
.TH waveinfo 7 "2023-11-07" "Jamie Hardt" "Miscellaneous Information Manuals"
|
||||
.SH NAME
|
||||
wavinfo \- information about wave sound file metadata
|
||||
.\" .SH DESCRIPTION
|
||||
.SH CHUNK MENAGERIE
|
||||
A list of chunks that you may find in a wave file from our experience.
|
||||
.SS Essential WAV Chunks
|
||||
.IP fmt
|
||||
Defines the format of the audio in the
|
||||
.I data
|
||||
chunk: the audio codec, the sample rate, bit depth, channel count, block
|
||||
alignment and other data. May take an "extended" form, with additional data
|
||||
(such as channel speaker assignments) if there are more than two channels in
|
||||
the file or if it is a compressed format.
|
||||
.IP data
|
||||
The audio data itself. PCM audio data is always stored as interleaved samples.
|
||||
.IP JUNK
|
||||
A region of the file not currently in use. Clients sometimes add these before
|
||||
the
|
||||
.I data
|
||||
chunk in order to align the beginning of the audio data with a memory page
|
||||
boundary (this can make memory-mapped reads from a wave file a little more
|
||||
efficient). A
|
||||
.I JUNK
|
||||
chunk is often placed at the beginning of a WAVE file to reserve space for
|
||||
a
|
||||
.I ds64
|
||||
chunk that will be written to the file at the end of recording, in the event
|
||||
that after the file is finalized, it exceeds the RIFF size limit. Thus a WAVE
|
||||
file can be upgraded in-place to an RF64 without re-writing the audio data.
|
||||
.IP fact
|
||||
Fact chunks record the number of samples in the decoded audio stream. It's only
|
||||
present in WAVE files that contain compressed audio.
|
||||
.IP "LIST or list"
|
||||
(Both have been seen) Not a chunk type itself but signals to a RIFF parser that
|
||||
this chunk contains chunks itself. A LIST chunk's payload will begin with a
|
||||
four-character code identifying the form of the list, and is then followed
|
||||
by chunks of the standard key-length-data form, which may themselves be
|
||||
LISTs that themselves contain child chunks. WAVE files don't tend to have a
|
||||
very deep heirarchy of chunks, compared to AVI files.
|
||||
.SS RIFF Metadata
|
||||
The RIFF container format has a metadata system common to all RIFF files, WAVE
|
||||
being the most common at present, AVI being another very common format
|
||||
historically.
|
||||
.IP INFO
|
||||
A
|
||||
.I LIST
|
||||
form containing a flat list of chunks, each containing text metadata. The role
|
||||
of the string, like "Artist", "Composer", "Comment", "Engineer" etc. are given
|
||||
by the four-character code: "Artist" is
|
||||
.IR IART ,
|
||||
Composer is
|
||||
.IR ICMP ,
|
||||
engineer is
|
||||
.IR IENG ,
|
||||
Comment is
|
||||
.IR ICMT ,
|
||||
etc.
|
||||
.IP cue
|
||||
A binary list of cues, which are timed points within the audio data.
|
||||
.IP adtl
|
||||
A
|
||||
.I LIST
|
||||
form containing text labels
|
||||
.RI ( labl )
|
||||
for the cues in the
|
||||
.I cue
|
||||
chunk, "notes"
|
||||
.RI ( note ,
|
||||
which are structurally identical to
|
||||
.I labl
|
||||
but hosts tend to use notes for longer text), and "length text"
|
||||
.I ltxt
|
||||
metadata records, which can give a cue a length, making it a range, and a text
|
||||
field that defines its own encoding.
|
||||
.IP CSET
|
||||
Defines the character set for all text fields in
|
||||
.IR INFO ,
|
||||
.I adtl
|
||||
and other RIFF-defined text fields. By default, all of the text in RIFF
|
||||
metadata fields is Windows Latin 1/ISO 8859-1, though as time passes many
|
||||
clients have simply taken to sticking UTF-8 into these fields. The
|
||||
.I CSET
|
||||
cannot represent UTF-8 as a valid option for text encoding, it only speaks
|
||||
Windows codepages, and we've never seen one in a WAVE file in any event and
|
||||
it's vanishingly likely an audio app would recognize one if it saw it.
|
||||
.SS Broadcast-WAVE Metadata
|
||||
Broadcast-WAVE is a set of extensions to WAVE files to facilitate media
|
||||
production maintained by the EBU.
|
||||
.IP bext
|
||||
A multi-field structure containing mostly fixed-width text data capturing
|
||||
essential production information: a 256 character free description field,
|
||||
originator name and a unique reference, recording date and time, a frame-based
|
||||
timestamp for sample-accurate recording time, and a coding history record. The
|
||||
extended form of the structure can hold a SMPTE UMID (a kind of UUID, which
|
||||
may also contain timestamp and geolocation data) and pre-computed program
|
||||
loudness measurements.
|
||||
.IP peak
|
||||
A binary data structure containing the peak envelope for the audio data, for
|
||||
use by clients to generate a waveform overview.
|
||||
.SS Audio Definition Model Metadata
|
||||
Audio Definition Model (ADM) metadata is a metadata standard for audio
|
||||
broadcast and distribution maintained by the ITU.
|
||||
.IP chna
|
||||
A binary list that associates individual channels in the file to entities
|
||||
in the ADM XML document stored in the
|
||||
.I axml
|
||||
chunk. A
|
||||
.I chna
|
||||
chunk will always appear with an
|
||||
.I axml
|
||||
chunk and vice versa.
|
||||
.IP axml
|
||||
Contains an XML document with Audio Definition Model metadata. ADM metadata
|
||||
describes the program the WAVE file belongs to, role, channel assignment,
|
||||
and encoding properties of individual channels in the WAVE file, and if the
|
||||
WAVE file contains object-based audio, it will also give all of the positioning
|
||||
and panning automation envelopes.
|
||||
.IP bxml
|
||||
This is defined by the ITU as a gzip-compressed version of the
|
||||
.I axml
|
||||
chunk.
|
||||
.IP sxml
|
||||
This is a hybrid binary/gzip-compressed-XML chunk that associates ADM
|
||||
documents with timed ranges of a WAVE file.
|
||||
.SS Dolby Metadata
|
||||
.IP dbmd
|
||||
Records hints for Dolby playback applications for downmixing, level
|
||||
normalization and other things.
|
||||
.SS Proprietary Chunks
|
||||
.IP ovwf
|
||||
.B (Pro Tools)
|
||||
Pre-computed waveform overview data.
|
||||
.IP regn
|
||||
.B (Pro Tools)
|
||||
Region and cue point metadata.
|
||||
.SS Chunks of Unknown Purpose
|
||||
.IP elm1
|
||||
.IP minf
|
||||
.IP umid
|
||||
.SH HISTORY
|
||||
The oldest document that defines the form of a Wave file is the
|
||||
.I Multimedia Programming Interface and Data Specifications 1.0
|
||||
of August 1991.
|
||||
.\" .SH REFERENCES
|
||||
.\" .SS ESSENTIAL FILE FORMAT
|
||||
.\" .TP
|
||||
.\" .UR https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
||||
.\" Multimedia Programming Interface and Data Specifications 1.0
|
||||
.\" .UE
|
||||
.\" The original definition of the
|
||||
.\" .I RIFF
|
||||
.\" container, the
|
||||
.\" .I WAVE
|
||||
.\" form, the original metadata facilites, and things like language, country and
|
||||
.\" dialect enumerations.
|
||||
.\" .TP
|
||||
.\" .UR https://datatracker.ietf.org/doc/html/rfc2361
|
||||
.\" RFC 2361
|
||||
.\" .UE
|
||||
.\" A large RFC compilation of all of the known (in 1998) audio encoding formats
|
||||
.\" in use. 104 different codecs are documented with a name, the corresponding
|
||||
.\" magic number, and a vendor contact name, phone number and address (no
|
||||
.\" emails, strangely). Almost all of these are of historical interest only.
|
||||
.\" .SS RF64/Extended WAVE Format
|
||||
.\"
|
||||
.\" .TP
|
||||
.\" .UR https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
|
||||
.\" ITU Recommendation BS.2088-1-2019
|
||||
.\" .UE
|
||||
.\" BS.2088 gives a detailed description of the internals of an RF64 file,
|
||||
.\" .I ds64
|
||||
.\" structure and all formal requirements. It also defines the use of
|
||||
.\" .IR <axml> ,
|
||||
.\" .IR <bxml> ,
|
||||
.\" .IR <sxml> ,
|
||||
.\" and
|
||||
.\" .I <chna>
|
||||
.\" metadata chunks for the carriage of Audio Definition Model metadata.
|
||||
.\" .TP
|
||||
.\" .UR https://tech.ebu.ch/docs/tech/tech3306.pdf
|
||||
.\" EBU Tech 3306 "RF64: An Extended File Format for Audio Data"
|
||||
.\" .UE
|
||||
.\" Version 1 of Tech 3306 laid out the
|
||||
.\" .I RF64
|
||||
.\" extended WAVE
|
||||
.\" file format almost identically to
|
||||
.\" .IR BS.2088 ,
|
||||
.\" Version 2 of the standard wholly adopted
|
||||
.\" .IR BS.2088 .
|
||||
@@ -23,7 +23,7 @@ import wavinfo
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'wavinfo'
|
||||
copyright = u'2022, Jamie Hardt'
|
||||
copyright = u'2018-2023, Jamie Hardt'
|
||||
author = u'Jamie Hardt'
|
||||
|
||||
# The short X.Y version
|
||||
|
||||
@@ -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__
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ iXML
|
||||
* `Gallery Software iXML Specification <http://www.gallery.co.uk/ixml/>`_
|
||||
|
||||
|
||||
RIFF INFO
|
||||
---------
|
||||
RIFF Metadata
|
||||
-------------
|
||||
* `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,45 @@ 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
|
||||
|
||||
print(info.bext.description)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -16,7 +16,8 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11"
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12"
|
||||
]
|
||||
dependencies = [
|
||||
"lxml ~= 4.9.2"
|
||||
@@ -54,6 +55,9 @@ wavinfo = 'wavinfo.__main__:main'
|
||||
[project.scripts]
|
||||
wavinfo = "wavinfo.__main__:main"
|
||||
|
||||
[tool.flit.external-data]
|
||||
directory = "data"
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ class TestDolby(TestCase):
|
||||
def test_version(self):
|
||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||
d = t1.dolby
|
||||
|
||||
|
||||
assert d is not None
|
||||
self.assertEqual((1,0,0,6), d.version)
|
||||
|
||||
def test_segments(self):
|
||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||
d = t1.dolby
|
||||
assert d is not None
|
||||
|
||||
ddp = [x for x in d.segment_list if x[0] == SegmentType.DolbyDigitalPlus]
|
||||
atmos = [x for x in d.segment_list if x[0] == SegmentType.DolbyAtmos]
|
||||
@@ -26,6 +28,7 @@ class TestDolby(TestCase):
|
||||
def test_checksums(self):
|
||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||
d = t1.dolby
|
||||
assert d is not None
|
||||
|
||||
for seg in d.segment_list:
|
||||
self.assertTrue(seg[1])
|
||||
@@ -33,7 +36,7 @@ class TestDolby(TestCase):
|
||||
def test_ddp(self):
|
||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||
d = t1.dolby
|
||||
|
||||
assert d is not None
|
||||
ddp = d.dolby_digital_plus()
|
||||
self.assertEqual(len(ddp), 1, "Failed to find exactly one Dolby Digital Plus metadata segment")
|
||||
self.assertTrue( ddp[0].audio_coding_mode, DolbyDigitalPlusMetadata.AudioCodingMode.CH_ORD_3_2 )
|
||||
@@ -42,8 +45,8 @@ class TestDolby(TestCase):
|
||||
def test_atmos(self):
|
||||
t1 = wavinfo.WavInfoReader(self.test_file)
|
||||
d = t1.dolby
|
||||
|
||||
assert d is not None
|
||||
atmos = d.dolby_atmos()
|
||||
self.assertEqual(len(atmos), 1, "Failed to find exactly one Atmos metadata segment")
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
tests/test_files/cue_chunks/STE-000.wav
Normal file
BIN
tests/test_files/cue_chunks/STE-000.wav
Normal file
Binary file not shown.
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.
33
tests/test_main.py
Normal file
33
tests/test_main.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import unittest
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from wavinfo.__main__ import main
|
||||
|
||||
import sys
|
||||
import glob
|
||||
|
||||
class MainTest(unittest.TestCase):
|
||||
|
||||
def test_empty_argv(self):
|
||||
with patch.object(sys, 'argv', []):
|
||||
try:
|
||||
main()
|
||||
except:
|
||||
self.fail("main() throwing an exception")
|
||||
|
||||
def test_a_file(self):
|
||||
for path in glob.glob("tests/test_files/**/*.wav"):
|
||||
with patch.object(sys, 'argv', ["TEST", path]):
|
||||
try:
|
||||
main()
|
||||
except:
|
||||
self.fail("main() throwing an exception")
|
||||
|
||||
def test_ixml(self):
|
||||
with patch.object(sys, 'argv',
|
||||
['TEST', '--ixml', 'tests/test_files/sounddevices/A101_1.WAV']):
|
||||
try:
|
||||
main()
|
||||
except:
|
||||
self.fail("main() throwing an exception")
|
||||
25
tests/test_rf64.py
Normal file
25
tests/test_rf64.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# import os.path
|
||||
import gzip
|
||||
from glob import glob
|
||||
# from typing import Dict, Any, cast
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
# from .utils import all_files, ffprobe
|
||||
|
||||
import wavinfo
|
||||
|
||||
class TestRf64(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
return super().setUp()
|
||||
|
||||
def test_open(self):
|
||||
|
||||
for path in glob("tests/test_files/rf64/*.wav.gz"):
|
||||
gz = gzip.open(path)
|
||||
wav_info = wavinfo.WavInfoReader(gz)
|
||||
|
||||
self.assertIsNotNone(wav_info)
|
||||
# self.assertIsNotNone(wav_info.bext)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import os.path
|
||||
from glob import glob
|
||||
from typing import Dict, Any, cast
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
@@ -19,6 +21,9 @@ class TestWaveInfo(TestCase):
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
ffprobe_info = ffprobe(wav_file)
|
||||
|
||||
assert info.fmt is not None
|
||||
assert ffprobe_info is not None
|
||||
|
||||
self.assertEqual(info.fmt.channel_count, ffprobe_info['streams'][0]['channels'])
|
||||
self.assertEqual(info.fmt.sample_rate, int(ffprobe_info['streams'][0]['sample_rate']))
|
||||
self.assertEqual(info.fmt.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_sample']))
|
||||
@@ -32,13 +37,17 @@ class TestWaveInfo(TestCase):
|
||||
def test_data_against_ffprobe(self):
|
||||
for wav_file in all_files():
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
ffprobe_info = ffprobe(wav_file)
|
||||
ffprobe_info = cast(Dict[str,Any], ffprobe(wav_file))
|
||||
assert ffprobe_info is not None
|
||||
assert info.data is not None
|
||||
self.assertEqual(info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts']))
|
||||
|
||||
def test_bext_against_ffprobe(self):
|
||||
for wav_file in all_files():
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
ffprobe_info = ffprobe(wav_file)
|
||||
assert ffprobe_info is not None
|
||||
|
||||
if info.bext:
|
||||
if 'comment' in ffprobe_info['format']['tags']:
|
||||
self.assertEqual(info.bext.description, ffprobe_info['format']['tags']['comment'])
|
||||
@@ -81,7 +90,8 @@ class TestWaveInfo(TestCase):
|
||||
if basename in expected:
|
||||
info = wavinfo.WavInfoReader(wav_file)
|
||||
e = expected[basename]
|
||||
|
||||
self.assertIsNotNone(info.ixml)
|
||||
assert info.ixml is not None
|
||||
self.assertEqual(e['project'], info.ixml.project)
|
||||
self.assertEqual(e['scene'], info.ixml.scene)
|
||||
self.assertEqual(e['take'], info.ixml.take)
|
||||
@@ -93,10 +103,32 @@ class TestWaveInfo(TestCase):
|
||||
if basename == 'A101_4.WAV' and track.channel_index == '1':
|
||||
self.assertEqual(track.name, 'MKH516 A')
|
||||
|
||||
def test_metadata(self):
|
||||
def test_steinberg_ixml(self):
|
||||
nuendo_files = 'tests/test_files/nuendo/*.wav'
|
||||
for file in glob(nuendo_files):
|
||||
info = wavinfo.WavInfoReader(file)
|
||||
assert info.ixml is not None
|
||||
self.assertIsNotNone(info.ixml.steinberg)
|
||||
assert info.ixml.steinberg is not None
|
||||
self.assertIsNotNone(info.ixml.steinberg.audio_speaker_arrangement)
|
||||
self.assertEqual(info.ixml.steinberg.sample_format_size, 3)
|
||||
self.assertEqual(info.ixml.steinberg.media_company, "https://github.com/iluvcapra/wavinfo")
|
||||
self.assertFalse(info.ixml.steinberg.media_drop_frames)
|
||||
self.assertEqual(info.ixml.steinberg.media_duration, 1200.0)
|
||||
|
||||
def test_steinberg_missing(self):
|
||||
file_with_no_nuendo = "tests/test_files/sounddevices/A101_1.WAV"
|
||||
|
||||
info = wavinfo.WavInfoReader(file_with_no_nuendo)
|
||||
assert info.ixml is not None
|
||||
self.assertIsNone(info.ixml.steinberg)
|
||||
|
||||
def test_info_metadata(self):
|
||||
file_with_metadata = 'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
|
||||
self.assertTrue(os.path.exists(file_with_metadata))
|
||||
info = wavinfo.WavInfoReader(file_with_metadata).info
|
||||
|
||||
assert info is not None
|
||||
self.assertEqual(info.title, 'camera bumb 1')
|
||||
self.assertEqual(info.artist, 'Jamie Hardt')
|
||||
self.assertEqual(info.copyright, '© 2010 Jamie Hardt')
|
||||
|
||||
@@ -5,5 +5,5 @@ Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.
|
||||
from .wave_reader import WavInfoReader
|
||||
from .riff_parser import WavInfoEOFError
|
||||
|
||||
__version__ = '2.2.1'
|
||||
__short_version__ = '2.2.1'
|
||||
__version__ = '2.3.0'
|
||||
__short_version__ = '2.3.0'
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import struct
|
||||
from collections import namedtuple
|
||||
|
||||
from . import riff_parser
|
||||
|
||||
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
|
||||
|
||||
|
||||
def parse_rf64(stream, signature = b'RF64'):
|
||||
# print("starting parse_rf64")
|
||||
def parse_rf64(stream, signature = b'RF64') -> RF64Context:
|
||||
start = stream.tell()
|
||||
assert( stream.read(4) == b'WAVE' )
|
||||
|
||||
ds64_chunk = riff_parser.parse_chunk(stream)
|
||||
assert type(ds64_chunk) is riff_parser.ChunkDescriptor, \
|
||||
f"Expected ds64 chunk here, found {type(ds64_chunk)}"
|
||||
|
||||
ds64_field_spec = "<QQQI"
|
||||
ds64_fields_size = struct.calcsize(ds64_field_spec)
|
||||
assert(ds64_chunk.ident == b'ds64')
|
||||
|
||||
ds64_data = ds64_chunk.read_data(stream)
|
||||
assert(len(ds64_data) >= ds64_fields_size )
|
||||
assert(len(ds64_data) >= ds64_fields_size)
|
||||
|
||||
# print("Read ds64 chunk: len()",len(ds64_data))
|
||||
riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec , ds64_data[0:ds64_fields_size] )
|
||||
riff_size, data_size, sample_count, length_lookup_table = struct.unpack(
|
||||
ds64_field_spec, ds64_data[0:ds64_fields_size])
|
||||
|
||||
bigchunk_table = {}
|
||||
chunksize64format = "<4sL"
|
||||
chunksize64size = struct.calcsize(chunksize64format)
|
||||
# print("Found chunks64s:", length_lookup_table)
|
||||
# chunksize64size = struct.calcsize(chunksize64format)
|
||||
|
||||
for n in range(length_lookup_table):
|
||||
bigname, bigsize = struct.unpack_from( chunksize64format , ds64_data, offset= ds64_fields_size )
|
||||
for _ in range(length_lookup_table):
|
||||
bigname, bigsize = struct.unpack_from(chunksize64format, ds64_data,
|
||||
offset= ds64_fields_size)
|
||||
bigchunk_table[bigname] = bigsize
|
||||
|
||||
bigchunk_table[b'data'] = data_size
|
||||
bigchunk_table[signature] = riff_size
|
||||
|
||||
stream.seek(start, 0)
|
||||
# print("returning from parse_rf64, context: ", RF64Context(sample_count=sample_count, bigchunk_table=bigchunk_table))
|
||||
return RF64Context( sample_count=sample_count, bigchunk_table=bigchunk_table )
|
||||
return RF64Context( sample_count=sample_count,
|
||||
bigchunk_table=bigchunk_table)
|
||||
|
||||
|
||||
@@ -12,21 +12,10 @@ class WavInfoEOFError(EOFError):
|
||||
|
||||
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
|
||||
pass
|
||||
# def find(self, chunk_path):
|
||||
# if len(chunk_path) > 1:
|
||||
# for chunk in self.children:
|
||||
# if type(chunk) is ListChunkDescriptor and \
|
||||
# chunk.signature is chunk_path[0]:
|
||||
# return chunk.find(chunk_path[1:])
|
||||
# else:
|
||||
# for chunk in self.children:
|
||||
# if type(chunk) is ChunkDescriptor and \
|
||||
# chunk.ident is chunk_path[0]:
|
||||
# return chunk
|
||||
|
||||
|
||||
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
|
||||
def read_data(self, from_stream):
|
||||
def read_data(self, from_stream) -> bytes:
|
||||
from_stream.seek(self.start)
|
||||
return from_stream.read(self.length)
|
||||
|
||||
@@ -59,15 +48,21 @@ def parse_chunk(stream, rf64_context=None):
|
||||
if rf64_context is None and ident in {b'RF64', b'BW64'}:
|
||||
rf64_context = parse_rf64(stream=stream, signature=ident)
|
||||
|
||||
assert rf64_context is not None, \
|
||||
f"Sentinel data size 0xFFFFFFFF found outside of RF64 context"
|
||||
|
||||
data_size = rf64_context.bigchunk_table[ident]
|
||||
|
||||
displacement = data_size
|
||||
if displacement % 2:
|
||||
displacement += 1
|
||||
|
||||
if ident in {b'RIFF', b'LIST', b'RF64', b'BW64'}:
|
||||
return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
|
||||
if ident in {b'RIFF', b'LIST', b'RF64', b'BW64', b'list'}:
|
||||
return parse_list_chunk(stream=stream, length=data_size,
|
||||
rf64_context=rf64_context)
|
||||
|
||||
else:
|
||||
data_start = stream.tell()
|
||||
stream.seek(displacement, 1)
|
||||
return ChunkDescriptor(ident=ident, start=data_start, length=data_size, rf64_context=rf64_context)
|
||||
return ChunkDescriptor(ident=ident, start=data_start, length=data_size,
|
||||
rf64_context=rf64_context)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from functools import reduce
|
||||
# from functools import reduce
|
||||
|
||||
|
||||
def binary_to_string(binary_value):
|
||||
return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
|
||||
# def binary_to_string(binary_value):
|
||||
# return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
|
||||
|
||||
|
||||
class UMIDParser:
|
||||
"""
|
||||
Parse a raw binary SMPTE 330M Universal Materials Identifier
|
||||
|
||||
This implementation is based on SMPTE ST 330:2011
|
||||
"""
|
||||
def __init__(self, raw_umid: bytes):
|
||||
self.raw_umid = raw_umid
|
||||
# class UMIDParser:
|
||||
# """
|
||||
# Parse a raw binary SMPTE 330M Universal Materials Identifier
|
||||
#
|
||||
# This implementation is based on SMPTE ST 330:2011
|
||||
# """
|
||||
# def __init__(self, raw_umid: bytes):
|
||||
# self.raw_umid = raw_umid
|
||||
#
|
||||
# @property
|
||||
# def universal_label(self) -> bytearray:
|
||||
@@ -22,8 +22,8 @@ class UMIDParser:
|
||||
# def basic_umid(self):
|
||||
# return self.raw_umid[0:32]
|
||||
|
||||
def basic_umid_to_str(self):
|
||||
return binary_to_string(self.raw_umid[0:32])
|
||||
# def basic_umid_to_str(self):
|
||||
# return binary_to_string(self.raw_umid[0:32])
|
||||
#
|
||||
# @property
|
||||
# def universal_label_is_valid(self) -> bool:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import struct
|
||||
from .umid_parser import UMIDParser
|
||||
# from .umid_parser import UMIDParser
|
||||
|
||||
from typing import Optional
|
||||
|
||||
@@ -80,11 +80,12 @@ class WavBextReader:
|
||||
self.max_shortterm_loudness = unpacked[12] / 100.0
|
||||
|
||||
def to_dict(self):
|
||||
if self.umid is not None:
|
||||
umid_parsed = UMIDParser(self.umid)
|
||||
umid_str = umid_parsed.basic_umid_to_str()
|
||||
else:
|
||||
umid_str = None
|
||||
# if self.umid is not None:
|
||||
# umid_parsed = UMIDParser(self.umid)
|
||||
# umid_str = umid_parsed.basic_umid_to_str()
|
||||
# else:
|
||||
|
||||
umid_str = None
|
||||
|
||||
return {'description': self.description,
|
||||
'originator': self.originator,
|
||||
|
||||
272
wavinfo/wave_cues_reader.py
Normal file
272
wavinfo/wave_cues_reader.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
Cues metadata
|
||||
|
||||
For reference on implementation of cues and related metadata see:
|
||||
August 1991, "Multimedia Programming Interface and Data Specifications 1.0",
|
||||
IBM Corporation and Microsoft Corporation
|
||||
|
||||
https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import encodings
|
||||
from .riff_parser import ChunkDescriptor
|
||||
|
||||
from struct import unpack, calcsize
|
||||
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.
|
||||
CountryCodes = """000 None Indicated
|
||||
001,USA
|
||||
002,Canada
|
||||
003,Latin America
|
||||
030,Greece
|
||||
031,Netherlands
|
||||
032,Belgium
|
||||
033,France
|
||||
034,Spain
|
||||
039,Italy
|
||||
041,Switzerland
|
||||
043,Austria
|
||||
044,United Kingdom
|
||||
045,Denmark
|
||||
046,Sweden
|
||||
047,Norway
|
||||
049,West Germany
|
||||
052,Mexico
|
||||
055,Brazil
|
||||
061,Australia
|
||||
064,New Zealand
|
||||
081,Japan
|
||||
082,Korea
|
||||
086,People’s Republic of China
|
||||
088,Taiwan
|
||||
090,Turkey
|
||||
351,Portugal
|
||||
352,Luxembourg
|
||||
354,Iceland
|
||||
358,Finland"""
|
||||
|
||||
#: Language and Dialect codes used in the RIFF standard to resolve native
|
||||
#: language of text fields. These codes appear in CSET and LTXT metadata.
|
||||
LanguageDialectCodes = """0 0 None Indicated
|
||||
1,1,Arabic
|
||||
2,1,Bulgarian
|
||||
3,1,Catalan
|
||||
4,1,Traditional Chinese
|
||||
4,2,Simplified Chinese
|
||||
5,1,Czech
|
||||
6,1,Danish
|
||||
7,1,German
|
||||
7,2,Swiss German
|
||||
8,1,Greek
|
||||
9,1,US English
|
||||
9,2,UK English
|
||||
10,1,Spanish
|
||||
10,2,Spanish Mexican
|
||||
11,1,Finnish
|
||||
12,1,French
|
||||
12,2,Belgian French
|
||||
12,3,Canadian French
|
||||
12,4,Swiss French
|
||||
13,1,Hebrew
|
||||
14,1,Hungarian
|
||||
15,1,Icelandic
|
||||
16,1,Italian
|
||||
16,2,Swiss Italian
|
||||
17,1,Japanese
|
||||
18,1,Korean
|
||||
19,1,Dutch
|
||||
19,2,Belgian Dutch
|
||||
20,1,Norwegian - Bokmal
|
||||
20,2,Norwegian - Nynorsk
|
||||
21,1,Polish
|
||||
22,1,Brazilian Portuguese
|
||||
22,2,Portuguese
|
||||
23,1,Rhaeto-Romanic
|
||||
24,1,Romanian
|
||||
25,1,Russian
|
||||
26,1,Serbo-Croatian (Latin)
|
||||
26,2,Serbo-Croatian (Cyrillic)
|
||||
27,1,Slovak
|
||||
28,1,Albanian
|
||||
29,1,Swedish
|
||||
30,1,Thai
|
||||
31,1,Turkish
|
||||
32,1,Urdu
|
||||
33,1,Bahasa"""
|
||||
|
||||
|
||||
class CueEntry(NamedTuple):
|
||||
name: int
|
||||
position: int
|
||||
chunk_id: bytes
|
||||
chunk_start: int
|
||||
block_start: int
|
||||
sample_offset: int
|
||||
|
||||
Format = "<II4sIII"
|
||||
|
||||
@classmethod
|
||||
def format_size(cls) -> int:
|
||||
return calcsize(cls.Format)
|
||||
|
||||
@classmethod
|
||||
def read(cls, data: bytes) -> 'CueEntry':
|
||||
assert len(data) == cls.format_size(), \
|
||||
f"cue data size incorrect, expected {calcsize(cls.Format)} found {len(data)}"
|
||||
|
||||
parsed = unpack(cls.Format, data)
|
||||
|
||||
return cls(name=parsed[0], position=parsed[1], chunk_id=parsed[2],
|
||||
chunk_start=parsed[3], block_start=parsed[4],
|
||||
sample_offset=parsed[5])
|
||||
|
||||
|
||||
class LabelEntry(NamedTuple):
|
||||
name: int
|
||||
text: str
|
||||
|
||||
@classmethod
|
||||
def read(cls, data: bytes, encoding: str):
|
||||
return cls(name=unpack("<I", data[0:4])[0],
|
||||
text=data[4:].decode(encoding).rstrip("\0"))
|
||||
|
||||
|
||||
NoteEntry = LabelEntry
|
||||
|
||||
|
||||
class RangeLabel(NamedTuple):
|
||||
name: int
|
||||
length: int
|
||||
purpose: str
|
||||
country: int
|
||||
language: int
|
||||
dialect: int
|
||||
codepage: int
|
||||
text: str
|
||||
|
||||
@classmethod
|
||||
def read(cls, data: bytes, fallback_encoding: str):
|
||||
leader_struct_fmt = "<II4sHHHH"
|
||||
parsed = unpack(leader_struct_fmt, data[0:calcsize(leader_struct_fmt)])
|
||||
text_data = data[calcsize(leader_struct_fmt):]
|
||||
|
||||
if data[6] != 0:
|
||||
fallback_encoding = f"cp{data[6]}"
|
||||
|
||||
return cls(name=parsed[0], length=parsed[1], purpose=parsed[2],
|
||||
country=parsed[3], language=parsed[4],
|
||||
dialect=parsed[5], codepage=parsed[6],
|
||||
text=text_data.decode(fallback_encoding))
|
||||
|
||||
|
||||
@dataclass
|
||||
class WavCuesReader:
|
||||
cues: List[CueEntry]
|
||||
labels: List[LabelEntry]
|
||||
ranges: List[RangeLabel]
|
||||
notes: List[NoteEntry]
|
||||
|
||||
@classmethod
|
||||
def read_all(cls, f,
|
||||
cues: Optional[ChunkDescriptor],
|
||||
labls: List[ChunkDescriptor],
|
||||
ltxts: List[ChunkDescriptor],
|
||||
notes: List[ChunkDescriptor],
|
||||
fallback_encoding: str) -> 'WavCuesReader':
|
||||
|
||||
cue_list = []
|
||||
if cues is not None:
|
||||
cues_data = cues.read_data(f)
|
||||
assert len(cues_data) >= 4, "cue metadata too short"
|
||||
offset = calcsize("<I")
|
||||
cues_count = unpack("<I", cues_data[0:offset])
|
||||
|
||||
for _ in range(cues_count[0]):
|
||||
cue_bytes = cues_data[offset: offset + CueEntry.format_size()]
|
||||
cue_list.append(CueEntry.read(cue_bytes))
|
||||
offset += CueEntry.format_size()
|
||||
|
||||
label_list = []
|
||||
for labl in labls:
|
||||
label_list.append(
|
||||
LabelEntry.read(labl.read_data(f),
|
||||
encoding=fallback_encoding)
|
||||
)
|
||||
|
||||
range_list = []
|
||||
for r in ltxts:
|
||||
range_list.append(
|
||||
RangeLabel.read(r.read_data(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, 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]:
|
||||
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])
|
||||
|
||||
|
||||
|
||||
@@ -547,10 +547,10 @@ class DolbyAtmosSupplementalMetadata:
|
||||
render_modes = []
|
||||
|
||||
h = BytesIO(data)
|
||||
magic = unpack("<I", h.read(4))
|
||||
magic = unpack("<I", h.read(4))[0]
|
||||
assert magic == cls.MAGIC, "Magic value was not found"
|
||||
|
||||
object_count = unpack("<H", h.read(2))
|
||||
object_count = unpack("<H", h.read(2))[0]
|
||||
|
||||
h.read(1) #skip 1
|
||||
|
||||
@@ -563,7 +563,7 @@ class DolbyAtmosSupplementalMetadata:
|
||||
h.read(object_count) # skip object_count bytes
|
||||
|
||||
for _ in range(object_count):
|
||||
binaural_mode = unpack("B", h.read(1))
|
||||
binaural_mode = unpack("B", h.read(1))[0]
|
||||
binaural_mode &= 0x7
|
||||
render_modes.append(binaural_mode)
|
||||
|
||||
@@ -582,7 +582,7 @@ class WavDolbyMetadataReader:
|
||||
#: indicating if the segment's checksum was valid, and the
|
||||
#: segment's parsed dataclass (or a `bytes` array if it was
|
||||
#: not recognized).
|
||||
segment_list: Tuple[Union[SegmentType, int], bool, Any]
|
||||
segment_list: List[Tuple[Union[SegmentType, int], bool, Any]]
|
||||
|
||||
version: Tuple[int,int,int,int]
|
||||
|
||||
@@ -625,8 +625,8 @@ class WavDolbyMetadataReader:
|
||||
segment = DolbyDigitalPlusMetadata.load(segment)
|
||||
elif stype == SegmentType.DolbyAtmos:
|
||||
segment = DolbyAtmosMetadata.load(segment)
|
||||
# elif stype == SegmentType.DolbyAtmosSupplemental:
|
||||
# segment = DolbyAtmosSupplementalMetadata.load(segment)
|
||||
elif stype == SegmentType.DolbyAtmosSupplemental:
|
||||
segment = DolbyAtmosSupplementalMetadata.load(segment)
|
||||
|
||||
self.segment_list.append( (stype, checksum == expected_checksum, segment) )
|
||||
|
||||
@@ -644,12 +644,12 @@ class WavDolbyMetadataReader:
|
||||
return [x[2] for x in self.segment_list \
|
||||
if x[0] == SegmentType.DolbyAtmos and x[1]]
|
||||
|
||||
# def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
||||
# """
|
||||
# Every valid Dolby Atmos Supplemental metadata segment in the file.
|
||||
# """
|
||||
# return [x[2] for x in self.segment_list \
|
||||
# if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
||||
def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]:
|
||||
"""
|
||||
Every valid Dolby Atmos Supplemental metadata segment in the file.
|
||||
"""
|
||||
return [x[2] for x in self.segment_list \
|
||||
if x[0] == SegmentType.DolbyAtmosSupplemental and x[1]]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class SteinbergMetadata:
|
||||
AURO_13_0 = 41
|
||||
AURO_13_1 = 42
|
||||
|
||||
Steinberg_xpath = "//BWFXML/STEINBERG"
|
||||
Steinberg_xpath = "./STEINBERG"
|
||||
|
||||
@classmethod
|
||||
def present(cls, xml: ET.ElementTree) -> bool:
|
||||
@@ -58,53 +58,59 @@ class SteinbergMetadata:
|
||||
:param xml: an iXML ElementTree
|
||||
"""
|
||||
x = xml.find(cls.Steinberg_xpath)
|
||||
return len(x) > 0
|
||||
return x is not None
|
||||
|
||||
def __init__(self, xml: ET.ElementTree) -> None:
|
||||
"""
|
||||
Parse Steinberg iXML data.
|
||||
:param xml: The entire iXML Tree
|
||||
"""
|
||||
self.parsed = xml.find("//BWFXML/STEINBERG")
|
||||
self.parsed = xml.find(self.Steinberg_xpath)
|
||||
|
||||
@property
|
||||
def audio_speaker_arrangement(self) -> Optional[AudioSpeakerArrangement]:
|
||||
"""
|
||||
`AudioSpeakerArrangement` property
|
||||
"""
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME/text() = 'AudioSpeakerArrangement']/VALUE/text()")
|
||||
if len(val) > 0:
|
||||
return type(self).AudioSpeakerArrangement(int(val[0]))
|
||||
else:
|
||||
return None
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSpeakerArrangement']/VALUE")
|
||||
if val is not None:
|
||||
return type(self).AudioSpeakerArrangement(int(val.text))
|
||||
|
||||
@property
|
||||
def sample_format_size(self) -> Optional[int]:
|
||||
"""
|
||||
AudioSampleFormatSize
|
||||
"""
|
||||
pass
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'AudioSampleFormatSize']/VALUE")
|
||||
if val is not None:
|
||||
return int(val.text)
|
||||
|
||||
@property
|
||||
def media_company(self) -> Optional[str]:
|
||||
"""
|
||||
MediaCompany
|
||||
"""
|
||||
pass
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaCompany']/VALUE")
|
||||
if val is not None:
|
||||
return val.text
|
||||
|
||||
@property
|
||||
def media_drop_frames(self) -> Optional[bool]:
|
||||
"""
|
||||
MediaDropFrames
|
||||
"""
|
||||
pass
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDropFrames']/VALUE")
|
||||
if val is not None:
|
||||
return val.text == "1"
|
||||
|
||||
@property
|
||||
def media_duration(self) -> Optional[float]:
|
||||
"""
|
||||
MediaDuration
|
||||
"""
|
||||
pass
|
||||
val = self.parsed.find("./ATTR_LIST/ATTR[NAME = 'MediaDuration']/VALUE")
|
||||
if val is not None:
|
||||
return float(val.text)
|
||||
|
||||
@property
|
||||
def media_start_time(self) -> Optional[float]:
|
||||
|
||||
@@ -13,20 +13,20 @@ from .wave_bext_reader import WavBextReader
|
||||
from .wave_info_reader import WavInfoChunkReader
|
||||
from .wave_adm_reader import WavADMReader
|
||||
from .wave_dbmd_reader import WavDolbyMetadataReader
|
||||
from .wave_cues_reader import WavCuesReader
|
||||
|
||||
#: Calculated statistics about the audio data.
|
||||
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
|
||||
|
||||
#: The format of the audio samples.
|
||||
WavAudioFormat = namedtuple('WavAudioFormat',
|
||||
'audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
|
||||
['audio_format', 'channel_count', 'sample_rate',
|
||||
'byte_rate', 'block_align', 'bits_per_sample'])
|
||||
|
||||
|
||||
class WavInfoReader:
|
||||
"""
|
||||
Parse a WAV audio file for metadata.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
|
||||
@@ -38,8 +38,8 @@ class WavInfoReader:
|
||||
file handle to an open file.
|
||||
|
||||
:param info_encoding:
|
||||
The text encoding of the INFO metadata fields.
|
||||
latin_1/Win CP1252 has always been a pretty good guess for this.
|
||||
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
|
||||
@@ -71,6 +71,9 @@ class WavInfoReader:
|
||||
#: RIFF INFO metadata.
|
||||
self.info :Optional[WavInfoChunkReader]= None
|
||||
|
||||
#: RIFF cues markers, labels, and notes.
|
||||
self.cues :Optional[WavCuesReader] = None
|
||||
|
||||
if hasattr(path, 'read'):
|
||||
self.get_wav_info(path)
|
||||
self.url = 'about:blank'
|
||||
@@ -80,7 +83,7 @@ class WavInfoReader:
|
||||
absolute_path = os.path.abspath(path)
|
||||
|
||||
#: `file://` url for the file.
|
||||
self.url: pathlib.Path = pathlib.Path(absolute_path).as_uri()
|
||||
self.url: str = pathlib.Path(absolute_path).as_uri()
|
||||
|
||||
self.path = absolute_path
|
||||
|
||||
@@ -89,6 +92,7 @@ class WavInfoReader:
|
||||
|
||||
def get_wav_info(self, wavfile):
|
||||
chunks = parse_chunk(wavfile)
|
||||
assert type(chunks) is ListChunkDescriptor
|
||||
|
||||
self.main_list = chunks.children
|
||||
wavfile.seek(0)
|
||||
@@ -99,41 +103,44 @@ class WavInfoReader:
|
||||
self.adm = self._get_adm(wavfile)
|
||||
self.info = self._get_info(wavfile, encoding=self.info_encoding)
|
||||
self.dolby = self._get_dbmd(wavfile)
|
||||
self.cues = self._get_cue(wavfile)
|
||||
self.data = self._describe_data()
|
||||
|
||||
def _find_chunk_data(self, ident, from_stream, default_none=False):
|
||||
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor and chunk.ident == ident)
|
||||
chunk_descriptor = next(top_chunks, None) if default_none else next(top_chunks)
|
||||
return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None
|
||||
def _find_chunk_data(self, ident, from_stream, default_none=False) -> Optional[bytes]:
|
||||
top_chunks = (chunk for chunk in self.main_list \
|
||||
if type(chunk) is ChunkDescriptor and chunk.ident == ident)
|
||||
|
||||
chunk_descriptor = next(top_chunks, None) \
|
||||
if default_none else next(top_chunks)
|
||||
|
||||
return chunk_descriptor.read_data(from_stream) \
|
||||
if chunk_descriptor else None
|
||||
|
||||
def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]:
|
||||
top_chunks = (chunk for chunk in self.main_list \
|
||||
if type(chunk) is ListChunkDescriptor and \
|
||||
chunk.signature == signature)
|
||||
|
||||
return next(top_chunks, None)
|
||||
|
||||
def _describe_data(self):
|
||||
data_chunk = next(c for c in self.main_list if type(c) is ChunkDescriptor and c.ident == b'data')
|
||||
data_chunk = next(c for c in self.main_list \
|
||||
if type(c) is ChunkDescriptor and c.ident == b'data')
|
||||
|
||||
return WavDataDescriptor(byte_count=data_chunk.length,
|
||||
frame_count=int(data_chunk.length / self.fmt.block_align))
|
||||
assert isinstance(self.fmt, WavAudioFormat)
|
||||
return WavDataDescriptor(
|
||||
byte_count=data_chunk.length,
|
||||
frame_count=int(data_chunk.length / self.fmt.block_align))
|
||||
|
||||
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],
|
||||
@@ -143,7 +150,8 @@ class WavInfoReader:
|
||||
)
|
||||
|
||||
def _get_info(self, f, encoding):
|
||||
finder = (chunk.signature for chunk in self.main_list if type(chunk) is ListChunkDescriptor)
|
||||
finder = (chunk.signature for chunk in self.main_list \
|
||||
if type(chunk) is ListChunkDescriptor)
|
||||
|
||||
if b'INFO' in finder:
|
||||
return WavInfoChunkReader(f, encoding)
|
||||
@@ -155,26 +163,46 @@ class WavInfoReader:
|
||||
def _get_adm(self, f):
|
||||
axml = self._find_chunk_data(b'axml', f, default_none=True)
|
||||
chna = self._find_chunk_data(b'chna', f, default_none=True)
|
||||
return WavADMReader(axml_data=axml, chna_data=chna) if axml and chna else None
|
||||
return WavADMReader(axml_data=axml, chna_data=chna) \
|
||||
if axml and chna else None
|
||||
|
||||
def _get_dbmd(self, f):
|
||||
dbmd_data = self._find_chunk_data(b'dbmd', f, default_none=True)
|
||||
return WavDolbyMetadataReader(dbmd_data=dbmd_data) if dbmd_data else None
|
||||
return WavDolbyMetadataReader(dbmd_data=dbmd_data) \
|
||||
if dbmd_data else None
|
||||
|
||||
def _get_ixml(self, f):
|
||||
ixml_data = self._find_chunk_data(b'iXML', f, default_none=True)
|
||||
return WavIXMLFormat(ixml_data.rstrip(b'\0')) if ixml_data else None
|
||||
|
||||
def _get_cue(self, f):
|
||||
cue = next((cue_chunk for cue_chunk in self.main_list if \
|
||||
type(cue_chunk) is ChunkDescriptor and \
|
||||
cue_chunk.ident == b'cue '), None)
|
||||
|
||||
adtl = self._find_list_chunk(b'adtl')
|
||||
labls = []
|
||||
ltxts = []
|
||||
notes = []
|
||||
if adtl is not None:
|
||||
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.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()"
|
||||
"""
|
||||
Walk all of the available metadata fields.
|
||||
|
||||
:yields: tuples of the *scope*, *key*, and *value* of
|
||||
each metadatum. The *scope* value will be one of
|
||||
"fmt", "data", "ixml", "bext", "info", "dolby", or "adm".
|
||||
"fmt", "data", "ixml", "bext", "info", "dolby", "cues" or "adm".
|
||||
"""
|
||||
|
||||
scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', '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