audiotree.sources

exception AudioReadError(message: str, file_path: str)

One audio file could not be read.

Raised by the dataset loaders in place of whatever the decoding stack threw, so that a corrupt file in a large corpus always names itself. The original exception is kept as __cause__ and the path as file_path.

The wrapping exists because the underlying exception frequently does not identify the file. soundfile puts the path in its message, but a truncated header sends librosa down its audioread fallback, which surfaces an EOFError or an audioread.exceptions.NoBackendError whose str() is empty – a blank traceback line at the end of a multi-hour run.

Note

On Windows, holding one of these keeps the offending file open. The traceback references librosa’s audioread fallback frames, which hold a file handle, and Windows will not delete or replace an open file. It matters only if you accumulate errors rather than handling them – collect err.file_path and str(err) and let the exception go, or call gc.collect() after dropping it (exception and traceback reference each other, so refcounting alone does not free it).

Variables:

file_path – The file that could not be read.

class WindowConfig(duration: float = 1.0, hop: float | None = None, alpha: float = 1.0, jitter: bool = True, lufs_cache: str | None = None, lufs_cutoff: float = -40.0, lufs_window_sec: float | None = None)

Windowing knobs for length-aware sampling, bundled for reuse.

Pass an instance as window= to create_balanced_audio_dataset() to build every file-based group with create_windowed_audio_dataset() instead of the default one-excerpt-per-file behavior. The fields mirror that function’s windowing arguments (sample_rate/mono stay on the dataset call so a single value applies across groups).

Variables:
  • duration (float) – Length in seconds of each excerpt.

  • hop (float | None) – Stride in seconds between natural windows at alpha == 1; defaults to duration (non-overlapping) when None.

  • alpha (float) – Length power for slots-per-file (0 = uniform per file, 1 = proportional to length).

  • jitter (bool) – Randomize each draw’s offset within its slot’s stride.

  • lufs_cache (str | None) – Optional path to a cache from build_window_lufs_cache() for build-time saliency filtering.

  • lufs_cutoff (float) – Minimum per-window LUFS to keep a slot (when filtering).

  • lufs_window_sec (float | None) – Analysis window of the loudness cache; taken from the cache when lufs_cache is given.

class WindowLufsCache(lufs: dict[str, ndarray], durations: dict[str, float] | None, lufs_window_sec: float, sample_rate: int | None, mono: bool, file_sizes: dict[str, int] | None = None)

A loaded windowed-loudness cache (see load_window_lufs()).

Variables:
  • lufs (dict[str, numpy.ndarray]) – Mapping from filepath to its 1-D per-window LUFS array.

  • durations (dict[str, float] | None) – Mapping from filepath to duration in seconds, or None if durations were not stored.

  • lufs_window_sec (float) – The analysis window length the cache was built with.

  • sample_rate (int | None) – The sample rate the loudness was measured at (None if native rates were used).

  • mono (bool) – Whether channels were averaged to mono before measuring.

  • file_sizes (dict[str, int] | None) – Mapping from filepath to its byte size when the cache was built, or None for caches written before sizes were recorded. Used to detect a file re-rendered in place, whose cached durations and loudness would otherwise silently describe audio that no longer exists. (A rewrite that keeps the byte size identical is not detectable this way.)

build_window_lufs_cache(filepaths: List[str], lufs_window_sec: float, out_dir: str | Path, *, sample_rate: int | None = 44100, mono: bool = True, durations: Mapping[str, float] | None = None) Path

Compute and persist a windowed-loudness cache in one preprocessing pass.

Convenience wrapper that runs precompute_window_lufs(), scans durations (via scan_durations() unless durations is given), and writes both to out_dir with save_window_lufs(). Run this once offline, then pass lufs_cache=out_dir to create_windowed_audio_dataset().

Parameters:
  • filepaths – Audio file paths to analyze.

  • lufs_window_sec – Analysis window length in seconds.

  • out_dir – Directory to write the cache into.

  • sample_rate – If given, resample every file to this rate before analysis.

  • mono – If True, average channels to mono before analysis.

  • durations – Optional precomputed durations; scanned from headers if None.

Returns:

The cache directory as a Path.

