diff --git a/docs/source/references.rst b/docs/source/references.rst
index aa388c1..4006cb2 100644
--- a/docs/source/references.rst
+++ b/docs/source/references.rst
@@ -36,6 +36,11 @@ iXML
* `Gallery Software iXML Specification `_
+Sampler Metadata
+----------------
+
+* `RecordingBlogs.com — Sample chunk (of a Wave file)`_
+
RIFF Metadata
-------------
* `1991. Multimedia Programming Interface and Data Specifications 1.0 `_
diff --git a/tests/test_files/smpl/alarm_citizen_loop1.wav b/tests/test_files/smpl/alarm_citizen_loop1.wav
new file mode 100644
index 0000000..9b61e91
Binary files /dev/null and b/tests/test_files/smpl/alarm_citizen_loop1.wav differ
diff --git a/wavinfo/__main__.py b/wavinfo/__main__.py
index bd077ad..5d444b7 100644
--- a/wavinfo/__main__.py
+++ b/wavinfo/__main__.py
@@ -7,12 +7,15 @@
import json
from enum import Enum
import importlib.metadata
+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_reader.py b/wavinfo/wave_reader.py
index 6c7c506..ca41c80 100644
--- a/wavinfo/wave_reader.py
+++ b/wavinfo/wave_reader.py
@@ -5,6 +5,7 @@
import pathlib
+
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
from .wave_ixml_reader import WavIXMLFormat
from .wave_bext_reader import WavBextReader
@@ -12,9 +13,11 @@
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):
byte_count: int
frame_count: int
@@ -80,6 +83,9 @@ def __init__(self, path, info_encoding='latin_1', bext_encoding='ascii'):
#: 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 +116,7 @@ def get_wav_info(self, wavfile):
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 +210,10 @@ def _get_cue(self, f):
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 +221,12 @@ def walk(self) -> Generator[str, str, Any]:
: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 +235,10 @@ def walk(self) -> Generator[str, str, Any]:
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
new file mode 100644
index 0000000..746689d
--- /dev/null
+++ b/wavinfo/wave_smpl_reader.py
@@ -0,0 +1,111 @@
+import struct
+
+from typing import Tuple, NamedTuple, List
+
+
+class WaveSmplLoop(NamedTuple):
+ ident: int
+ loop_type: int
+ start: int
+ end: int
+ 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'
+
+ 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):
+ """
+ Read sampler metadata from smpl chunk.
+ """
+
+ header_field_fmt = "