mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 17:00:41 +00:00
Compare commits
21 Commits
maint-poet
...
feature-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2830cb87a4 | ||
|
|
1c8581ff35 | ||
|
|
1d499d9741 | ||
|
|
299f79aeb3 | ||
|
|
a46590df29 | ||
|
|
c6f66b2d6e | ||
|
|
b8617a35e2 | ||
|
|
8cabf948ff | ||
|
|
8a755b4466 | ||
|
|
c13b07e4a3 | ||
|
|
ac37c14b3d | ||
|
|
36e4a02ab8 | ||
|
|
ffc0c48af7 | ||
|
|
206962b218 | ||
|
|
d560e5a9f0 | ||
|
|
98ca1ec462 | ||
|
|
f0353abd4e | ||
|
|
6304666d11 | ||
|
|
d2b0c68dd2 | ||
|
|
a0a9c38cb4 | ||
|
|
f68eea4cd9 |
@@ -31,6 +31,7 @@ it is not supported, please submit an issue!
|
||||
and Dolby Atmos `dbmd` metadata for re-renders and mixdowns.
|
||||
* Wave embedded [cue markers][cues], cue marker labels, notes and timed ranges as used
|
||||
by Zoom, iZotope RX, etc.
|
||||
* Wave embedded [sampler][smpl] and sample loop metadata.
|
||||
* The [wav format][format] is also parsed, so you can access the basic sample rate
|
||||
and channel count information.
|
||||
|
||||
@@ -38,6 +39,7 @@ it is not supported, please submit an issue!
|
||||
[format]:https://wavinfo.readthedocs.io/en/latest/classes.html#wavinfo.wave_reader.WavAudioFormat
|
||||
[cues]:https://wavinfo.readthedocs.io/en/latest/scopes/cue.html
|
||||
[bext]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html
|
||||
[smpl]:https://wavinfo.readthedocs.io/en/latest/scopes/smpl.html
|
||||
[smpte_330m2011]:https://wavinfo.readthedocs.io/en/latest/scopes/bext.html#wavinfo.wave_bext_reader.WavBextReader.umid
|
||||
[adm]:https://wavinfo.readthedocs.io/en/latest/scopes/adm.html
|
||||
[ebu3285s6]:https://wavinfo.readthedocs.io/en/latest/scopes/dolby.html
|
||||
|
||||
@@ -6,89 +6,135 @@ from the command line and output metadata to stdout.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ wavinfo [--ixml | --adm] INFILE +
|
||||
|
||||
By default, `wavinfo` will output a JSON dictionary for each file argument.
|
||||
$ wavinfo [[-i] | [--ixml | --adm]] INFILE +
|
||||
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
Two option flags will change the behavior of the command:
|
||||
By default, `wavinfo` will output a JSON dictionary for each file argument.
|
||||
|
||||
``-i``
|
||||
`wavinfo` will run in `interactive mode`_.
|
||||
|
||||
Two option flags will change the behavior of the command in non-interactive
|
||||
mode:
|
||||
|
||||
``--ixml``
|
||||
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload
|
||||
of each input wave file, or will emit an error message to stderr if iXML
|
||||
metadata is not present.
|
||||
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata
|
||||
payload of each input wave file, or will emit an error message to stderr if
|
||||
iXML metadata is not present.
|
||||
|
||||
``--adm``
|
||||
The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata
|
||||
payload of each input wave file, or will emit an error message to stderr if
|
||||
ADM XML metadata is not present.
|
||||
|
||||
These options are mutually-exclusive, with `\-\-adm` taking precedence.
|
||||
These options are mutually-exclusive, with `\-\-adm` taking precedence. The
|
||||
``--ixml`` and ``--adm`` flags futher take precedence over ``-i``.
|
||||
|
||||
|
||||
Interactive Mode
|
||||
-----------------
|
||||
|
||||
In interactive mode, `wavinfo` will present a command prompt which allows you
|
||||
to query the files provided on the command line and explore the metadata tree
|
||||
interactively. Each file on the command line is scanned and presented as a
|
||||
tree of metadata records.
|
||||
|
||||
Commands include:
|
||||
|
||||
``ls``
|
||||
List the available metadata keys at the current level.
|
||||
|
||||
``cd``
|
||||
Traverse to a metadata key in the current level (or enter `..` to go up
|
||||
to the prevvious level).
|
||||
|
||||
``bye``
|
||||
Exit to the shell.
|
||||
|
||||
Type `help` or `?` at the prompt to get a full list of commands.
|
||||
|
||||
|
||||
Example Output
|
||||
--------------
|
||||
|
||||
.. attention::
|
||||
|
||||
Metadata fields containing binary data, such as the Broadcast-WAV UMID, will
|
||||
be included in the JSON output as a base-64 encoded string, preceded by the
|
||||
marker "base64:".
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
{
|
||||
"filename": "tests/test_files/sounddevices/A101_1.WAV",
|
||||
"run_date": "2022-11-26T17:56:38.342935",
|
||||
"application": "wavinfo 2.1.0",
|
||||
"scopes": {
|
||||
"fmt": {
|
||||
"audio_format": 1,
|
||||
"channel_count": 2,
|
||||
"sample_rate": 48000,
|
||||
"byte_rate": 288000,
|
||||
"block_align": 6,
|
||||
"bits_per_sample": 24
|
||||
{
|
||||
"filename": "../tests/test_files/nuendo/wavinfo Test Project - Audio - 1OA.wav",
|
||||
"run_date": "2024-11-25T10:26:11.280053",
|
||||
"application": "wavinfo 3.0.0",
|
||||
"scopes": {
|
||||
"fmt": {
|
||||
"audio_format": 65534,
|
||||
"channel_count": 4,
|
||||
"sample_rate": 48000,
|
||||
"byte_rate": 576000,
|
||||
"block_align": 12,
|
||||
"bits_per_sample": 24
|
||||
},
|
||||
"data": {
|
||||
"byte_count": 576000,
|
||||
"frame_count": 48000
|
||||
},
|
||||
"ixml": {
|
||||
"track_list": [
|
||||
{
|
||||
"channel_index": "1",
|
||||
"interleave_index": "1",
|
||||
"name": "",
|
||||
"function": "ACN0-FOA"
|
||||
},
|
||||
"data": {
|
||||
"byte_count": 1441434,
|
||||
"frame_count": 240239
|
||||
{
|
||||
"channel_index": "2",
|
||||
"interleave_index": "2",
|
||||
"name": "",
|
||||
"function": "ACN1-FOA"
|
||||
},
|
||||
"ixml": {
|
||||
"track_list": [
|
||||
{
|
||||
"channel_index": "1",
|
||||
"interleave_index": "1",
|
||||
"name": "MKH516 A",
|
||||
"function": ""
|
||||
},
|
||||
{
|
||||
"channel_index": "2",
|
||||
"interleave_index": "2",
|
||||
"name": "Boom",
|
||||
"function": ""
|
||||
}
|
||||
],
|
||||
"project": "BMH",
|
||||
"scene": "A101",
|
||||
"take": "1",
|
||||
"tape": "18Y12M31",
|
||||
"family_uid": "USSDVGR1112089007124001008206300",
|
||||
"family_name": null
|
||||
{
|
||||
"channel_index": "3",
|
||||
"interleave_index": "3",
|
||||
"name": "",
|
||||
"function": "ACN2-FOA"
|
||||
},
|
||||
"bext": {
|
||||
"description": "sSPEED=023.976-ND\r\nsTAKE=1\r\nsUBITS=$12311801\r\nsSWVER=2.67\r\nsPROJECT=BMH\r\nsSCENE=A101\r\nsFILENAME=A101_1.WAV\r\nsTAPE=18Y12M31\r\nsTRK1=MKH516 A\r\nsTRK2=Boom\r\nsNOTE=\r\n",
|
||||
"originator": "Sound Dev: 702T S#GR1112089007",
|
||||
"originator_ref": "USSDVGR1112089007124001008206301",
|
||||
"originator_date": "2018-12-31",
|
||||
"originator_time": "12:40:00",
|
||||
"time_reference": 2190940753,
|
||||
"version": 1,
|
||||
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
|
||||
"loudness_value": null,
|
||||
"loudness_range": null,
|
||||
"max_true_peak": null,
|
||||
"max_momentary_loudness": null,
|
||||
"max_shortterm_loudness": null
|
||||
{
|
||||
"channel_index": "4",
|
||||
"interleave_index": "4",
|
||||
"name": "",
|
||||
"function": "ACN3-FOA"
|
||||
}
|
||||
],
|
||||
"project": "wavinfo Test Project",
|
||||
"scene": null,
|
||||
"take": null,
|
||||
"tape": null,
|
||||
"family_uid": "E5DDE719B9484A758162FF7B652383A3",
|
||||
"family_name": null
|
||||
},
|
||||
"bext": {
|
||||
"description": "wavinfo Test Project Nuendo output",
|
||||
"originator": "Nuendo",
|
||||
"originator_ref": "USJPHNNNNNNNNN202829RRRRRRRRR",
|
||||
"originator_date": "2022-12-02",
|
||||
"originator_time": "10:21:06",
|
||||
"time_reference": 172800000,
|
||||
"version": 2,
|
||||
"umid": "base64:k/zr4qE4RiaXyd/fO7GuCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"coding_history": "A=PCM,F=48000,W=24,T=Nuendo\r\n",
|
||||
"loudness_value": 327.67,
|
||||
"loudness_range": 327.67,
|
||||
"max_true_peak": 327.67,
|
||||
"max_momentary_loudness": 327.67,
|
||||
"max_shortterm_loudness": 327.67
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,24 +12,25 @@
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
sys.path.insert(0, os.path.abspath("../../.."))
|
||||
print(sys.path)
|
||||
|
||||
import wavinfo
|
||||
import importlib
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'wavinfo'
|
||||
copyright = u'2018-2023, Jamie Hardt'
|
||||
copyright = u'2018-2024, Jamie Hardt'
|
||||
author = u'Jamie Hardt'
|
||||
|
||||
# The short X.Y version
|
||||
version = wavinfo.__short_version__
|
||||
version = "3.1"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = wavinfo.__version__
|
||||
release = importlib.metadata.version("wavinfo")
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -39,7 +39,7 @@ iXML
|
||||
Sampler Metadata
|
||||
----------------
|
||||
|
||||
* `RecordingBlogs.com — Sample chunk (of a Wave file)<https://www.recordingblogs.com/wiki/sample-chunk-of-a-wave-file>`_
|
||||
* `RecordingBlogs.com — Sample chunk (of a Wave file) <https://www.recordingblogs.com/wiki/sample-chunk-of-a-wave-file>`_
|
||||
|
||||
RIFF Metadata
|
||||
-------------
|
||||
|
||||
14
docs/source/scopes/smpl.rst
Normal file
14
docs/source/scopes/smpl.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
Sampler Metadata
|
||||
=================
|
||||
|
||||
Class Reference
|
||||
---------------
|
||||
|
||||
.. automodule:: wavinfo.wave_smpl_reader
|
||||
|
||||
.. autoclass:: wavinfo.wave_smpl_reader.WavSmplReader
|
||||
:members:
|
||||
|
||||
.. autoclass:: wavinfo.wave_smpl_reader.WaveSmplLoop
|
||||
:members:
|
||||
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "wavinfo"
|
||||
version = "3.0.1"
|
||||
version = "3.1.0"
|
||||
description = "Probe WAVE files for all metadata"
|
||||
authors = ["Jamie Hardt <jamiehardt@me.com>"]
|
||||
license = "MIT"
|
||||
|
||||
BIN
tests/test_files/smpl/alarm_citizen_loop1_udata.wav
Normal file
BIN
tests/test_files/smpl/alarm_citizen_loop1_udata.wav
Normal file
Binary file not shown.
@@ -8,6 +8,9 @@ import json
|
||||
from enum import Enum
|
||||
import importlib.metadata
|
||||
from base64 import b64encode
|
||||
from cmd import Cmd
|
||||
from shlex import split
|
||||
from typing import List, Dict, Union
|
||||
|
||||
|
||||
class MyJSONEncoder(json.JSONEncoder):
|
||||
@@ -15,7 +18,7 @@ class MyJSONEncoder(json.JSONEncoder):
|
||||
if isinstance(o, Enum):
|
||||
return o._name_
|
||||
elif isinstance(o, bytes):
|
||||
return b64encode(o).decode('ascii')
|
||||
return 'base64:' + b64encode(o).decode('ascii')
|
||||
else:
|
||||
return super().default(o)
|
||||
|
||||
@@ -24,6 +27,85 @@ class MissingDataError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class MetaBrowser(Cmd):
|
||||
prompt = "(wavinfo) "
|
||||
|
||||
metadata: Union[List, Dict]
|
||||
path: List[str] = []
|
||||
|
||||
@property
|
||||
def cwd(self):
|
||||
root: List | Dict = self.metadata
|
||||
for key in self.path:
|
||||
if isinstance(root, list):
|
||||
root = root[int(key)]
|
||||
else:
|
||||
root = root[key]
|
||||
|
||||
return root
|
||||
|
||||
@staticmethod
|
||||
def print_value(collection, key):
|
||||
val = collection[key]
|
||||
if isinstance(val, int):
|
||||
print(f" - {key}: {val}")
|
||||
elif isinstance(val, str):
|
||||
print(f" - {key}: \"{val}\"")
|
||||
elif isinstance(val, dict):
|
||||
print(f" - {key}: Dict ({len(val)} keys)")
|
||||
elif isinstance(val, list):
|
||||
print(f" - {key}: List ({len(val)} keys)")
|
||||
elif isinstance(val, bytes):
|
||||
print(f" - {key}: ({len(val)} bytes)")
|
||||
elif val is None:
|
||||
print(f" - {key}: (NO VALUE)")
|
||||
else:
|
||||
print(f" - {key}: Unknown")
|
||||
|
||||
def do_ls(self, _):
|
||||
'List items at the current node: LS'
|
||||
root = self.cwd
|
||||
|
||||
if isinstance(root, list):
|
||||
print("List:")
|
||||
for i in range(len(root)):
|
||||
self.print_value(root, i)
|
||||
|
||||
elif isinstance(root, dict):
|
||||
print("Dictionary:")
|
||||
for key in root:
|
||||
self.print_value(root, key)
|
||||
|
||||
else:
|
||||
print("Cannot print node, is not a list or dictionary.")
|
||||
|
||||
def do_cd(self, args):
|
||||
'Switch to a different node: CD node-name | ".."'
|
||||
argv = split(args)
|
||||
if argv[0] == "..":
|
||||
self.path = self.path[0:-1]
|
||||
else:
|
||||
if isinstance(self.cwd, list):
|
||||
if int(argv[0]) < len(self.cwd):
|
||||
self.path = self.path + [argv[0]]
|
||||
else:
|
||||
print(f"Index {argv[0]} does not exist")
|
||||
elif isinstance(self.cwd, dict):
|
||||
if argv[0] in self.cwd.keys():
|
||||
self.path = self.path + [argv[0]]
|
||||
else:
|
||||
print(f"Key \"{argv[0]}\" does not exist")
|
||||
|
||||
if len(self.path) > 0:
|
||||
self.prompt = "(" + "/".join(self.path) + ") "
|
||||
else:
|
||||
self.prompt = "(wavinfo) "
|
||||
|
||||
def do_bye(self, _):
|
||||
'Exit the interactive browser: BYE'
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
version = importlib.metadata.version('wavinfo')
|
||||
manpath = os.path.dirname(__file__) + "/man"
|
||||
@@ -51,8 +133,15 @@ def main():
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
parser.add_option('-i',
|
||||
help='Read metadata with an interactive prompt',
|
||||
default=False,
|
||||
action='store_true')
|
||||
|
||||
(options, args) = parser.parse_args(sys.argv)
|
||||
|
||||
interactive_dict = []
|
||||
|
||||
# if options.install_manpages:
|
||||
# print("Installing manpages...")
|
||||
# print(f"Docfiles at {__file__}")
|
||||
@@ -98,7 +187,12 @@ def main():
|
||||
|
||||
ret_dict['scopes'][scope][name] = value
|
||||
|
||||
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
|
||||
if options.i:
|
||||
interactive_dict.append(ret_dict)
|
||||
else:
|
||||
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout,
|
||||
indent=2)
|
||||
|
||||
except MissingDataError as e:
|
||||
print("MissingDataError: Missing metadata (%s) in file %s" %
|
||||
(e, arg), file=sys.stderr)
|
||||
@@ -106,6 +200,11 @@ def main():
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
if len(interactive_dict) > 0:
|
||||
cli = MetaBrowser()
|
||||
cli.metadata = interactive_dict
|
||||
cli.cmdloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
wavinfo \- probe wave files for metadata
|
||||
.SH SYNOPSIS
|
||||
.SY wavinfo
|
||||
.I "[\-i]"
|
||||
.I "[\-\-adm]"
|
||||
.I "[\-\-ixml]"
|
||||
.I FILE ...
|
||||
@@ -24,6 +25,10 @@ Output any iXML metdata in
|
||||
.BR FILE .
|
||||
.IP "\-h, \-\-help"
|
||||
Print brief help.
|
||||
.IP "\-i"
|
||||
Enter
|
||||
.I "interactive mode"
|
||||
and browse metadata in FILE with an interactive command prompt.
|
||||
.SH DETAILED DESCRIPTION
|
||||
.B wavinfo
|
||||
collects metadata according to different
|
||||
|
||||
@@ -89,7 +89,7 @@ class WavBextReader:
|
||||
# umid_str = umid_parsed.basic_umid_to_str()
|
||||
# else:
|
||||
|
||||
umid_str = None
|
||||
# umid_str = None
|
||||
|
||||
return {'description': self.description,
|
||||
'originator': self.originator,
|
||||
@@ -98,7 +98,7 @@ class WavBextReader:
|
||||
'originator_time': self.originator_time,
|
||||
'time_reference': self.time_reference,
|
||||
'version': self.version,
|
||||
'umid': umid_str,
|
||||
'umid': self.umid,
|
||||
'coding_history': self.coding_history,
|
||||
'loudness_value': self.loudness_value,
|
||||
'loudness_range': self.loudness_range,
|
||||
|
||||
@@ -8,7 +8,7 @@ class WaveSmplLoop(NamedTuple):
|
||||
loop_type: int
|
||||
start: int
|
||||
end: int
|
||||
fraction: int
|
||||
detune_cents: int
|
||||
repetition_count: int
|
||||
|
||||
def loop_type_desc(self):
|
||||
@@ -30,7 +30,7 @@ class WaveSmplLoop(NamedTuple):
|
||||
'loop_type_description': self.loop_type_desc(),
|
||||
'start_samples': self.start,
|
||||
'end_samples': self.end,
|
||||
'fraction': self.fraction,
|
||||
'detune_cents': self.detune_cents,
|
||||
'repetition_count': self.repetition_count,
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ class WavSmplReader:
|
||||
Read sampler metadata from smpl chunk.
|
||||
"""
|
||||
|
||||
header_field_fmt = "<IIIIIIbbbbII"
|
||||
loop_field_fmt = "<IIIIII"
|
||||
header_field_fmt = "<IIIIiIbbbbII"
|
||||
loop_field_fmt = "<IIIIiI"
|
||||
header_size = struct.calcsize(header_field_fmt)
|
||||
loop_size = struct.calcsize(loop_field_fmt)
|
||||
|
||||
@@ -65,7 +65,7 @@ class WavSmplReader:
|
||||
self.midi_note: int = unpacked_data[3]
|
||||
|
||||
#: The number of semitones above the MIDI note the loops tune for.
|
||||
self.midi_pitch_fraction_semis: int = unpacked_data[4]
|
||||
self.midi_pitch_detune_cents: int = unpacked_data[4]
|
||||
|
||||
#: SMPTE timecode format, one of (0, 24, 25, 29, 30)
|
||||
self.smpte_format: int = unpacked_data[5]
|
||||
@@ -89,13 +89,16 @@ class WavSmplReader:
|
||||
loop_type=unpacked_loop[1],
|
||||
start=unpacked_loop[2],
|
||||
end=unpacked_loop[3],
|
||||
fraction=unpacked_loop[4],
|
||||
detune_cents=unpacked_loop[4],
|
||||
repetition_count=unpacked_loop[5]))
|
||||
|
||||
#: Sampler-specific user data.
|
||||
self.sampler_udata: bytes = smpl_data[
|
||||
header_size + loop_size * loop_count:
|
||||
header_size + loop_size * loop_count + sampler_udata_length]
|
||||
self.sampler_udata: bytes | None = None
|
||||
|
||||
if sampler_udata_length > 0:
|
||||
self.sampler_udata = smpl_data[
|
||||
header_size + loop_size * loop_count:
|
||||
header_size + loop_size * loop_count + sampler_udata_length]
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@@ -103,7 +106,7 @@ class WavSmplReader:
|
||||
'product': self.product,
|
||||
'sample_period_ns': self.sample_period_ns,
|
||||
'midi_note': self.midi_note,
|
||||
'midi_pitch_fraction_semis': self.midi_pitch_fraction_semis,
|
||||
'midi_pitch_detune_cents': self.midi_pitch_detune_cents,
|
||||
'smpte_format': self.smpte_format,
|
||||
'smpte_offset': "%02i:%02i:%02i:%02i" % self.smpte_offset,
|
||||
'loops': [x.to_dict() for x in self.sample_loops],
|
||||
|
||||
Reference in New Issue
Block a user