create_audio_dataset(sources: str | Path | List[str | Path] | None = None, filepaths: List[str | Path] | None = None, *, shuffle: bool = True, num_epochs: int | None = 1, shuffle_seed: int = 0, excerpt_seed: int | None = None, sample_rate: int = 44100, mono: bool = True, duration: float = 1.0, pad_mode: Literal['constant', 'edge', 'reflect', 'symmetric', 'wrap'] | None = 'constant', extensions: List[str] | None = None, excerpt: ExcerptConfig = ExcerptConfig(strategy='random', num_tries=8, lufs_cutoff=-40.0, search='uniform', on_failure='keep'), source: str | None = None, channels: int | None = None, on_read_error: Literal['raise', 'skip', 'warn'] = 'raise') MapDataset

Create a simple MapDataset from audio files.

This function creates a grain MapDataset that loads audio files from one or more directories. Unlike create_balanced_audio_dataset, this treats all files equally without balancing across groups.

Parameters:
  • sources – A directory path, file path, or glob pattern (e.g. "/data/*/mixture.wav"), or a list of them, containing audio files. Each entry may be a str or a Path. See find_audio_files() for how each entry is resolved. Mutually exclusive with filepaths — provide exactly one.

  • filepaths – An explicit list of audio file paths to use instead of searching sources. Mutually exclusive with sources — provide exactly one. Useful for custom splits (e.g. train/val) over a single directory without reorganizing it on disk. The given order is preserved (then shuffled if shuffle=True).

  • shuffle – Whether to shuffle files.

  • num_epochs – How many passes over the corpus the dataset yields. None repeats forever – what training wants – and makes len(ds) report sys.maxsize, grain’s spelling of “infinite”. An integer n >= 1 yields exactly n passes, so len(ds) is n times the file count; 0 or a negative count raises. Defaults to a single finite pass.

  • shuffle_seed – Random seed for shuffling file order. The stream grain shuffles with is derived from this value, not used verbatim; see excerpt_seed.

  • excerpt_seed – Random seed for excerpt selection (random_map). If None, defaults to shuffle_seed. The shuffle and excerpt streams are taken from two different draws of one numpy.random.SeedSequence, so the file order and the excerpt offsets stay independent even when one seed feeds both. Use different values to create datasets that visit files in the same order but load different random excerpts.

  • sample_rate – Target sample rate for audio files.

  • mono – Whether to convert audio to mono.

  • duration – Duration in seconds to load from each file.

  • pad_mode – Padding mode for files shorter than duration (numpy.pad modes). Options: “constant” (zeros), “edge” (repeat edge), “reflect” (mirror), “symmetric” (mirror with edge), “wrap” (circular), or None (no padding).

  • extensions – List of audio file extensions to search for. Defaults to [“.wav”, “.flac”].

  • excerpt – Which part of each file to take; see ExcerptConfig. Defaults to a uniformly random offset.

  • source – Optional source group name (e.g., “music”, “speech”) stored as provenance (metadata["source"], read back via the source property). If None, no source provenance is recorded.

  • channels – Expected channel count of every file, so that a mixed-channel corpus fails at load time with the offending filename instead of at batch time with a shape error. Ignored when mono=True (everything is one channel then). When None, the count is taken from the header of the first file, which makes the odd stereo file in a mono corpus (or vice versa) name itself.

  • on_read_error

    What to do when one file cannot be read – truncated, zero-byte, unreadable by this process. In a 100k-file corpus a single such file otherwise ends a multi-hour run.

    • "raise" (default): raise AudioReadError, which always names the path even when the underlying decoder error does not.

    • "warn": drop the item (the loader returns None, which grain skips at iteration) and emit a UserWarning naming the file and the original error.

    • "skip": drop the item without warning, for a corpus already known to contain junk.

    Dropping uses grain’s own convention: a None element passes through later map stages untouched and to_iter_dataset() skips it, so batches stay full of real audio. Note that under a non-raising policy ds[i] can therefore return None for a broken file; iterate via to_iter_dataset() (or guard for None) rather than assuming every index yields an item.

Returns:

A grain.MapDataset that loads audio files using random_map for proper RNG seeding.

Example

Create a couple of short .wav files in a temporary directory to load from:

>>> import os, tempfile
>>> import numpy as np
>>> import soundfile
>>> data_dir = tempfile.mkdtemp()
>>> for i in range(2):
...     soundfile.write(
...         os.path.join(data_dir, f"clip_{i}.wav"),
...         np.zeros((44100, 1), dtype=np.float32),
...         44100,
...     )

Load all files from the directory. Each item is a single-example AudioTree shaped (Batch, Channels, Samples):

