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/__init__.py +12 -0
- splifft/__main__.py +197 -0
- splifft/config.py +265 -0
- splifft/core.py +551 -0
- splifft/inference.py +137 -0
- splifft/io.py +70 -0
- splifft/models/__init__.py +140 -0
- splifft/models/bs_roformer.py +718 -0
- splifft/models/utils/__init__.py +13 -0
- splifft/models/utils/attend.py +107 -0
- splifft/models/utils/attend_sage.py +136 -0
- splifft/training.py +68 -0
- splifft/utils/mil.py +608 -0
- splifft-0.0.2.dist-info/METADATA +266 -0
- splifft-0.0.2.dist-info/RECORD +18 -0
- splifft-0.0.2.dist-info/WHEEL +4 -0
- splifft-0.0.2.dist-info/entry_points.txt +2 -0
- splifft-0.0.2.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|