21 Commits

Author SHA1 Message Date
Jamie Hardt
2830cb87a4 flake8 2024-11-25 11:15:16 -08:00
Jamie Hardt
1c8581ff35 Merge branch 'master' of https://github.com/iluvcapra/wavinfo into feature-interactive 2024-11-25 11:11:04 -08:00
Jamie Hardt
1d499d9741 Merge pull request #36 from iluvcapra/feature-smpl
Feature: smpl Metadata
2024-11-25 11:09:43 -08:00
Jamie Hardt
299f79aeb3 README update and stubbed out docs. 2024-11-25 11:05:32 -08:00
Jamie Hardt
a46590df29 Merge branch 'master' of https://github.com/iluvcapra/wavinfo into feature-smpl 2024-11-25 10:52:46 -08:00
Jamie Hardt
c6f66b2d6e Changes to fix docs 2024-11-25 10:48:58 -08:00
Jamie Hardt
b8617a35e2 Fixing doc dependencies I think 2024-11-25 10:41:05 -08:00
Jamie Hardt
8cabf948ff Merge branch 'master' of https://github.com/iluvcapra/wavinfo into feature-interactive 2024-11-25 10:38:43 -08:00
Jamie Hardt
8a755b4466 Merge pull request #35 from iluvcapra/maint-poetry
Change build system to Poetry
2024-11-25 10:37:05 -08:00
Jamie Hardt
c13b07e4a3 typing fix for python 3.8/3.9 2024-11-25 10:33:07 -08:00
Jamie Hardt
ac37c14b3d flake8 2024-11-25 10:30:02 -08:00
Jamie Hardt
36e4a02ab8 Documenation of base64 output 2024-11-25 10:27:25 -08:00
Jamie Hardt
ffc0c48af7 A small change to report umids as binary data 2024-11-25 10:18:24 -08:00
Jamie Hardt
206962b218 More documentation changes. 2024-11-25 10:16:26 -08:00
Jamie Hardt
d560e5a9f0 Added documentation. 2024-11-25 10:04:50 -08:00
Jamie Hardt
98ca1ec462 Implementing an interactive shell
...for browsing metadata
2024-11-24 16:23:39 -08:00
Jamie Hardt
f0353abd4e Added a test case for sampler udata
And a little marker for base64
2024-11-24 15:10:31 -08:00
Jamie Hardt
6304666d11 Autopep8 2024-11-24 15:05:19 -08:00
Jamie Hardt
d2b0c68dd2 Made sampler udata field nullable 2024-11-24 15:04:00 -08:00
Jamie Hardt
a0a9c38cb4 Assuming detune is signed 2024-11-24 14:37:18 -08:00
Jamie Hardt
f68eea4cd9 Rectified some terminology 2024-11-24 14:35:56 -08:00
11 changed files with 250 additions and 80 deletions

View File

@@ -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

View File

@@ -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
}
}
}
}

View File

@@ -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 ---------------------------------------------------

View File

@@ -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
-------------

View 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:

View File

@@ -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"

Binary file not shown.

View File

@@ -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()

View File

@@ -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

View File

@@ -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,

View File

@@ -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],