diff --git a/wavinfo/__main__.py b/wavinfo/__main__.py index 0fd7428..74d06e5 100644 --- a/wavinfo/__main__.py +++ b/wavinfo/__main__.py @@ -6,12 +6,14 @@ from optparse import OptionParser import sys import json from enum import Enum - +from base64 import b64encode class MyJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Enum): return o._name_ + elif isinstance(o, bytes): + return b64encode(o).decode('ascii') else: return super().default(o) diff --git a/wavinfo/wave_info_reader.py b/wavinfo/wave_info_reader.py index 34b9b96..bf097ef 100644 --- a/wavinfo/wave_info_reader.py +++ b/wavinfo/wave_info_reader.py @@ -1,4 +1,4 @@ -from .riff_parser import parse_chunk, ListChunkDescriptor +from .riff_parser import ChunkDescriptor, parse_chunk, ListChunkDescriptor from typing import Optional diff --git a/wavinfo/wave_reader.py b/wavinfo/wave_reader.py index 6c7c506..5cb9ff6 100644 --- a/wavinfo/wave_reader.py +++ b/wavinfo/wave_reader.py @@ -5,6 +5,7 @@ from typing import Optional, Generator, Any, NamedTuple import pathlib + from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor from .wave_ixml_reader import WavIXMLFormat from .wave_bext_reader import WavBextReader @@ -12,7 +13,7 @@ from .wave_info_reader import WavInfoChunkReader from .wave_adm_reader import WavADMReader from .wave_dbmd_reader import WavDolbyMetadataReader from .wave_cues_reader import WavCuesReader - +from .wave_smpl_reader import WavSmplReader #: Calculated statistics about the audio data. class WavDataDescriptor(NamedTuple): @@ -80,6 +81,9 @@ class WavInfoReader: #: RIFF cues markers, labels, and notes. self.cues: Optional[WavCuesReader] = None + #: Sampler `smpl` metadata + self.smpl: Optional[WavSmplReader] = None + if hasattr(path, 'read'): self.get_wav_info(path) self.url = 'about:blank' @@ -110,6 +114,7 @@ class WavInfoReader: self.info = self._get_info(wavfile, encoding=self.info_encoding) self.dolby = self._get_dbmd(wavfile) self.cues = self._get_cue(wavfile) + self.smpl = self._get_sampler_loops(wavfile) self.data = self._describe_data() def _find_chunk_data(self, ident, from_stream, @@ -203,6 +208,10 @@ class WavInfoReader: return WavCuesReader.read_all(f, cue, labls, ltxts, notes, fallback_encoding=self.info_encoding) + def _get_sampler_loops(self, f): + sampler_data = self._find_chunk_data(b'smpl', f, default_none=True) + return WavSmplReader(sampler_data) if sampler_data else None + # FIXME: this should probably be named "iter()" def walk(self) -> Generator[str, str, Any]: """ @@ -210,11 +219,12 @@ class WavInfoReader: :yields: tuples of the *scope*, *key*, and *value* of each metadatum. The *scope* value will be one of - "fmt", "data", "ixml", "bext", "info", "dolby", "cues" or "adm". + "fmt", "data", "ixml", "bext", "info", "dolby", "cues", "adm" or + "smpl". """ scopes = ('fmt', 'data', 'ixml', 'bext', 'info', 'adm', 'cues', - 'dolby') + 'dolby', 'smpl') for scope in scopes: if scope in ['fmt', 'data']: @@ -223,10 +233,10 @@ class WavInfoReader: yield scope, field, attr.__getattribute__(field) else: - dict = self.__getattribute__(scope).to_dict( + mdict = self.__getattribute__(scope).to_dict( ) if self.__getattribute__(scope) else {} - for key in dict.keys(): - yield scope, key, dict[key] + for key in mdict.keys(): + yield scope, key, mdict[key] def __repr__(self): return 'WavInfoReader({}, {}, {})'.format(self.path, diff --git a/wavinfo/wave_smpl_reader.py b/wavinfo/wave_smpl_reader.py index 27a3f95..20da66b 100644 --- a/wavinfo/wave_smpl_reader.py +++ b/wavinfo/wave_smpl_reader.py @@ -11,8 +11,31 @@ class WaveSmplLoop(NamedTuple): fraction: int repetition_count: int + def loop_type_desc(self): + if self.loop_type == 0: + return 'FORWARD' + elif self.loop_type == 1: + return 'FORWARD_BACKWARD' + elif self.loop_type == 2: + return 'BACKWARD' + elif 3 <= self.loop_type <= 31: + return 'RESERVED' + else: + return 'VENDOR' -class WaveSmplReader: + def to_dict(self): + return { + 'ident': self.ident, + 'loop_type': self.loop_type, + 'loop_type_description': self.loop_type_desc(), + 'start_samples': self.start, + 'end_samples': self.end, + 'fraction': self.fraction, + 'repetition_count': self.repetition_count, + } + + +class WavSmplReader: def __init__(self, smpl_data: bytes): """ @@ -36,13 +59,13 @@ class WaveSmplReader: self.product: int = unpacked_data[1] #: The number of nanoseconds in one audio frame. - self.sample_period: int = unpacked_data[2] + self.sample_period_ns: int = unpacked_data[2] #: The MIDI note number for the loops in this sample self.midi_note: int = unpacked_data[3] #: The number of semitones above the MIDI note the loops tune for. - self.midi_pitch_fraction: int = unpacked_data[4] + self.midi_pitch_fraction_semis: int = unpacked_data[4] #: SMPTE timecode format, one of (0, 24, 25, 29, 30) self.smpte_format: int = unpacked_data[5] @@ -57,8 +80,8 @@ class WaveSmplReader: #: List of loops in the file. self.sample_loops: List[WaveSmplLoop] = [] - loop_buffer = smpl_data[header_field_fmt: - header_field_fmt + loop_size * loop_count] + loop_buffer = smpl_data[header_size: + header_size + loop_size * loop_count] for unpacked_loop in struct.iter_unpack(loop_field_fmt, loop_buffer): self.sample_loops.append(WaveSmplLoop( @@ -71,5 +94,18 @@ class WaveSmplReader: #: Sampler-specific user data. self.sampler_udata: bytes = smpl_data[ - header_field_fmt + loop_size * loop_count: - header_field_fmt + loop_size * loop_count + sampler_udata_length] + header_size + loop_size * loop_count: + header_size + loop_size * loop_count + sampler_udata_length] + + def to_dict(self): + return { + 'manufactuer': self.manufacturer, + 'product': self.product, + 'sample_period_ns': self.sample_period_ns, + 'midi_note': self.midi_note, + 'midi_pitch_fraction_semis': self.midi_pitch_fraction_semis, + 'smpte_format': self.smpte_format, + 'smpte_offset': "%02i:%02i:%02i:%02i" % self.smpte_offset, + 'loops': [x.to_dict() for x in self.sample_loops], + 'sampler_user_data': self.sampler_udata, + }