splifft 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
splifft/core.py ADDED
@@ -0,0 +1,551 @@
1
+ """Reusable, pure algorithmic components for inference and training."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Annotated, Generic, Iterator, Literal, NewType, TypeVar
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from annotated_types import Ge, Gt, Lt
11
+ from torch import Tensor
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import Mapping, Sequence, TypeAlias
15
+
16
+ from .config import DerivedStemsConfig, StemName
17
+ from .models import ModelOutputStemName
18
+
19
+
20
+ _AudioTensorLike = TypeVar("_AudioTensorLike")
21
+
22
+
23
+ @dataclass
24
+ class Audio(Generic[_AudioTensorLike]):
25
+ data: _AudioTensorLike
26
+ """This should either be an [raw][splifft.core.RawAudioTensor] or a
27
+ [normalized][splifft.core.NormalizedAudioTensor] audio tensor."""
28
+ sample_rate: SampleRate
29
+
30
+
31
+ #
32
+ # normalization
33
+ #
34
+
35
+
36
+ @dataclass
37
+ class NormalizationStats:
38
+ """Statistics for normalizing and denormalizing audio.
39
+
40
+ Neural networks are sensitive to the scale of input data and often perform better with inputs
41
+ that have a consistent statistical distribution. Normalization helps to achieve this.
42
+ """
43
+
44
+ mean: float
45
+ r"""Mean $\mu$ of the mixture"""
46
+ std: Annotated[float, Gt(0)]
47
+ r"""Standard deviation $\sigma$ of the mixture"""
48
+
49
+
50
+ @dataclass
51
+ class NormalizedAudio:
52
+ """Container for normalized audio and its original stats."""
53
+
54
+ audio: Audio[NormalizedAudioTensor] # NOTE: composition over inheritance.
55
+ stats: NormalizationStats
56
+
57
+
58
+ def normalize_audio(audio: Audio[RawAudioTensor]) -> NormalizedAudio:
59
+ """Preprocess the raw audio in the time domain to have a mean of 0 and a std of 1
60
+ before passing it to the model.
61
+
62
+ Operates on the mean of the [channels][splifft.core.Channels].
63
+ """
64
+ mono_audio = audio.data.mean(dim=0)
65
+ mean = float(mono_audio.mean())
66
+ std = float(mono_audio.std())
67
+
68
+ if std <= 1e-8: # silent audio
69
+ return NormalizedAudio(
70
+ audio=Audio(data=NormalizedAudioTensor(audio.data), sample_rate=audio.sample_rate),
71
+ stats=NormalizationStats(mean, 1.0),
72
+ )
73
+
74
+ normalized_data = (audio.data - mean) / std
75
+ return NormalizedAudio(
76
+ audio=Audio(data=NormalizedAudioTensor(normalized_data), sample_rate=audio.sample_rate),
77
+ stats=NormalizationStats(mean, std),
78
+ )
79
+
80
+
81
+ def denormalize_audio(
82
+ audio_data: NormalizedAudioTensor, stats: NormalizationStats
83
+ ) -> RawAudioTensor:
84
+ """Take the model output and restore them to their original loudness."""
85
+ return RawAudioTensor((audio_data * stats.std) + stats.mean)
86
+
87
+
88
+ #
89
+ # chunking
90
+ #
91
+
92
+
93
+ def generate_chunks(
94
+ audio_data: RawAudioTensor | NormalizedAudioTensor,
95
+ chunk_size: ChunkSize,
96
+ hop_size: HopSize,
97
+ batch_size: BatchSize,
98
+ *,
99
+ padding_mode: PaddingMode = "reflect",
100
+ ) -> Iterator[PaddedChunkedAudioTensor]:
101
+ """Generates batches of overlapping chunks from an audio tensor.
102
+
103
+ :return: An iterator that yields batches of chunks of shape (B, C, chunk_T).
104
+ """
105
+ padding = chunk_size - hop_size
106
+ padded_audio = F.pad(audio_data, (padding, padding), mode=padding_mode)
107
+
108
+ unfolded = padded_audio.unfold(
109
+ dimension=-1, size=chunk_size, step=hop_size
110
+ ) # (C, num_chunks, chunk_size)
111
+
112
+ num_chunks = unfolded.shape[1]
113
+ unfolded = unfolded.permute(1, 0, 2) # (num_chunks, C, chunk_size)
114
+
115
+ for i in range(0, num_chunks, batch_size):
116
+ yield PaddedChunkedAudioTensor(unfolded[i : i + batch_size])
117
+
118
+
119
+ def stitch_chunks(
120
+ processed_chunks: Sequence[SeparatedChunkedTensor],
121
+ num_stems: NumModelStems,
122
+ chunk_size: ChunkSize,
123
+ hop_size: HopSize,
124
+ target_num_samples: Samples,
125
+ *,
126
+ window: WindowTensor,
127
+ ) -> RawSeparatedTensor:
128
+ r"""Stitches processed audio chunks back together using the [overlap-add method](https://en.wikipedia.org/wiki/Overlap%E2%80%93add_method).
129
+
130
+ Reconstructs the full audio signal from a sequence of overlapping, processed chunks.
131
+ To avoid artifacts at chunk boundaries, each chunk is multiplied by a synthesis window $g[n]$
132
+ before being added to the final output buffer.
133
+
134
+ We must ensure that the sum of all overlapping windows $w[n]$ is constant at every time step:
135
+ $$
136
+ \sum_{m=-\infty}^{\infty} w[n - mH] = C
137
+ $$
138
+ where $H$ is the [hop size][splifft.core.HopSize] and $C$ is a constant, ideally 1. Not to be
139
+ confused with the condition for *power-complementary* windows ($\sum w^2 = C$), which is used to
140
+ reconstruct the signal's *power*.
141
+ """
142
+ all_chunks = torch.cat(tuple(processed_chunks), dim=0)
143
+ total_chunks, _N, num_channels, _chunk_T = all_chunks.shape
144
+ windowed_chunks = all_chunks * window.view(1, 1, 1, -1)
145
+
146
+ # folding: (B, N * C * chunk_T) -> (1, N * C * chunk_T, total_chunks)
147
+ reshaped_for_fold = windowed_chunks.permute(1, 2, 3, 0).reshape(
148
+ 1, num_stems * num_channels * chunk_size, total_chunks
149
+ )
150
+
151
+ total_length = (total_chunks - 1) * hop_size + chunk_size
152
+
153
+ folded = F.fold(
154
+ reshaped_for_fold,
155
+ output_size=(1, total_length),
156
+ kernel_size=(1, chunk_size),
157
+ stride=(1, hop_size),
158
+ ) # (1, N * C, 1, total_length)
159
+ stitched = folded.view(num_stems, num_channels, total_length)
160
+
161
+ # normalization for overlap-add
162
+ ones_template = torch.ones(1, 1, total_length, device=window.device)
163
+ unfolded_ones = ones_template.unfold(dimension=-1, size=chunk_size, step=hop_size)
164
+ windowed_unfolded_ones = unfolded_ones * window
165
+ reshaped_for_fold_norm = windowed_unfolded_ones.permute(0, 1, 3, 2).reshape(
166
+ 1, chunk_size, total_chunks
167
+ )
168
+ norm_window = F.fold(
169
+ reshaped_for_fold_norm,
170
+ output_size=(1, total_length),
171
+ kernel_size=(1, chunk_size),
172
+ stride=(1, hop_size),
173
+ ).squeeze()
174
+
175
+ norm_window.clamp_min_(1e-8) # for edges where the window sum might be zero
176
+ stitched /= norm_window
177
+
178
+ padding = chunk_size - hop_size
179
+ return RawSeparatedTensor(stitched[..., padding : padding + target_num_samples])
180
+
181
+
182
+ #
183
+ # derive stems
184
+ #
185
+ def derive_stems(
186
+ separated_stems: Mapping[ModelOutputStemName, RawAudioTensor],
187
+ mixture_input: RawAudioTensor,
188
+ stem_rules: DerivedStemsConfig,
189
+ ) -> dict[StemName, RawAudioTensor]:
190
+ """
191
+ It is the caller's responsibility to ensure that all tensors are aligned and have the same shape.
192
+
193
+ !!! note
194
+ Mixture input and separated stems must first be [denormalized][splifft.core.denormalize_audio].
195
+ """
196
+ stems = {
197
+ "mixture": RawAudioTensor(mixture_input), # for subtraction
198
+ **separated_stems,
199
+ }
200
+
201
+ for derived_name, rule in stem_rules.items():
202
+ if rule.operation == "subtract":
203
+ minuend = stems.get(rule.stem_name, mixture_input)
204
+ subtrahend = stems.get(rule.by_stem_name, mixture_input)
205
+ stems[derived_name] = RawAudioTensor(minuend - subtrahend)
206
+ elif rule.operation == "sum":
207
+ to_sum = tuple(stems[s] for s in rule.stem_names)
208
+ stems[derived_name] = RawAudioTensor(torch.stack(to_sum).sum(dim=0))
209
+
210
+ stems.pop("mixture", None)
211
+ return stems
212
+
213
+
214
+ #
215
+ # misc
216
+ #
217
+
218
+
219
+ def get_dtype(dtype: Dtype) -> torch.dtype:
220
+ if dtype == "float32":
221
+ return torch.float32
222
+ elif dtype == "float16":
223
+ return torch.float16
224
+ elif dtype == "bfloat16":
225
+ return torch.bfloat16
226
+ else:
227
+ raise ValueError(f"unsupported {dtype=}")
228
+
229
+
230
+ #
231
+ # The following used purely for type annotations and documentation.
232
+ # they provide semantic meaning *only* and we additionally use `NewType` for strong semantic distinction.
233
+ # to avoid mixing up different kinds of tensors.
234
+ #
235
+ # they are put right at the bottom for brevity and so no code implementations shall be placed beyond this point.
236
+ #
237
+
238
+ #
239
+ # key time domain concepts
240
+ #
241
+
242
+ Samples: TypeAlias = int
243
+ """Number of samples in the audio signal."""
244
+
245
+ SampleRate: TypeAlias = Annotated[int, Gt(0)]
246
+ """The number of samples of audio recorded per second (hertz).
247
+
248
+ According to the [Nyquist-Shannon sampling theorem](https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem),
249
+ the maximum frequency that can be accurately represented is half the sample rate. The full range of
250
+ human hearing is approximately 20 Hz to 20000 Hz.
251
+
252
+ - 44100 Hz: Standard for CD audio, most common sample rate for music.
253
+ - 48000 Hz: Standard in professional audio
254
+ - 16000 Hz: Common for voice recordings as it sufficiently captures the human voice
255
+ """
256
+
257
+ Channels: TypeAlias = int
258
+ """Number of audio streams.
259
+
260
+ - 1: Mono audio
261
+ - 2: Stereo (left and right). Models are usually trained on stereo audio.
262
+ """
263
+
264
+
265
+ FileFormat: TypeAlias = Literal["flac", "wav", "ogg"]
266
+ AudioEncoding: TypeAlias = Literal["PCM_S", "PCM_U", "PCM_F", "ULAW", "ALAW"]
267
+ """
268
+ [Audio encoding](https://trac.ffmpeg.org/wiki/audio%20types)
269
+
270
+ - `PCM_S`: Signed integer linear pulse-code modulation
271
+ - `PCM_U`: Unsigned integer linear pulse-code modulation
272
+ - `PCM_F`: Floating-point pulse-code modulation
273
+ - `ULAW`: [μ-law](https://en.wikipedia.org/wiki/%CE%9C-law_algorithm)
274
+ - `ALAW`: [a-law](https://en.wikipedia.org/wiki/A-law_algorithm)
275
+ """
276
+ BitDepth: TypeAlias = Literal[8, 16, 24, 32, 64]
277
+ """Number of bits of information in each sample.
278
+
279
+ It determines the dynamic range of the audio signal: the difference between the quietest and loudest
280
+ possible sounds.
281
+
282
+ - 16-bit: Standard for CD audio: ~96 dB dynamic range.
283
+ - 24-bit: Common in professional audio, allowing for more headroom during mixing
284
+ - 32-bit float: Standard in digital audio workstations (DAWs) and deep learning models.
285
+ The amplitude is represented by a floating-point number, which prevents clipping (distortion
286
+ from exceeding the maximum value). This library primarily works with fp32 tensors.
287
+ """
288
+
289
+ RawAudioTensor = NewType("RawAudioTensor", Tensor)
290
+ """Time domain tensor of audio samples.
291
+ Shape ([channels][splifft.core.Channels], [samples][splifft.core.Samples])"""
292
+
293
+ NormalizedAudioTensor = NewType("NormalizedAudioTensor", Tensor)
294
+ """A mixture tensor that has been normalized using [on-the-fly statistics][splifft.core.NormalizationStats].
295
+ Shape ([channels][splifft.core.Channels], [samples][splifft.core.Samples])"""
296
+
297
+ #
298
+ # key time-frequency domain concepts
299
+ #
300
+
301
+ ComplexSpectrogram = NewType("ComplexSpectrogram", Tensor)
302
+ r"""A complex-valued representation of audio's frequency content over time.
303
+
304
+ Shape ([channels][splifft.core.Channels], [frequency bins][splifft.core.FftSize], [time frames][splifft.core.ChunkSize], 2)
305
+
306
+ While the time domain gives us the amplitude over time, it doesn't explicitly tell us about
307
+ frequency content. The [Short-Time Fourier Transform](https://en.wikipedia.org/wiki/Short-time_Fourier_transform)
308
+ (STFT) is the cornerstone of transforming a 1D discrete-time signal $x[n]$ into a 2D time-frequency
309
+ representation $X[m, k]$ of shape `(frequency_bins, time_frames, 2)`).
310
+
311
+ The STFT coefficient $X[m, k]$ is a complex number that can be decomposed into:
312
+
313
+ - the **magnitude** $|X[m, k]|$ (tells us "how much" of a frequency is present)
314
+ - the **phase** $\phi(m, k)$ (tells us "how it's aligned" in time)
315
+
316
+ of a specific time frame, where $m$ is the time frame index and $k$ is the frequency bin index.
317
+ Human hearing is highly sensitive to phase differences, crucial for sound localisation and timbre
318
+ perception.
319
+
320
+ Phase is notoriously difficult to model: it behaves chaotically and wraps around from $-\pi$ to
321
+ $\pi$. Early models discarded phase information, focusing only on the magnitude spectrogram,
322
+ and used the [Griffin-Lim algorithm](https://ieeexplore.ieee.org/document/1164317) to estimate the
323
+ plausible phase. Modern models like [SCNet](https://arxiv.org/abs/2401.13276) use the complex valued
324
+ spectrogram as the loss function directly.
325
+
326
+ Definition:
327
+ $$
328
+ X(m, k) = \sum_{n=-\infty}^{\infty} x[n] \cdot w[n - mH] \cdot e^{-j \frac{2\pi kn}{N_\text{fft}}},
329
+ $$
330
+ where:
331
+
332
+ Practically, the process involves:
333
+
334
+ 1. Dividing the audio signal into short, overlapping segments in time (chunks), parameterised by the
335
+ [hop size][splifft.core.HopSize] $H$
336
+ 2. Applying a [window function][splifft.core.WindowShape] $w[n]$ (e.g.
337
+ [Hann window][torch.hann_window]) to each chunk to reduce spectral leakage
338
+ 3. Computing the Fast Fourier Transform (FFT) on each windowed segment to get its complex frequency
339
+ spectrum. The [FFT size][splifft.core.FftSize] $N_\text{fft}$ determines the number of frequency
340
+ bins.
341
+ 4. Stacking these spectra to form the 2D complex spectrogram.
342
+
343
+ Some models like [BS-Roformer][splifft.models.bs_roformer.BSRoformer] use the linear frequency scale and
344
+ learn their own perceptually relevant [bandings][splifft.core.Bands]. Other models like Mel-Roformer
345
+ is based on the [Mel scale](https://en.wikipedia.org/wiki/Mel_scale), which is a perceptual scale
346
+ of pitches that approximates human hearing.
347
+
348
+ Neural networks in source separation essentially learn to approximate an ideal ratio mask (or its
349
+ complex equivalent): $\hat{S}_\text{source} = M_\text{complex} \odot S_\text{mixture}$.
350
+ """
351
+
352
+ HopSize: TypeAlias = int
353
+ """The step size, in samples, between the start of consecutive [chunks][splifft.core.ChunkSize].
354
+
355
+ To avoid artifacts at the edges of chunks, we process them with overlap. The hop size is the
356
+ distance we "slide" the chunking window forward. `ChunkSize < HopSize` implies overlap and the
357
+ overlap amount is `ChunkSize - HopSize`.
358
+ """
359
+
360
+
361
+ WindowShape: TypeAlias = Literal["hann", "hamming", "linear_fade"]
362
+ r"""The shape of the window function applied to each chunk before computing the STFT.
363
+
364
+ Reduces spectral leakage""" # NOTE: sharing both for stft and overlap-add stitching for now
365
+
366
+
367
+ FftSize: TypeAlias = int
368
+ r"""The number of frequency bins in the STFT.
369
+
370
+ The [time-frequency uncertainty principle](https://en.wikipedia.org/wiki/Uncertainty_principle#Signal_processing)
371
+ states that there is a fundamental tradeoff between the standard deviations in time and frequency
372
+ energy concentrations:
373
+ $$
374
+ \sigma_t \sigma_f \ge \frac{1}{4\pi}
375
+ $$
376
+
377
+ - A short window (small $N_\text{fft}$) gives good time resolution, excellent for capturing sharp
378
+ percussive sounds like drum hits (transients), but it blurs frequencies together, making it hard to
379
+ separate instruments with close pitches.
380
+ - A long window (large $N_\text{fft}$) gives good frequency resolution, excellent for separating the
381
+ fine harmonics of tonal instruments like a violin or piano, but it blurs the exact timing.
382
+
383
+ The `auraloss` library's `MultiResolutionSTFTLoss` calculates the loss on spectrograms with
384
+ multiple FFT sizes, forcing the model to optimise for both transient and tonal sources.
385
+ """
386
+
387
+ Bands: TypeAlias = Tensor
388
+ """Groups of adjacent frequency bins in the spectrogram.
389
+
390
+ Instead of processing every single frequency bin independently, we can group them into "bands".
391
+ This reduces the computational complexity and allows the model to learn relationships within
392
+ frequency regions, which often correspond to musical harmonics or instrument characteristics.
393
+ """
394
+
395
+ #
396
+ # miscallaneous
397
+ #
398
+ BatchSize: TypeAlias = Annotated[int, Gt(0)]
399
+ """The number of chunks processed simultaneously by the GPU.
400
+
401
+ Increasing the batch size can improve GPU utilisation and speed up training, but it requires more
402
+ memory.
403
+ """
404
+
405
+ Dtype: TypeAlias = Literal["float32", "float16", "bfloat16"]
406
+
407
+ # preprocessing
408
+
409
+ PaddingMode: TypeAlias = Literal["reflect", "constant", "replicate"]
410
+ """The method used to pad the audio before chunking, crucial for handling the edges of the audio signal.
411
+
412
+ - `reflect`: Pads the signal by reflecting the audio at the boundary. This creates a smooth
413
+ continuation and often yields the best results for music.
414
+ - `constant`: Pads with zeros. Simpler, but can introduce silence at the edges.
415
+ - `replicate`: Repeats the last sample at the edge.
416
+ """
417
+ # TODO: we should intelligently decide whether to choose reflect or constant.
418
+ # for songs that start with silence, we should use constant padding.
419
+
420
+ ChunkSize: TypeAlias = Annotated[int, Gt(0)]
421
+ """The length of an audio segment, in samples, processed by the model at one time.
422
+
423
+ A full audio track is often too long to fit into GPU, instead we process it in fixed-size chunks.
424
+ A larger chunk size may allow the model to capture more temporal context at the cost of increased
425
+ memory usage.
426
+ """
427
+
428
+ ChunkDuration: TypeAlias = Annotated[float, Gt(0)]
429
+ """The length of an audio segment, in seconds, processed by the model at one time.
430
+
431
+ Equivalent to [chunk size][splifft.core.ChunkSize] divided by the [sample rate][splifft.core.SampleRate].
432
+ """
433
+
434
+ OverlapRatio: TypeAlias = Annotated[float, Ge(0), Lt(1)]
435
+ r"""The fraction of a chunk that overlaps with the next one.
436
+
437
+ The relationship with [hop size][splifft.core.HopSize] is:
438
+ $$
439
+ \text{hop\_size} = \text{chunk\_size} \cdot (1 - \text{overlap\_ratio})
440
+ $$
441
+
442
+ - A ratio of `0.0` means no overlap (hop_size = chunk_size).
443
+ - A ratio of `0.5` means 50% overlap (hop_size = chunk_size / 2).
444
+ - A higher overlap ratio increases computational cost as more chunks are processed, but it can lead
445
+ to smoother results by averaging more predictions for each time frame.
446
+ """
447
+
448
+ Padding: TypeAlias = int
449
+ """Samples to add to the beginning and end of each chunk.
450
+
451
+ - To ensure that the very beginning and end of a track can be centerd within a chunk, we often may
452
+ add "reflection padding" or "zero padding" before chunking.
453
+ - To ensure that the last chunk is full-size, we may pad the audio so its length is a multiple of
454
+ the hop size.
455
+ """
456
+
457
+ PaddedChunkedAudioTensor = NewType("PaddedChunkedAudioTensor", Tensor)
458
+ """A batch of audio chunks from a padded source.
459
+ Shape ([batch size][splifft.core.BatchSize], [channels][splifft.core.Channels], [chunk size][splifft.core.ChunkSize])"""
460
+
461
+ NumModelStems: TypeAlias = int
462
+ """The number of stems the model outputs. This should be the length of [splifft.models.ModelConfigLike.output_stem_names]."""
463
+
464
+ # post separation stitching
465
+
466
+ SeparatedChunkedTensor = NewType("SeparatedChunkedTensor", Tensor)
467
+ """A batch of separated audio chunks from the model.
468
+ Shape ([batch size][splifft.core.BatchSize], [number of stems][splifft.core.NumModelStems], [channels][splifft.core.Channels], [chunk size][splifft.core.ChunkSize])"""
469
+
470
+ WindowTensor = NewType("WindowTensor", Tensor)
471
+ """A 1D tensor representing a window function.
472
+ Shape ([chunk size][splifft.core.ChunkSize])"""
473
+
474
+ RawSeparatedTensor = NewType("RawSeparatedTensor", Tensor)
475
+ """The final, stitched, raw-domain separated audio.
476
+ Shape ([number of stems][splifft.core.NumModelStems], [channels][splifft.core.Channels], [samples][splifft.core.Samples])"""
477
+
478
+ #
479
+ # evaluation metrics
480
+ # We use bold letters like $\mathbf{s}$ to denote the entire signal tensor.
481
+ #
482
+
483
+ SDR: TypeAlias = float
484
+ r"""Signal-to-Distortion Ratio (decibels). Higher is better.
485
+
486
+ Measures the ratio of the power of clean reference signal to the power of all other error components
487
+ (interference, artifacts, and spatial distortion).
488
+
489
+ Definition:
490
+ $$
491
+ \text{SDR} = 10 \log_{10} \frac{|\mathbf{s}|^2}{|\mathbf{s} - \mathbf{\hat{s}}|^2},
492
+ $$
493
+ where:
494
+
495
+ - $\mathbf{s}$: ground truth source signal
496
+ - $\mathbf{\hat{s}}$: estimated source signal produced by the model
497
+ - $||\cdot||^2$: squared L2 norm (power) of the signal
498
+ """
499
+
500
+ SISDR: TypeAlias = float
501
+ r"""Scale-Invariant SDR (SI-SDR) is invariant to scaling errors (decibels). Higher is better.
502
+
503
+ It projects the estimate onto the reference to find the optimal scaling factor $\alpha$, creating a scaled reference that best matches the estimate's amplitude.
504
+
505
+ - Optimal scaling factor: $\alpha = \frac{\langle\mathbf{\hat{s}}, \mathbf{s}\rangle}{||\mathbf{s}||^2}$
506
+ - Scaled reference: $\mathbf{s}_\text{target} = \alpha \cdot \mathbf{s}$
507
+ - Error: $\mathbf{e} = \mathbf{\hat{s}} - \mathbf{s}_\text{target}$
508
+ - $\text{SI-SDR} = 10 \log_{10} \frac{||\mathbf{s}_\text{target}||^2}{||\mathbf{e}||^2}$
509
+ """
510
+
511
+ L1Norm: TypeAlias = float
512
+ r"""L1 norm (mean absolute error) between two signals (dimensionless). Lower is better.
513
+
514
+ Measures the average absolute difference between the reference and estimated signals.
515
+
516
+ - Time domain: $\mathcal{L}_\text{L1} = \frac{1}{N}
517
+ \sum_{n=1}^{N} |\mathbf{s}[n] - \mathbf{\hat{s}}[n]|$,
518
+ - Frequency domain: $\mathcal{L}_\text{L1Freq} = \frac{1}{\text{MK}}\sum_{m=1}^{M}
519
+ \sum_{k=1}^{K} \left||S(m, k)| - |\hat{S}(m, k)|\right|$
520
+ """ # NOTE: zfturbo scales by to 1-100
521
+
522
+ DbDifferenceMel: TypeAlias = float
523
+ r"""Difference in the dB-scaled mel spectrogram.
524
+ $$
525
+ \mathbf{D}(m, k) = \text{dB}(|\hat{S}_\text{mel}(m, k)|) - \text{dB}(|S_\text{mel}(m, k)|)
526
+ $$
527
+ """
528
+
529
+ Bleedless: TypeAlias = float
530
+ r"""A metric to quantify the amount of "bleeding" from other sources. Higher is better.
531
+
532
+ Measures the average energy of the parts of the [mel spectrogram][splifft.core.DbDifferenceMel]
533
+ that are louder than the reference.
534
+ A high value indicates that the estimate contains unwanted energy (bleed) from other sources:
535
+ $$
536
+ \text{Bleed} = \text{mean}(\mathbf{D}(m, k)) \quad \forall \quad \mathbf{D}(m, k) > 0
537
+ $$
538
+ """
539
+
540
+ Fullness: TypeAlias = float
541
+ r"""A metric to quantify how much of the original source is missing. Higher is better.
542
+
543
+ Complementary to [Bleedless][splifft.core.Bleedless].
544
+ Measures the average energy of the parts of the [mel spectrogram][splifft.core.DbDifferenceMel]
545
+ that are quieter than the reference.
546
+ A high value indicates that parts of the target loss were lost during the separation, indicating
547
+ that more of the original source's character is preserved.
548
+ $$
549
+ \text{Fullness} = \text{mean}(|\mathbf{D}(m, k)|) \quad \forall \quad \mathbf{D}(m, k) < 0
550
+ $$
551
+ """
splifft/inference.py ADDED
@@ -0,0 +1,137 @@
1
+ """High level orchestrator for model inference"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import torch
8
+ from torch import Tensor, nn
9
+
10
+ from .core import (
11
+ NormalizedAudioTensor,
12
+ RawAudioTensor,
13
+ WindowTensor,
14
+ denormalize_audio,
15
+ derive_stems,
16
+ generate_chunks,
17
+ get_dtype,
18
+ normalize_audio,
19
+ stitch_chunks,
20
+ )
21
+
22
+ try:
23
+ from tqdm import tqdm
24
+ except ImportError:
25
+ tqdm = None
26
+
27
+ if TYPE_CHECKING:
28
+ from .config import ChunkingConfig, Config, StemName
29
+ from .core import Audio, BatchSize, ChunkSize, Dtype, NormalizationStats, NumModelStems
30
+ from .models import ModelOutputStemName
31
+
32
+
33
+ def run_inference_on_file(
34
+ mixture: Audio[RawAudioTensor], config: Config, model: nn.Module
35
+ ) -> dict[StemName, RawAudioTensor]:
36
+ """Runs the full source separation pipeline on a single audio file."""
37
+
38
+ mixture_data: RawAudioTensor | NormalizedAudioTensor = mixture.data
39
+ mixture_stats: NormalizationStats | None = None
40
+ if config.inference.normalize_input_audio:
41
+ norm_audio = normalize_audio(mixture)
42
+ mixture_data = norm_audio.audio.data
43
+ mixture_stats = norm_audio.stats
44
+
45
+ separated_data = separate(
46
+ mixture_data=mixture_data,
47
+ chunk_cfg=config.chunking,
48
+ model=model,
49
+ batch_size=config.inference.batch_size,
50
+ num_model_stems=len(config.model.output_stem_names),
51
+ chunk_size=config.model.chunk_size,
52
+ use_autocast_dtype=config.inference.use_autocast_dtype,
53
+ )
54
+
55
+ denormalized_stems: dict[ModelOutputStemName, RawAudioTensor] = {}
56
+ for i, stem_name in enumerate(config.model.output_stem_names):
57
+ stem_data = separated_data[i, ...]
58
+ if mixture_stats is not None:
59
+ stem_data = denormalize_audio(
60
+ audio_data=NormalizedAudioTensor(stem_data),
61
+ stats=mixture_stats,
62
+ )
63
+ denormalized_stems[stem_name] = stem_data
64
+ else:
65
+ denormalized_stems[stem_name] = RawAudioTensor(stem_data)
66
+
67
+ if config.inference.apply_tta:
68
+ raise NotImplementedError
69
+
70
+ output_stems = denormalized_stems
71
+ if config.derived_stems:
72
+ output_stems = derive_stems(
73
+ denormalized_stems,
74
+ mixture.data,
75
+ config.derived_stems,
76
+ )
77
+
78
+ return output_stems
79
+
80
+
81
+ def separate(
82
+ mixture_data: RawAudioTensor | NormalizedAudioTensor,
83
+ chunk_cfg: ChunkingConfig,
84
+ model: nn.Module,
85
+ batch_size: BatchSize,
86
+ num_model_stems: NumModelStems,
87
+ chunk_size: ChunkSize,
88
+ *,
89
+ use_autocast_dtype: Dtype | None = None,
90
+ ) -> Tensor: # FIXME: update type hint.
91
+ """Chunk, predict and stitch."""
92
+ device = mixture_data.device
93
+ original_num_samples = mixture_data.shape[-1]
94
+
95
+ hop_size = int(chunk_size * (1 - chunk_cfg.overlap_ratio))
96
+
97
+ if chunk_cfg.window_shape == "hann":
98
+ window = torch.hann_window(chunk_size, device=device)
99
+ else:
100
+ raise NotImplementedError(f"{chunk_cfg.window_shape=}")
101
+
102
+ chunk_generator = generate_chunks(
103
+ audio_data=mixture_data,
104
+ chunk_size=chunk_size,
105
+ hop_size=hop_size,
106
+ batch_size=batch_size,
107
+ padding_mode=chunk_cfg.padding_mode,
108
+ )
109
+ if tqdm is not None:
110
+ chunk_generator = tqdm(
111
+ chunk_generator,
112
+ desc="Processing chunks",
113
+ )
114
+
115
+ processed_chunks = []
116
+ with (
117
+ torch.inference_mode(),
118
+ torch.autocast(
119
+ device_type=device.type,
120
+ enabled=use_autocast_dtype is not None,
121
+ dtype=(
122
+ get_dtype(use_autocast_dtype) if use_autocast_dtype is not None else torch.float32
123
+ ),
124
+ ),
125
+ ):
126
+ for chunk_batch in chunk_generator:
127
+ separated_batch = model(chunk_batch)
128
+ processed_chunks.append(separated_batch)
129
+
130
+ return stitch_chunks(
131
+ processed_chunks=processed_chunks,
132
+ num_stems=num_model_stems,
133
+ chunk_size=chunk_size,
134
+ hop_size=hop_size,
135
+ target_num_samples=original_num_samples,
136
+ window=WindowTensor(window),
137
+ )