>>> ds = create_audio_dataset(sources=data_dir, sample_rate=44100, duration=1.0)
>>> len(ds)
2
>>> ds[0].waveform.shape
(1, 1, 44100)

Training dataset (shuffled, repeating forever):

>>> train_ds = create_audio_dataset(
...     sources=data_dir,
...     shuffle=True,
...     num_epochs=None,
...     sample_rate=44100,
...     duration=1.0,
... )

Validation dataset (deterministic, a single pass):

>>> val_ds = create_audio_dataset(
...     sources=data_dir,
...     shuffle=False,
...     num_epochs=1,
...     sample_rate=44100,
...     duration=1.0,
... )
>>> len(val_ds)
2

Any finite number of passes, which a boolean could not express:

>>> len(create_audio_dataset(sources=data_dir, num_epochs=3))
6

Two datasets that visit files in the same order but load different random excerpts (same shuffle_seed, different excerpt_seed):

>>> ds1 = create_audio_dataset(sources=data_dir, shuffle_seed=42, excerpt_seed=100)
>>> ds2 = create_audio_dataset(sources=data_dir, shuffle_seed=42, excerpt_seed=200)
create_balanced_audio_dataset(sources: Mapping[str, str | Path | List[str | Path]] | None = None, weights: Mapping[str, float] | None = None, datasets: Mapping[str, MapDataset] | None = None, *, shuffle: bool = True, num_epochs: int | None = None, shuffle_seed: int = 0, excerpt_seed: int | None = None, sample_rate: int = 44100, mono: bool = True, duration: float | None = None, pad_mode: Literal['constant', 'edge', 'reflect', 'symmetric', 'wrap'] | None = 'constant', extensions: List[str] | None = None, excerpt: ExcerptConfig = ExcerptConfig(strategy='random', num_tries=8, lufs_cutoff=-40.0, search='uniform', on_failure='keep'), window: WindowConfig | None = None, channels: int | None = None, on_read_error: Literal['raise', 'skip', 'warn'] = 'raise') MapDataset

Create a balanced MapDataset from multiple audio groups and/or pre-constructed datasets.

This function creates a grain MapDataset that samples from multiple sources with specified weights. Sources can be either audio file directories or pre-constructed grain MapDatasets. It uses grain’s random_map for excerpt loading, ensuring infinite variety in RNG seeds even when files are repeated.

Parameters:
  • sources – Optional dictionary mapping group names to directories (or globs, or lists of them, as str or Path) of audio files. At least one of sources or datasets must be non-empty.

  • weights – Optional dictionary mapping group names to sampling weights. Weights are normalized to sum to 1.0. Groups not in the dict default to weight 1.0. If None, all groups are weighted equally. Every key must name a group in sources or datasets; an unknown key raises rather than silently leaving its intended group at 1.0. Every weight must be a finite number greater than zero – to disable a group, omit it from sources/datasets instead of weighting it 0.

  • datasets – Optional dictionary mapping group names to pre-constructed grain MapDatasets. These datasets will be mixed with file-based sources. Useful for combining different data sources or including pre-processed datasets. Each item of a pre-built dataset is stamped with source= its group name (overwriting any source it already carried); a missing filepath is filled with empty strings and a missing offset with NaN, so every item exposes the same provenance schema as the file-based groups and the two collate together under AudioTree.batch(). IMPORTANT: Pre-constructed datasets MUST already be repeated (call .repeat() before passing them) to ensure infinite sampling. If a finite dataset is passed, grain.MapDataset.mix will truncate the mixed output to the shortest dataset length.

  • shuffle – Whether to shuffle files within each file-based group. Set to False for deterministic iteration (e.g., pre-rendering). Does not affect pre-constructed datasets.

  • num_epochs – How many passes each file-based group makes before it runs dry. None (the default) repeats every group forever, which is what balanced mixing normally wants: grain.MapDataset.mix truncates its output to the shortest input, so any finite group caps the whole mixture. An integer n >= 1 gives each group n passes and therefore a finite mixture bounded by the smallest of them; 0 or a negative count raises. Note the default differs from create_audio_dataset()’s single pass – there one pass over the corpus is exactly one epoch, whereas here a finite group silently truncates every other group.

  • shuffle_seed – Random seed for shuffling file order. Each group’s own seed is derived from this and the group’s name, so adding or reordering groups leaves the other groups’ streams untouched.

  • excerpt_seed – Random seed for excerpt selection (random_map). If None, defaults to shuffle_seed. Derived per group the same way, and kept independent of the shuffle stream even when the two base seeds are equal.

  • sample_rate – Target sample rate for audio files (only applies to file-based sources).

  • mono – Whether to convert audio to mono (only applies to file-based sources, 0 or 1).

  • duration – Duration in seconds to load from each file (only applies to file-based sources). Defaults to 1.0. Mutually exclusive with window, which carries its own duration.

  • pad_mode – Padding mode for files shorter than duration (only applies to file-based sources). Options: “constant” (zeros), “edge” (repeat edge), “reflect” (mirror), “symmetric” (mirror with edge), “wrap” (circular), or None (no padding).

  • extensions – List of audio file extensions to search for (only applies to file-based sources).

  • excerpt – Which part of each file to take; see ExcerptConfig. Defaults to a uniformly random offset. Only applies to file-based sources.

  • window – Optional WindowConfig. When given, each file-based group is built with create_windowed_audio_dataset() (length-aware, evenly-covering window sampling) instead of one excerpt per file, using the group’s duration/alpha/etc. from the params and the shared sample_rate/mono/pad_mode here. The group weights still balance across groups, composing multiplicatively with the within-group length weighting. Mutually exclusive with a customized excerpt and with duration.

  • channels – Expected channel count of every file (only applies to file-based sources built without window); see create_audio_dataset(). When None (and mono=False), one file is probed and the count is applied to every group, so two internally-consistent groups that disagree with each other fail at load time with a filename instead of at batch time with a shape error.

  • on_read_error – What to do when one file cannot be read; see create_audio_dataset(). Applies to every file-based group (and, like channels, is not supported alongside window). Pre-constructed datasets keep whatever policy they were built with – mixing a "raise" dataset with a "skip" one produces items with different extras keys, which AudioTree.batch() cannot collate.

