audiotree¶
- class AudioTree(waveform: ndarray | None, sample_rate: int, lufs: ndarray | None = None, lufs_windows: ndarray | None = None, pitch: ndarray | None = None, velocity: ndarray | None = None, note_duration: ndarray | None = None, codes: ndarray | None = None, latents: ndarray | None = None, extras: dict = <factory>, _metadata: dict = <factory>)¶
A flax.struct.dataclass for holding audio information including a waveform, sample rate, and extras.
- The
AudioTreeclass is inspired by Descript AudioTools’s AudioSignal.
The constructor stores its arguments verbatim – it neither reshapes the waveform nor encodes provenance. Use
create()(which accepts a(Samples,)or(Channels, Samples)waveform andfilepath/sourcestrings) unless you already hold batched arrays.- Parameters:
waveform (np.ndarray or jax.Array) – Audio waveform data shaped
(Batch, Channels, Samples), orNonefor token-only trees (codes/latentswithout audio).sample_rate (int) – Sample rate of
waveform, such as 44100 Hz.lufs (np.ndarray or jax.Array, optional) – Integrated loudness of the audio waveform in LUFS, shaped
(Batch,). You may not need to set this when initializing. Instead, usereplace_lufs()to create a new AudioTree withlufs(andlufs_windows) calculated.lufs_windows (np.ndarray or jax.Array, optional) – Per-window integrated loudness in LUFS, shaped
(Batch, Windows)— one value per non-overlapping analysis window. Populated alongsidelufsbyreplace_lufs().pitch (np.ndarray or jax.Array, optional) – The MIDI pitch where 60 is middle C. The shape is
(Batch,).velocity (np.ndarray or jax.Array, optional) – The MIDI velocity between 0 and 127. The shape is
(Batch,).note_duration (np.ndarray or jax.Array, optional) – A note duration in units of your choice. The value is not necessarily the same as the duration of the audio data. The shape is
(Batch,).codes (np.ndarray or jax.Array, optional) – The neural audio codec tokens for the audio.
latents (np.ndarray or jax.Array, optional) – The latent representations of the audio.
extras (dict) – Any extra per-item data can be placed here. This dict is entirely yours: the library never plants keys of its own in it. Leaves should be arrays with the batch as their leading axis, or lists of strings (one per item) –
create()validates this; a list of numbers would be flattened into scalar leaves byjax.tree_utiland silently stop tracking the batch._metadata (dict) – Library-managed provenance container – private, as the underscore says; not for user data (put your own data in
extras). It holds the encoded"filepath"and"source"arrays plus the per-item"offset"array: passfilepath=/source=/offset=tocreate()to fill it, and read the values back via thefilepath,sourceandoffsetproperties. On disk it keeps the spellingmetadata.
Example
>>> audio = AudioTree.create(jnp.zeros((2, 44100)), 44100) # stereo, 1 s >>> audio.waveform.shape (1, 2, 44100) >>> audio.sample_rate 44100
Note
Every consumer that enumerates these fields derives its list from
dataclasses.fields(seePYTREE_FIELDSbelow), so adding a field here is picked up automatically.- property backend: str¶
Which array library this tree’s arrays belong to.
One of
"numpy","jax", or"mixed". A tree is not required to be homogeneous and quietly stops being so more often than you would expect: applying a NumPy-namespace transform to a JAX tree converts the fields it touches, soaudiotree.transforms.trimon a JAX tree hands back a NumPywaveform. Nothing is wrong with that until something downstream assumes otherwise –jax.jiton a NumPy leaf silently re-uploads it every call – which is what this property is for."mixed"is reported rather than raised, so it is safe to log. Convert withjax.device_put()/jax.device_get().Examples
>>> import numpy as np, jax.numpy as jnp >>> from audiotree import AudioTree >>> AudioTree.create(np.zeros((1, 1, 8)), 16000).backend 'numpy' >>> AudioTree.create(jnp.zeros((1, 1, 8)), 16000).backend 'jax'
- static batch(items: Sequence[Any]) Any¶
Batch function for use with grain’s IterDataset.batch().
Concatenates AudioTree objects along the batch axis (axis 0). Use this instead of grain’s default batching, which would add an extra dimension since AudioTree already has shape (batch, channels, samples).
Supports arbitrary nested structures containing AudioTrees. All arrays (including AudioTrees) are concatenated along axis 0, so data should have a leading batch dimension.
Concatenation dispatches on the leaves’ array library, so JAX in gives JAX out: batching a tree of
jax.Arrayleaves stays on device instead of forcing a blocking host sync and silently returning NumPy.- Parameters:
items – Sequence of AudioTree objects, or structures (dicts, lists, etc.) containing AudioTree objects. Must be non-empty — there is no array library, sample rate or structure to infer from nothing.
- Returns:
Batched structure with the same shape as the input items.
- Raises:
ValueError – If
itemsis empty.
Example
>>> a = AudioTree.create(jnp.zeros((1, 1, 16000)), 16000) >>> batched = AudioTree.batch([a, a, a]) # concatenate along the batch axis >>> batched.waveform.shape (3, 1, 16000) >>> isinstance(batched.waveform, jax.Array) # JAX in, JAX out True
With Grain, pass it as the
batch_fn(each item already has a leading batch axis):ds.to_iter_dataset().batch(32, batch_fn=AudioTree.batch)
- property batch_size: int¶
Return the size of the leading (batch) axis.
Derived from
waveform, falling back tocodes/latentsfor audio-less trees (e.g. token-only training examples).This is the leading axis, not the item count: on a mini-batched tree (rank 4, from
reshape_mini_batches()) it is the number of mini-batches, which is also whatlen(), iteration and indexing walk over.flatten_mini_batches()first if you want items.
- clear_lufs() Self¶
Return a copy with the cached
lufsandlufs_windowsdropped.Shorthand for
replace(lufs=None, lufs_windows=None). Call it after changing the waveform through a barereplace(), so the nextreplace_lufs()measures the new audio instead of trusting a cached value that describes the old one. The built-in transforms and AudioTree methods invalidate for you; this is for hand-rolled edits.Example
>>> audio = AudioTree.create(jnp.zeros((44100,)), 44100).replace_lufs() >>> quieter = audio.replace(waveform=audio.waveform * 0.5).clear_lufs() >>> quieter.lufs is None True
- codes: ndarray | None = None¶
- classmethod create(waveform: ndarray | None, sample_rate: int, *, lufs: ndarray | None = None, lufs_windows: ndarray | None = None, pitch: ndarray | None = None, velocity: ndarray | None = None, note_duration: ndarray | None = None, codes: ndarray | None = None, latents: ndarray | None = None, extras: dict | None = None, filepath: str | Path | List[str | Path] | None = None, source: str | List[str] | None = None, offset: float | ndarray | None = None) Self¶
Create an
AudioTree, normalizing the waveform to(Batch, Channels, Samples).A bare
(Samples,)or(Channels, Samples)waveform gains the missing leading axes, so you don’t have to reshape by hand.filepath,sourceandoffsetgo into the library-managed_metadatacontainer;extrasis left to the caller.- Parameters:
waveform – Audio of shape
(Samples),(Channels, Samples), or(Batch, Channels, Samples), orNonefor token-only trees (e.g.codes/latentswithout audio).sample_rate – Sample rate of
waveformin Hz (e.g. 44100).lufs – Optional precomputed integrated loudness (LUFS); usually left
Noneand filled byreplace_lufs().lufs_windows – Optional precomputed per-window loudness
(Batch, Windows); usually leftNoneand filled byreplace_lufs().pitch – Optional MIDI pitch
(Batch,)(60 = middle C).velocity – Optional MIDI velocity
(Batch,)in[0, 127].note_duration – Optional per-note duration
(Batch,)(not the audio duration).codes – Optional neural-codec tokens.
latents – Optional latent representations.
extras – Optional extras dict of additional per-item leaves (copied, not mutated). Leaves must be arrays with the batch as their leading axis, lists of strings (one per item), or nested dicts of those; a Python list/tuple of non-strings is rejected –
jax.tree_utilwould treat each element as its own scalar leaf, silently misaligning it with the batch. Convert such lists withnp.asarray.filepath – Optional path(s) for the batch; encoded into
_metadata["filepath"]and read back via thefilepathproperty. Pass a single path to tag the whole batch (it is repeated for every item), or a list with one path per batch item.source – Optional source-group name(s) (e.g.
"music"), encoded into_metadata["source"]and read back via thesourceproperty. Pass a single string to tag the whole batch, or a list with one name per batch item (unlikefrom_file(), which only accepts a single string).offset – Optional source-file offset(s) in seconds, stored in
_metadata["offset"]and read back via theoffsetproperty. Pass a single number to tag the whole batch, or a(Batch,)array with one offset per batch item.from_file()and the dataset loaders record this automatically.
- Returns:
A new
AudioTreewhose waveform is(Batch, Channels, Samples).- Return type:
Example
>>> audio = AudioTree.create(jnp.zeros((44100,)), 44100) # 1 s mono >>> audio.waveform.shape (1, 1, 44100) >>> audio.sample_rate 44100
- property device: Device | None¶
The device this tree’s arrays live on, or
Noneon NumPy.jax.device_putandjax.device_getalready move a tree; this is the missing half – asking where it currently is without reaching into a leaf and hoping the rest agree.- Returns:
The
jax.Deviceshared by every array leaf, orNoneif the tree is NumPy-backed (host memory, no JAX device).- Raises:
ValueError – If the leaves do not agree on one device – a mixed NumPy/JAX tree, or JAX arrays committed to different devices. Unlike
backendthis raises, because there is no honest single answer and returning one of them would be a guess.
- classmethod excerpt(audio_path: str | Path, rng: Generator, duration: float, offset: float = 0.0, excerpt: ExcerptConfig | None = None, **kwargs) Self | None¶
Create an AudioTree from one section of an audio file.
Which section is up to
excerpt.strategy:"start"takes the audio atoffset,"random"(the default) draws a single offset, and"loudest"defers toloudest_excerpt().- Parameters:
audio_path (str or Path) – Path to audio file.
rng (np.random.Generator) – Random number generator such as
np.random.default_rng(42).duration (float) – Duration in seconds of audio data; must be positive. The audio data will be trimmed or lengthened as necessary.
offset (float, optional) – Earliest offset in seconds the excerpt may start at.
excerpt (ExcerptConfig, optional) – How to choose the offset; defaults to a uniformly random one. See
ExcerptConfig.**kwargs – Keyword arguments passed to
AudioTree.from_file.
- Returns:
AudioTree, or
Nonewhenexcerptsearched for the loudest section, found nothing above the cutoff, and sayson_failure="skip".
- extras: dict¶
- property filepath: List[str] | List[list]¶
Return the decoded filepaths stored in
_metadata['filepath'].One string per batch item. A mini-batched tree (rank 4, from
reshape_mini_batches()) has two leading axes, so it returns one list per mini-batch — the nesting always matches the tree’s leading axes. Callflatten_mini_batches()first for a flat list.An empty list is returned if the AudioTree does not carry filepath provenance.
- filter(predicate: Callable[[Self], bool]) Self¶
Keep only the batch items for which
predicateis true.The batch is split into one tree per item,
predicateis called on each, and the survivors are concatenated back into a single tree.- Parameters:
predicate – Called with a batch-of-1
AudioTree; returnTrueto keep that item.- Returns:
A tree holding the kept items. When nothing is kept, the result has
batch_size == 0(every array field is empty along the batch axis, every string extras leaf is[]) rather than beingNone. Filtering an already-empty tree returns such an empty tree without callingpredicate, so chained filters compose.- Return type:
- Raises:
ValueError – If the tree is mini-batched (rank 4). Dropping items independently within each mini-batch would leave the mini-batches ragged, so there is no rank-4 answer; flatten, filter, and reshape again.
Example
>>> waveform = jnp.stack([jnp.zeros((1, 8)), jnp.ones((1, 8))]) >>> audio = AudioTree.create(waveform, 16000) >>> loud = audio.filter(lambda item: bool(item.waveform.max() > 0.5)) >>> loud.batch_size 1
- flatten_mini_batches() Self¶
Flatten mini-batches back into a single batch dimension.
Undoes the operation performed by reshape_mini_batches(), transforming audio data from shape (num_mini_batches, mini_batch_size, C, T) back to (B, C, T). String extras leaves lose their per-mini-batch nesting the same way, back to one string per item.
- Returns:
AudioTree with the mini-batch dimension flattened into the batch dimension.
- Raises:
ValueError – If the waveform has fewer than 4 dimensions, i.e. it was never mini-batched.
Example
>>> x = AudioTree(np.zeros((12, 1, 44100)), 44100) >>> x_batched = x.reshape_mini_batches(3) >>> x_batched.waveform.shape # 4 mini-batches of size 3 (4, 3, 1, 44100) >>> x_unbatched = x_batched.flatten_mini_batches() >>> x_unbatched.waveform.shape # back to original shape (12, 1, 44100)
- classmethod from_file(audio_path: str | Path, *, sample_rate: int | None = None, offset: float = 0.0, duration: float | None = None, mono: bool = False, pad_mode: Literal['constant', 'edge', 'reflect', 'symmetric', 'wrap'] | None = 'constant', filepath: str | Path | List[str | Path] | None = None, source: str | None = None, extras: Dict[str, Any] | None = None, lufs: ndarray | None = None, lufs_windows: ndarray | None = None, pitch: ndarray | None = None, velocity: ndarray | None = None, note_duration: ndarray | None = None, codes: ndarray | None = None, latents: ndarray | None = None)¶
Create an AudioTree from an audio file path.
- Parameters:
audio_path (str) – Path to audio file.
sample_rate (int, optional) – Sample rate of audio data, such as 44100 Hz. If left as
None, the file’s original sample rate will be used.offset (float, optional) – Offset in seconds to audio data.
duration (float, optional) – Duration in seconds of audio data. The audio data will be trimmed or extended as necessary.
mono (bool, optional) – Whether to force the audio data to be single-channel.
pad_mode (Literal) – If duration is not None, and duration is less than the length of the audio, then
pad_modecontrols how the audio is right-padded (numpy.pad modes). Options: “constant” (zeros, default), “edge” (repeat edge), “reflect” (mirror), “symmetric” (mirror with edge), “wrap” (circular/loop), or None (no padding).filepath (Union[str, Path, List[str | Path]], optional) – The path to store as the returned
AudioTree’s provenance (_metadata["filepath"], read back via thefilepathproperty) – a single path or a one-item list, sincefrom_filereturns a batch of 1. If None (default) the providedaudio_pathwill be used.source (str, optional) – The source group name for this audio file (e.g., “music”, “speech”). Stored as provenance (
_metadata["source"]) and accessible via thesourceproperty.extras (dict, optional) – Additional extras to include in the AudioTree. The dict is purely user payload; the read offset is recorded as provenance (
_metadata["offset"], read back via theoffsetproperty).lufs (np.ndarray or jax.Array, optional) – Integrated loudness (LUFS) values to assign to the AudioTree.
lufs_windows (np.ndarray or jax.Array, optional) – Per-window loudness (LUFS) values to assign to the AudioTree.
pitch (np.ndarray or jax.Array, optional) – Pitch values to assign to the AudioTree.
velocity (np.ndarray or jax.Array, optional) – Velocity values to assign to the AudioTree.
note_duration (np.ndarray or jax.Array, optional) – Note note_duration values to assign to the AudioTree.
codes (np.ndarray or jax.Array, optional) – The neural audio codec tokens for the audio.
latents (np.ndarray or jax.Array, optional) – The latent representations of the audio.
- Returns:
An instance of
AudioTree.- Return type:
- classmethod from_manifest(manifest_path: str | Path, *, audio_dir: str | Path | None = None, filter_fn: Callable[[Dict[str, Any]], bool] | None = None) Self¶
Create an AudioTree by loading all items from a manifest file.
This loads all entries from a manifest file created by AudioWriter and creates a single AudioTree with all items in the batch dimension.
Parsing the manifest – header check, presence masks, string decoding – is
audiotree._manifest.read_entries(), the one reader of that format; nothing here re-derives the file’s rules.- Parameters:
manifest_path – Path to the manifest file (NPZ format)
audio_dir – Optional directory containing audio files. If None, uses manifest directory
filter_fn – Optional predicate called with one manifest entry, a
dictkeyed by column name ("filename","sample_rate", the AudioTree label fields,"extras_*", plus"tags"); returnTrueto load that entry. These are the entries ofaudiotree._manifest.read_entries(), the same onesAudioDataSourcepasses itsfilter_fn, so one predicate serves both (that source additionally demotes bookkeeping numbers such assample_rateto plain Python scalars). A column with no value for an entry is absent from that entry’s dict.
- Returns:
AudioTree with all manifest entries concatenated along batch dimension
- Raises:
ValueError – If the manifest is unreadable, holds no entries, names an audio file outside
audio_dir, orfilter_fnmatches nothing.
Example
First, write a small manifest with
AudioWriter(here, 100 one-second stereo items; the first 60 are loud, the rest quiet, so the lufs field is recorded for filtering):>>> import tempfile >>> from audiotree import AudioWriter >>> out_dir = tempfile.mkdtemp() >>> lufs = np.where(np.arange(100) < 60, -10.0, -30.0).astype(np.float32) >>> batch = AudioTree.create(jnp.zeros((100, 2, 44100)), 44100, lufs=lufs) >>> with AudioWriter(out_dir) as writer: ... _ = writer.write(batch) >>> manifest_path = f"{out_dir}/manifest.npz"
Load every item into one batched
AudioTree:>>> audio = AudioTree.from_manifest(manifest_path) >>> audio.waveform.shape # 100 items, stereo, 1 second each (100, 2, 44100)
Load only entries that pass a filter on the manifest (the 60 loud items):
>>> audio = AudioTree.from_manifest( ... manifest_path, ... filter_fn=lambda entry: entry.get('lufs', -float('inf')) > -20 ... ) >>> audio.waveform.shape (60, 2, 44100)
- latents: ndarray | None = None¶
- classmethod loudest_excerpt(audio_path: str | Path, rng: Generator, excerpt: ExcerptConfig, **kwargs) Self | None¶
Create an AudioTree from the loudest of several candidate excerpts.
Draws up to
excerpt.num_triesoffsets and keeps the loudest, stopping early once one exceedsexcerpt.lufs_cutoff. This is a best-of-k search rather than a filter, so on a file where nothing clears the cutoff it still has to return something –excerpt.on_failuredecides what.- Parameters:
audio_path (str) – Path to audio file.
rng (np.random.Generator) – Random number generator such as
np.random.default_rng(42).excerpt (ExcerptConfig) – How to search.
strategymust be"loudest".**kwargs – Keyword arguments passed to
AudioTree.from_file.
- Returns:
AudioTree, or
Nonewhen nothing cleared the cutoff andon_failure="skip".
- lufs: ndarray | None = None¶
- lufs_windows: ndarray | None = None¶
- normalize_lufs(target_lufs: float, *, max_gain_db: float | None = None, device: Literal['cpu', 'gpu', 'tpu'] | Device | None = None, engine: Literal['numpy', 'jax'] | None = None) Self¶
Normalize audio to a target LUFS level.
Computes the current loudness (if not already set), then scales the audio to achieve the target LUFS. The returned AudioTree has updated
waveform,lufs, andlufs_windowsfields (a constant gain shifts every window’s LUFS by the same amount). Changing the level invalidatescodesandlatents, which describe the audio at its previous level.Items whose loudness is not finite are passed through unscaled. Digital silence, and any excerpt below the BS.1770 absolute gate, measure
-infLUFS, for which no gain reaches the target; scaling by the implied+infwould produce an all-NaNwaveform. Theirlufsstays-inf, so a silent item is still identifiable afterwards.- Parameters:
target_lufs – Target loudness in LUFS (e.g., -18.0 for broadcast standard).
max_gain_db – Optional ceiling on the applied gain, so a very quiet (but still measurable) item is not amplified without bound.
None(the default) applies whatever gain the target implies; a capped item lands atlufs + max_gain_dbrather than attarget_lufs.device – Where to compute the loudness when it is not already set, forwarded to
replace_lufs()(see there). Ignored whenlufsis already populated.engine – Which loudness kernel to use when it is not already set, forwarded to
replace_lufs()(see there). Ignored whenlufsis already populated.
- Returns:
AudioTree with audio scaled to target LUFS and loudness updated.
Example
>>> t = jnp.arange(44100) / 44100 # 1 s at 44.1 kHz >>> audio = AudioTree.create(0.5 * jnp.sin(2 * jnp.pi * 1000 * t), 44100) >>> normalized = audio.normalize_lufs(-18.0) >>> float(normalized.lufs[0]) # now at the target LUFS -18.0
Silence is left alone instead of becoming
NaN:>>> silent = AudioTree.create(jnp.zeros((1, 1, 44100)), 44100) >>> out = silent.normalize_lufs(-18.0) >>> bool(jnp.all(out.waveform == 0.0)), float(out.lufs[0]) (True, -inf)
- note_duration: ndarray | None = None¶
- property num_channels: int¶
Return the number of audio channels (
waveform.shape[-2]).
- property offset: ndarray | None¶
Return the per-item source-file offsets stored in
_metadata['offset'].Seconds from the start of each item’s source file to its sample 0, recorded by
from_file()and the dataset loaders. The shape follows the tree’s leading axes:(Batch,)normally,(Mini, Batch)on a mini-batched tree. ReturnsNonewhen the tree carries no offset provenance, or when a time-shifting transform such asroll()invalidated it (underprob < 1the whole batch’s offset is dropped, since “invalidated for some items only” cannot be represented in one array). An item with no source file (e.g. a synthetic pre-built dataset group) recordsNaN.
- pitch: ndarray | None = None¶
- replace(**updates: Unpack[_AudioTreeFields]) Self¶
Return a new
AudioTreewith the given fields replaced.- Parameters:
**updates – Any subset of the fields above; everything else is carried over from
self, which is never mutated.- Returns:
A copy of
selfwithupdatesapplied.- Return type:
Example
>>> audio = AudioTree.create(jnp.zeros((44100,)), 44100) >>> quiet = audio.replace(waveform=audio.waveform * 0.5) >>> quiet.sample_rate 44100
Note
flax.struct.dataclassinstalls its ownreplaceover this one at class-creation time. That implementation is this one verbatim (dataclasses.replace) minus the typing, which is the whole reason to spell it out here.
- replace_extras(**kwargs) Self¶
Return a new
AudioTreewithkwargsmerged intoextras.Syntactic sugar for
self.replace(extras={**self.extras, **kwargs}). Keys inkwargsoverwrite existingextraskeys with the same name; all other keys are kept. Neither the original tree nor itsextrasdict is mutated. For a key that isn’t a valid Python identifier, use theself.replace(extras=...)form directly.- Parameters:
**kwargs – Entries to merge into
extras. Values should be arrays (or pytrees of arrays) so the result stays batchable and jittable.- Returns:
A new
AudioTreewith the mergedextras.- Return type:
Example
>>> audio = AudioTree.create(jnp.zeros((44100,)), 44100) >>> audio = audio.replace_extras(tempo=np.array([120.0])) >>> audio.extras["tempo"] array([120.])
- replace_lufs(lufs_window_sec: float = 0.4, lufs_hop_sec: float | None = None, *, device: Literal['cpu', 'gpu', 'tpu'] | Device | None = None, engine: Literal['numpy', 'jax'] | None = None) Self¶
Compute and set the integrated and per-window loudness (LUFS).
Returns a new AudioTree with both
lufsandlufs_windowspopulated:lufs— the gated integrated loudness of each batch item, per the ITU-R BS.1770-4 standard (the standard “program loudness”). Measured on the CPU with theloudnesslibrary for NumPy waveforms, or a vmappedjaxloudnormmeter for JAX waveforms.lufs_windows— the ungated K-weighted loudness of each window (a loudness-over-time curve). The signal is K-weighted once and each window reports the K-weighted mean-square of its samples in LUFS, so windows are directly comparable and a fully silent window is-inf. NumPy uses exact IIR biquads (scipy); JAX usesjaxloudnorm’s FIR-approximated filters on the accelerator.
The two engines are not bit-identical.
Where the measurement runs and which kernel runs are separate choices, so
device=andengine=are separate arguments: asking for the exact IIR meter does not commit you to a device, and moving the work to an accelerator does not silently swap in the FIR approximation.- Parameters:
lufs_window_sec – Length in seconds of each
lufs_windowswindow. Must be at least 0.4s (the EBU momentary integration time).lufs_hop_sec – Step in seconds between window starts. Defaults to
lufs_window_sec(non-overlapping windows); a smaller value overlaps them, but it must still span at least one sample at the tree’s sample rate. The trailing partial window is dropped, so an excerpt shorter than one window yields an emptylufs_windows.device – Where to compute — an XLA platform name (
"cpu"/"gpu"/"tpu", mirroringjax.jit’sbackend) or ajax.Device.None(default) leaves the waveform where it is.device="gpu"is much faster for a large batch, since the NumPylufspath measures one item at a time.engine –
Which kernel to run.
"numpy"is the exact ITU-R BS.1770 IIR meter (theloudnessC++ library +scipybiquads); it is CPU-only, so it cannot be combined with a non-CPUdevice."jax"is the vmappedjaxloudnormkernel with FIR-approximated K-weighting, and runs on whateverdevicesays.None(default) follows the waveform’s own array library — NumPy waveform to"numpy", JAX waveform to"jax"— except that a non-CPUdeviceimplies"jax", the only engine that can run there.The returned
lufs/lufs_windowsalways match the waveform’s array type (a NumPy waveform yields NumPy loudness whatever the engine), so you never need a manualjax.device_put/jax.device_getround-trip.
- Returns:
AudioTree with
lufsshaped(*batch,)andlufs_windowsshaped(*batch, num_windows).
Note
Channel Limitations: Supports up to 5 channels:
Mono (1 channel): Single channel
Stereo (2 channels): [Left, Right]
5.0/5.1 Surround (5 channels): [Left, Right, Center, Left Surround, Right Surround]
Will raise ValueError if audio has more than 5 channels.
- resample(sample_rate: int, *, zeros: int = 24, rolloff: float = 0.945, output_length: int | None = None, full: bool = False) Self¶
Resample the AudioTree’s
waveformto a new sample rate. NumPy-backed waveforms are resampled on CPU with librosa (soxr); JAX-backed waveforms use a JAX port ofResampleFracfrom the PyTorch library Julius. The two backends are not bit-identical.- Parameters:
sample_rate (int) – The new sample rate of audio data, such as 44100 Hz.
zeros (int, optional) – number of zero crossing to keep in the sinc filter. JAX backend only.
rolloff (float) – use a lowpass filter that is
rolloff * sample_rate / 2, to ensure sufficient margin due to the imperfection of the FIR filter used. Lowering this value will reduce antialiasing, but will reduce some of the highest frequencies. JAX backend only.output_length (None or int) – This can be set to the desired output length (last dimension). Allowed values are between 0 and
ceil(length * sample_rate / old_sr). WhenNone(default) is specified, the floored output length will be used. In order to select the largest possible size, use the full argument.full (bool) – return the longest possible output from the input. This can be useful if you chain resampling operations, and want to give the
output_lengthonly for the last one, while passingfull=Trueto all the other ones. JAX backend only.
Changing the sample rate changes the audio’s length and its samples, so every derived field (
lufs,lufs_windows,codes,latents) is invalidated. Resampling to the rate the tree already has returns it unchanged, derived fields and all.- Returns:
A new
AudioTreeresampled tosample_rate(the original is unchanged).- Return type:
Example
>>> audio = AudioTree.create(jnp.zeros((44100,)), 44100) # 1 s at 44.1 kHz >>> resampled = audio.resample(22050) >>> resampled.waveform.shape (1, 1, 22050) >>> resampled.sample_rate 22050
- reshape_mini_batches(mini_batch_size: int) Self¶
Reshape batch dimension into mini-batches by adding a new leading axis.
Transforms audio data from shape (B, C, T) to (num_mini_batches, mini_batch_size, C, T), where B must be evenly divisible by mini_batch_size. String extras leaves nest the same way: a list of B strings becomes num_mini_batches lists of mini_batch_size strings, so indexing a mini-batch keeps them aligned with the arrays.
- Parameters:
mini_batch_size – Number of samples per mini-batch. The total batch size must be evenly divisible by this value.
- Returns:
AudioTree with an additional mini-batch dimension as the first axis.
- Raises:
ValueError – If the batch size is not divisible by
mini_batch_size, or if the tree is already mini-batched (rank 4) — nothing in this library reads the rank-5 tree that would produce.
Example
>>> x = AudioTree(np.zeros((12, 1, 44100)), 44100) >>> x_batched = x.reshape_mini_batches(3) >>> x_batched.waveform.shape # 4 mini-batches of size 3 (4, 3, 1, 44100)
- sample_rate: int¶
- property samples: int¶
Return the number of samples in the
waveform(its last dimension).
- property source: List[str] | List[list]¶
Return the decoded source names stored in
_metadata['source'].Source names indicate which data source group each item in the batch came from. For example, if
create_balanced_audio_dataset()was givensources={"music": [...], "speech": [...]}, this property might return["music", "music", "speech", "music"]for a batch of 4 items. Likefilepath, a mini-batched (rank-4) tree nests one list per mini-batch.An empty list is returned if the AudioTree does not carry source provenance.
- split(n_splits: int) List[Self]¶
Split batch dimension into a list of smaller AudioTree objects.
Divides the batch dimension evenly into n_splits separate AudioTree objects, each containing a portion of the original batch. Like indexing, this works on the leading axis, which on a mini-batched (rank-4) tree is the mini-batch axis rather than the item axis.
- Parameters:
n_splits – Number of AudioTree objects to create. The batch size must be evenly divisible by this value.
- Returns:
List of AudioTree objects, each with batch_size = original_batch_size / n_splits.
- Raises:
ValueError – If
n_splitsis not positive, or if the batch size is not divisible byn_splits.
Example
>>> audio = AudioTree(np.zeros((12, 1, 44100)), 44100) >>> audio.waveform.shape (12, 1, 44100) >>> halves = audio.split(2) >>> len(halves) 2 >>> halves[0].waveform.shape # each half has half the original batch size (6, 1, 44100)
- to_mono(strategy: Literal['average', 'left', 'right'] = 'average') Self¶
Reduce the
waveformto mono.Changing the channel count changes the audio, so every derived field (
lufs,lufs_windows,codes,latents) is invalidated. A waveform that is already mono is returned unchanged, derived fields and all.- Parameters:
strategy –
"average"mixes all channels down (default);"left"/"right"select the corresponding channel of a stereo waveform.- Returns:
An instance of
AudioTree.- Return type:
- to_stereo() Self¶
Make the
waveformstereo.Changing the channel count changes the audio, so every derived field (
lufs,lufs_windows,codes,latents) is invalidated. A waveform that is already stereo is returned unchanged, derived fields and all.- Returns:
An instance of
AudioTree.- Return type:
- velocity: ndarray | None = None¶
- waveform: ndarray | None¶
- write(filepath: str | Path, *, subtype: str | None = None, format: str | None = None, endian: str | None = None) Path¶
Write the
waveformto an audio file usingsoundfile.This is the inverse of
from_file(). The AudioTree must contain a single item (batch_size == 1); index or iterate the batch first (e.g.tree[0]orfor item in tree) to write each item. The sample rate is taken fromself.sample_rate— callresample()beforehand if you want a different one.- Parameters:
filepath – Output path. The file format is inferred from the extension (e.g.
.wav,.flac,.ogg) unless overridden byformat.subtype – soundfile subtype, e.g.,
"PCM_16","PCM_24","FLOAT". WhenNone(default) soundfile picks the format default (PCM_16for WAV).format – Major format override (e.g.
"WAV","FLAC"). WhenNoneit is inferred from the filepath extension.endian – Endianness override (e.g.
"FILE","LITTLE","BIG").
- Returns:
The path that was written.
- Return type:
Path
- Raises:
ValueError – If the tree has no
waveform, if it is mini-batched (rank 4 —batch_sizethen counts mini-batches, not items), or ifbatch_size != 1.
- The
- class ExcerptConfig(strategy: Literal['start', 'random', 'loudest'] = 'random', num_tries: int = 8, lufs_cutoff: float = -40.0, search: str | Callable = 'uniform', on_failure: Literal['keep', 'skip', 'raise'] = 'keep')¶
How to choose which part of a file an excerpt comes from.
One named
strategyrather than a set of interacting flags:"random"A random offset drawn by
search– uniform by default (the default strategy). No audio is measured, so this costs one read per item."start"Always offset 0. Deterministic, and the right choice for validation sets where every epoch should see identical audio.
"loudest"Draw up to
num_triescandidate offsets and keep the loudest, stopping early once one exceedslufs_cutoff. Use it on corpora with long quiet stretches; it costs up tonum_triesreads and loudness measurements per item.
Note that
"loudest"is a best-of-num_triessearch, not a filter: on a file where nothing clears the cutoff it still has to return something, andon_failuredecides what.- Variables:
strategy (Literal['start', 'random', 'loudest']) – Which of the three above.
num_tries (int) – Maximum candidate offsets to try (
"loudest"only).lufs_cutoff (float) – Integrated loudness (LUFS) that ends the search early (
"loudest"only).search (str | Callable) – How each offset is drawn (
"random"and"loudest"; not meaningful under"start") — a registered name ("uniform","bias_early"), a dotted path to an importable function, or a callable.on_failure (Literal['keep', 'skip', 'raise']) – What to do when no candidate clears
lufs_cutoff("loudest"only)."keep"returns the loudest excerpt found,"skip"returnsNone(grain drops it atto_iter_dataset()),"raise"raises naming the file.
Example
>>> from audiotree import ExcerptConfig >>> ExcerptConfig().strategy # random offset 'random' >>> ExcerptConfig(strategy="start").strategy # deterministic 'start' >>> loud = ExcerptConfig( ... strategy="loudest", lufs_cutoff=-30, on_failure="skip" ... ) >>> loud.num_tries, loud.on_failure (8, 'skip')
- lufs_cutoff: float = -40.0¶
- num_tries: int = 8¶
- on_failure: Literal['keep', 'skip', 'raise'] = 'keep'¶
- property resolved_search: Callable¶
The
searchspec as a callable.
- search: str | Callable = 'uniform'¶
- strategy: Literal['start', 'random', 'loudest'] = 'random'¶