Returns:

A grain.MapDataset that interleaves items from source groups according to the specified weights – infinite unless num_epochs is an integer or a finite dataset was passed in datasets.

Example

Set up two small groups of .wav files in temporary directories:

>>> import os, tempfile
>>> import numpy as np
>>> import soundfile
>>> speech_dir, music_dir = tempfile.mkdtemp(), tempfile.mkdtemp()
>>> for d in (speech_dir, music_dir):
...     for i in range(2):
...         soundfile.write(
...             os.path.join(d, f"{i}.wav"),
...             np.zeros((44100, 1), dtype=np.float32),
...             44100,
...         )

Equal weighting (the default). The returned dataset is infinite, so index it directly rather than calling len:

>>> ds = create_balanced_audio_dataset(
...     sources={"speech": [speech_dir], "music": [music_dir]},
...     sample_rate=44100,
...     duration=1.0,
... )
>>> ds[0].waveform.shape
(1, 1, 44100)

Custom weights (70% speech, 30% music):

>>> ds = create_balanced_audio_dataset(
...     sources={"speech": [speech_dir], "music": [music_dir]},
...     weights={"speech": 0.7, "music": 0.3},
...     sample_rate=44100,
...     duration=1.0,
... )

For pre-rendering (deterministic, no shuffle):

>>> ds = create_balanced_audio_dataset(
...     sources={"speech": [speech_dir], "music": [music_dir]},
...     weights={"speech": 0.5, "music": 0.5},
...     shuffle=False,
...     shuffle_seed=42,
...     sample_rate=44100,
...     duration=1.0,
... )

Mix file sources with a pre-constructed (already repeated) dataset:

>>> preprocessed_ds = create_audio_dataset(sources=music_dir, num_epochs=None)
>>> ds = create_balanced_audio_dataset(
...     sources={"speech": [speech_dir]},
...     datasets={"preprocessed": preprocessed_ds},
...     weights={"speech": 0.7, "preprocessed": 0.3},
... )
create_windowed_audio_dataset(sources: List[str] | str | None = None, filepaths: List[str] | None = None, *, duration: float = 1.0, hop: float | None = None, alpha: float = 1.0, jitter: bool = True, durations: Mapping[str, float] | None = None, lufs_cache: str | Path | None = None, lufs_per_file: Mapping[str, ndarray] | None = None, lufs_window_sec: float | None = None, lufs_cutoff: float = -40.0, shuffle: bool = True, num_epochs: int | None = 1, shuffle_seed: int = 0, excerpt_seed: int | None = None, sample_rate: int = 44100, mono: bool = True, pad_mode: Literal['constant', 'edge', 'reflect', 'symmetric', 'wrap'] | None = 'constant', extensions: List[str] | None = None, source: str | None = None) MapDataset

Create a length-aware, evenly-covering MapDataset of audio windows.

Unlike create_audio_dataset() (one random excerpt per file per epoch), this tiles every file into m_i slots with m_i proportional to length ** alpha, flattens all slots into one globally shuffled index, and draws a jittered excerpt from each. The result samples long files more often than short ones (tunable via alpha), covers each file evenly, and keeps batches diverse because grain scatters any one file’s slots across the epoch.

Parameters:
  • sources – A directory path, file path, or glob pattern (e.g. "/data/*/mixture.wav"), or a list of them. See find_audio_files() for how each entry is resolved. Mutually exclusive with filepaths – provide exactly one.

  • filepaths – An explicit list of audio file paths. Mutually exclusive with sources.

  • duration – Length in seconds of each excerpt.

  • hop – Stride in seconds between natural windows at alpha == 1. Defaults to duration (non-overlapping).

  • alpha – Length power for slots-per-file (0 = uniform per file, 1 = proportional to length). See the module docstring.

  • jitter – If True, randomize each draw’s offset within its slot’s stride so coverage spans the whole file and excerpts never repeat across epochs.

  • durations – Optional precomputed {filepath: seconds} cache (see scan_durations()). If None, durations are scanned from headers (or taken from lufs_cache if it stored them).

  • lufs_cache – Optional path to an on-disk cache written by build_window_lufs_cache(). When given, its per-file LUFS arrays, analysis window, and durations are loaded and used for build-time saliency filtering (overridable by the explicit lufs_per_file / lufs_window_sec / durations args).

  • lufs_per_file – Optional precomputed per-file windowed-LUFS arrays (see precompute_window_lufs()). If given, slots whose center is below lufs_cutoff are dropped at build time. If None and no lufs_cache (the default), no loudness filtering is done – assume curated data.

  • lufs_window_sec – Analysis window of lufs_per_file in seconds. Required when lufs_per_file is given without a lufs_cache.

  • lufs_cutoff – Minimum per-window LUFS to keep a slot.

  • shuffle – Whether to globally shuffle slots (required for batch diversity).

  • num_epochs – How many passes over the slot index the dataset yields. None repeats forever (training); an integer n >= 1 yields exactly n passes; 0 or a negative count raises. Defaults to a single finite pass, which covers every slot exactly once.

  • shuffle_seed – Seed for the global slot shuffle. Derived, not used verbatim; see excerpt_seed.

  • excerpt_seed – Seed for jitter. Defaults to shuffle_seed. The shuffle and jitter streams come from two different draws of one numpy.random.SeedSequence, so slot order and jitter offsets stay independent even when a single seed feeds both.

  • sample_rate – Target sample rate for loaded audio.

  • mono – Whether to convert audio to mono.

  • pad_mode – Padding mode for files shorter than duration (numpy.pad modes), or None to not pad.

  • extensions – Audio extensions to search when using sources.

  • source – Optional source group name stored as provenance.

Returns:

A grain.MapDataset over audio windows.

find_audio_files(sources: str | Path | List[str | Path], extensions: List[str] | None = None) List[str]

Find audio files under one or more directories or glob patterns.

Each entry in sources may be a directory, an individual file, or a glob pattern (any entry containing *, ?, or [...]). A directory is searched recursively; a glob is expanded, with each match then treated as a directory (searched recursively) or a file. ** is supported for recursive glob matching, e.g. "/data/**/mixture.wav".

Hidden files and directories (names starting with ., such as .git) are skipped when recursing into directories; glob patterns follow the usual shell rule that * does not match a leading .. In every case a file is only kept if its extension is in extensions. The returned paths are sorted and de-duplicated, so the order is deterministic across machines and filesystems — important for reproducible shuffling.

A source that matches nothing (a typo, an unmounted drive, an extension that is not in extensions) shrinks the corpus without failing, so each such source raises a UserWarning naming it.

Parameters:
  • sources – A path or glob pattern, or a list of them. Each may be a Path or str naming a directory (searched recursively) or a file, or a glob pattern such as "/mnt/d/musdb18hq/train/*/mixture.wav".

  • extensions – File extensions to match (e.g. [".wav", ".flac"]). Defaults to [".wav", ".flac"].

Returns:

A sorted, de-duplicated list of matching file paths.

load_window_lufs(cache_dir: str | Path) WindowLufsCache

Load a windowed-loudness cache written by save_window_lufs().

Parameters:

cache_dir – Directory containing lufs.bagz and manifest.json.

Returns:

A WindowLufsCache with the per-file LUFS arrays, optional durations, and the analysis window length.

Raises:

ValueError – If the manifest is missing required keys or malformed – checked before the bagz file is opened, so a truncated or hand-edited manifest names itself instead of dying on a raw KeyError.

precompute_window_lufs(filepaths: List[str], lufs_window_sec: float, *, sample_rate: int | None = 44100, mono: bool = True) dict[str, ndarray]

Compute a per-file windowed-LUFS array for build-time saliency filtering.

Each value is the integrated loudness (LUFS, ITU-R BS.1770) of one non-overlapping lufs_window_sec window, measured on the CPU with the upstream loudness library (loudness.integrated_loudness, the same kernel AudioTree.replace_lufs() uses for NumPy waveforms). This is a one-time offline pass that runs entirely on the CPU – it performs no JAX/GPU computation – so it is safe to run before forking grain workers and keeps the data-source layer free of GPU work. Files are processed serially in a single pass; there is no cross-file parallelism.

The arrays are ragged (length scales with file duration); files shorter than one window get an empty array, which downstream filtering treats as “keep”. The result is suitable for passing as lufs_per_file= to create_windowed_audio_dataset(); use build_window_lufs_cache() to compute and persist it to disk (bagz) in one pass.

The sample_rate and mono defaults match those of create_windowed_audio_dataset(), so by default the loudness reflects exactly the audio the model trains on; pass matching values if you change the dataset’s. Files are normalized per these (resample to sample_rate, average to mono); sample_rate=None keeps each file’s native rate. Windows are measured independently, so files need not share a shape.

TODO: very long files are read fully into RAM before windowing; segment-read them if hour-plus files strain memory.

Parameters:
  • filepaths – Audio file paths to analyze.

  • lufs_window_sec – Analysis window length in seconds (independent of the training duration). Should be at least 0.4s for a valid LUFS measurement.

  • sample_rate – Resample every file to this rate before analysis, or None to keep each file’s native rate. Defaults to the dataset’s sample_rate.

  • mono – If True (the dataset default), average channels to mono first.

Returns:

A mapping from filepath to a 1-D float32 array of per-window LUFS.

save_window_lufs(out_dir: str | Path, lufs_per_file: Mapping[str, ndarray], *, lufs_window_sec: float, durations: Mapping[str, float] | None = None, sample_rate: int | None = None, mono: bool = True) Path

Persist a windowed-loudness cache to out_dir as bagz + JSON manifest.

The ragged per-file LUFS arrays are stored as one float32 record each in a single lufs.bagz file (no padding), in the order of lufs_per_file. A manifest.json records the filepaths (parallel to the bagz records), the analysis window, optional durations, the sample_rate/mono the loudness was measured at (so the dataset can verify the cache matches the audio it loads), and each file’s byte size, so a file re-rendered in place after the build is refused at dataset construction rather than silently keeping stale durations and loudness. A rewrite that keeps the byte size identical is not detectable this way (see WindowLufsCache).

Parameters:
  • out_dir – Directory to write the cache into (created if missing).

  • lufs_per_file – Mapping from filepath to its per-window LUFS array.

  • lufs_window_sec – Analysis window length the arrays were computed with.

  • durations – Optional mapping from filepath to duration in seconds; stored so the cache can also supply durations= to the dataset.

  • sample_rate – The sample rate the loudness was measured at (None if native). Stored for the dataset’s consistency check.

  • mono – Whether channels were averaged to mono before measuring.

Returns:

The cache directory as a Path.

scan_durations(filepaths: List[str]) dict[str, float]

Read each file’s duration in seconds from its header (no decode).

soundfile.info reads only the container header, so this is cheap enough to run over tens of thousands of files. Persist the result (e.g. JSON/NPZ) and pass it back as durations= to skip the scan on subsequent runs; the slot index is a pure function of these durations, so alpha/hop/ duration can be retuned without re-reading any headers.

Parameters:

filepaths – Audio file paths to inspect.

Returns:

A mapping from filepath to duration in seconds.