STFT and features¶
kudio.core.stft
¶
STFT geometry as a value you can pass around and save.
A spectrogram and the waveform reconstructed from it must be computed with the
same n_fft / hop_length / win_length / window. Passing those
four to two separate functions and keeping them in sync by hand is how a
producer and a consumer end up quietly disagreeing — the geometry belongs
with the data that was made from it.
>>> stft = kudio.STFT(sr=16000, n_fft=512, hop_length=256)
>>> spec = stft.forward(y) # (frames, bins)
>>> back = stft.inverse(y, spec) # phase taken from y
>>> stft.to_dict() # store next to a model checkpoint
{'sr': 16000, 'n_fft': 512, 'hop_length': 256, 'win_length': 512, 'window': 'hamming'}
A thin wrapper: waveform_to_spectrogram and
spectrogram_to_waveform do the work.
Classes:
| Name | Description |
|---|---|
STFT |
Immutable STFT settings, with the two transforms attached. |
STFT
dataclass
¶
STFT(
sr: int = 16000,
n_fft: int = 512,
hop_length: int = 256,
win_length: Optional[int] = None,
window: str = "hamming",
)
Immutable STFT settings, with the two transforms attached.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sr
|
int
|
sample rate the settings were chosen for. Carried so a stored geometry can be checked against the audio it is applied to; the transforms themselves do not resample. |
16000
|
win_length
|
Optional[int]
|
defaults to n_fft. |
None
|
Methods:
| Name | Description |
|---|---|
n_frames |
Frames a waveform of n_samples will produce (centred STFT). |
frame_times |
Centre time, in seconds, of each frame. |
forward |
Waveform -> log-power spectrogram |
inverse |
Log-power spectrogram -> waveform, reusing the phase of y_ref. |
analyse |
Waveform -> complex spectrogram |
synthesise |
Complex spectrogram -> waveform, the inverse of |
to_dict |
Plain dict, ready for JSON/YAML next to a checkpoint. |
from_dict |
Rebuild the geometry |
matches |
Is sr the rate this geometry was configured for? |
Attributes:
| Name | Type | Description |
|---|---|---|
n_bins |
int
|
Frequency bins produced by this geometry. |
n_frames
¶
Frames a waveform of n_samples will produce (centred STFT).
librosa pads n_fft // 2 on each side, so an odd n_fft gives one
frame fewer than n // hop + 1 whenever n is a multiple of the hop.
Source code in kudio/core/stft.py
frame_times
¶
forward
¶
forward(
y: ndarray,
*,
norm: bool = False,
sequence: bool = False,
context: int = 0,
desired_length: Optional[int] = None,
) -> ndarray
Waveform -> log-power spectrogram (frames, bins).
Source code in kudio/core/stft.py
inverse
¶
Log-power spectrogram -> waveform, reusing the phase of y_ref.
Source code in kudio/core/stft.py
analyse
¶
Waveform -> complex spectrogram (frames, bins).
forward throws the phase away, which is right for a feature and
wrong for anything that has to reconstruct the signal. Enhancement
multiplies the complex spectrum by a real gain and inverts it, so it
needs the phase kept — see kudio.spectral_enhance.
Source code in kudio/core/stft.py
synthesise
¶
Complex spectrogram -> waveform, the inverse of analyse.
length trims (or pads) to an exact sample count. Pass the input length and the round trip is sample-aligned, which is what an A/B or a training target needs.
Source code in kudio/core/stft.py
to_dict
¶
from_dict
classmethod
¶
Rebuild the geometry to_dict stored.
Every key has to be there. A missing one taking its default is how a
stored geometry and the code reading it end up disagreeing -- a config
without hop_length would quietly analyse at 256.
Source code in kudio/core/stft.py
kudio.core.feature
¶
Feature extraction and spectrogram <-> waveform conversions.
v3 canonical names¶
================================= =============================================
canonical deprecated aliases
================================= =============================================
waveform_to_spectrogram w2s, wavform2spec
spectrogram_to_waveform spec2wavform
file_to_spectrogram f2s, wav2spec
save_spectrogram_as_wave s2w, spec2wav
mfcc w2mfcc, wav2mfcc
mfcc_from_files concat_mfcc, concat_mfcc_
logspec_from_files concat_logspec, concat_logspec_,
contextual_LogSpectrogram
melspectrogram wav2mel
================================= =============================================
The aliases still work but emit a DeprecationWarning.
Classes:
| Name | Description |
|---|---|
ContextFrames |
|
Standardizer |
Per-column mean/std normalisation, with the statistics saved alongside. |
Functions:
| Name | Description |
|---|---|
wave_separate |
Split an interleaved multi-channel waveform into per-channel arrays. |
waveform_to_spectrogram |
Waveform -> log-power spectrogram |
spectrogram_to_waveform |
Log-power spectrogram -> waveform, reusing the phase of reference y. |
file_to_spectrogram |
File -> log-power spectrogram (see |
save_spectrogram_as_wave |
Reconstruct a waveform from enhanced_spec (phase from noisy_file) |
mfcc |
Waveform -> MFCC, shaped |
mfcc_from_files |
Load each wave file and stack their MFCC matrices |
logspec_from_files |
Convert each file to a log spectrogram and stack them. |
melspectrogram |
Audio -> stacked log-mel feature vectors |
denoise_wav2spec |
Peak-normalized magnitude spectrogram + phase, for denoising models. |
denoise_spec2wav |
Inverse of |
stack_context |
Stack |
frame_windows |
Cut a feature matrix into fixed-length windows. |
wave_slicing |
Slice a waveform into overlapping frames (flattened if is_reshape). |
features2matrix |
Concatenate feature matrices; label rows by list index. |
ContextFrames
¶
stack_context over a whole corpus, stacked a batch at a time.
Stacking ±5 frames makes every row eleven rows wide, so a training set stacked up front takes eleven times the memory of its features. This keeps the features once -- each utterance padded at its own edges, so no window reaches into the next utterance -- and stacks only the rows asked for:
```pycon
>>> bank = kudio.ContextFrames(features, context=5) # (frames, dim) each
>>> bank.shape
(707112, 2827)
>>> x = bank[batch] # indices, a slice or a mask
Row i is row i of np.vstack([stack_context(f, context, pad) for f in
features]), so a target built with np.vstack(targets) lines up with
it index for index. A single (frames, dim) matrix is one utterance; a
3-D array is read as a stack of them.
Attributes:
| Name | Type | Description |
|---|---|---|
shape |
Tuple[int, int]
|
|
Source code in kudio/core/feature.py
Standardizer
¶
Per-column mean/std normalisation, with the statistics saved alongside.
Fit on the training features, then reused verbatim at inference — the statistics are as much a part of a trained model as its weights, and a model loaded without them produces confident nonsense.
>>> std = kudio.Standardizer().fit(train_frames)
>>> x = std.transform(frames)
>>> frames_again = std.inverse(x)
>>> std.save('runs/exp1/stats.npz')
Methods:
| Name | Description |
|---|---|
fit |
Learn each column's mean and standard deviation from frames |
transform |
|
inverse |
Undo |
fit_transform |
|
save |
Write the statistics to an |
load |
Read statistics |
Source code in kudio/core/feature.py
fit
¶
Learn each column's mean and standard deviation from frames
(rows, dim) -- the training set, never the data it is applied to.
Source code in kudio/core/feature.py
transform
¶
(frames - mean) / std, column by column, as float32.
fit_transform
¶
save
¶
Write the statistics to an .npz file and return its path.
numpy adds .npz to a name without it, so the path returned is the
file actually written: save('stats') returns stats.npz.
Source code in kudio/core/feature.py
load
classmethod
¶
Read statistics save wrote. 'stats' finds stats.npz.
Source code in kudio/core/feature.py
wave_separate
¶
Split an interleaved multi-channel waveform into per-channel arrays.
Source code in kudio/core/feature.py
waveform_to_spectrogram
¶
waveform_to_spectrogram(
y: ndarray,
sequence: bool = False,
forward_backward: int = 0,
norm: bool = False,
hop_length: int = 256,
n_fft: int = 512,
win_length: Optional[int] = None,
window: str = "hamming",
desired_length: Optional[int] = None,
) -> ndarray
Waveform -> log-power spectrogram (frames, bins), log10(|X|^2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ndarray
|
a mono waveform, shape |
required |
sequence
|
bool
|
return |
False
|
forward_backward
|
int
|
stack this many context frames on each side,
zero-padded ( |
0
|
norm
|
bool
|
standardise each frequency bin over this clip. |
False
|
hop_length
|
int
|
samples between frames. |
256
|
n_fft
|
int
|
FFT size; there are |
512
|
win_length
|
Optional[int]
|
window length, defaulting to n_fft. |
None
|
window
|
str
|
any window name librosa takes. |
'hamming'
|
desired_length
|
Optional[int]
|
cut the waveform to this many samples first. |
None
|
Raises:
| Type | Description |
|---|---|
FeatureError
|
for anything but a 1-D waveform. A
|
Source code in kudio/core/feature.py
spectrogram_to_waveform
¶
spectrogram_to_waveform(
y: ndarray,
enhanced_spec: ndarray,
sequence: bool = False,
hop_length: int = 256,
n_fft: int = 512,
win_length: Optional[int] = None,
window: str = "hamming",
) -> ndarray
Log-power spectrogram -> waveform, reusing the phase of reference y.
enhanced_spec is (frames, bins) with one row per frame of y, the
shape waveform_to_spectrogram gives for it. The result has exactly
len(y) samples.
Source code in kudio/core/feature.py
file_to_spectrogram
¶
file_to_spectrogram(
file: PathLike,
sequence: bool = False,
forward_backward: int = 0,
norm: bool = False,
hop_length: int = 256,
n_fft: int = 512,
win_length: Optional[int] = None,
desired_length: Optional[int] = None,
resample_rate: Optional[int] = None,
window: str = "hamming",
) -> ndarray
File -> log-power spectrogram (see waveform_to_spectrogram).
window comes last, after the arguments callers already pass by position.
Source code in kudio/core/feature.py
save_spectrogram_as_wave
¶
save_spectrogram_as_wave(
wave_out_dir: PathLike,
noisy_file: PathLike,
enhanced_spec: ndarray,
squeeze: bool = False,
hop_length: int = 256,
n_fft: int = 512,
win_length: Optional[int] = None,
window: str = "hamming",
) -> None
Reconstruct a waveform from enhanced_spec (phase from noisy_file) and write it to wave_out_dir as 16-bit PCM, clipped at full scale.
Source code in kudio/core/feature.py
mfcc
¶
Waveform -> MFCC, shaped (1, n_frames, n_mfcc). y is mono.
mfcc_from_files
¶
mfcc_from_files(
wav_list: Sequence,
n_mfcc: int = 40,
desired_samples: Optional[int] = None,
desired_length: Optional[int] = None,
) -> ndarray
Load each wave file and stack their MFCC matrices (n_files, frames, n_mfcc).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
desired_samples
|
Optional[int]
|
cut each file to this many samples. |
None
|
desired_length
|
Optional[int]
|
cut each file to this many seconds (the
spectrogram functions' |
None
|
Source code in kudio/core/feature.py
logspec_from_files
¶
logspec_from_files(
wav_list: Sequence,
sequence: bool,
forward_backward: int,
norm: bool,
hop_length: int = 256,
n_fft: int = 512,
win_length: Optional[int] = None,
desired_samples: Optional[int] = None,
window: str = "hamming",
) -> ndarray
Convert each file to a log spectrogram and stack them.
Source code in kudio/core/feature.py
melspectrogram
¶
melspectrogram(
source: Union[PathLike, ndarray],
n_mels: int = 64,
n_frames: int = 5,
n_fft: int = 1024,
hop_length: int = 512,
power: float = 2.0,
*,
sr: Optional[int] = None,
) -> ndarray
Audio -> stacked log-mel feature vectors (n_vectors, n_mels * n_frames).
source is a file path or a waveform; a waveform requires sr, the
same way mfcc does. With a path, sr resamples the file to that
rate, and leaving it out keeps the file's own.
The window is librosa's Hann, not the Hamming of STFT and
waveform_to_spectrogram.
Source code in kudio/core/feature.py
denoise_wav2spec
¶
Peak-normalized magnitude spectrogram + phase, for denoising models.
The two come back in different layouts: the spectrogram as
(1, frames, bins) and the phase as (bins, frames), the way
denoise_spec2wav takes them back.
Raises:
| Type | Description |
|---|---|
FeatureError
|
for a silent clip, which has no peak to normalise by. |
Source code in kudio/core/feature.py
denoise_spec2wav
¶
Inverse of denoise_wav2spec; output peak-normalized.
Source code in kudio/core/feature.py
stack_context
¶
Stack ±context neighbouring frames onto each frame.
(frames, dim) -> (frames, dim * (2 * context + 1)), one output row
per input row: the edges are padded rather than dropped, so a prediction
made from the result lines up 1:1 with the input.
Works on any feature matrix — MFCC, mel, log-spectrogram — unlike the
forward_backward argument of waveform_to_spectrogram, which
only reaches the spectrogram it computes itself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pad
|
str
|
|
'edge'
|
Source code in kudio/core/feature.py
frame_windows
¶
Cut a feature matrix into fixed-length windows.
(frames, dim) -> (n_windows, n_frames, dim). The tail is padded
rather than dropped, so a clip shorter than one window still yields one —
the shape sequence models are trained on.
Source code in kudio/core/feature.py
wave_slicing
¶
wave_slicing(
y: ndarray,
frame_length: int = 2048,
hop_length: int = 512,
is_reshape: bool = True,
) -> ndarray
Slice a waveform into overlapping frames (flattened if is_reshape).
Source code in kudio/core/feature.py
features2matrix
¶
Concatenate feature matrices; label rows by list index.
Source code in kudio/core/feature.py
kudio.core.spectrogram
¶
A spectrogram for audio that has not finished arriving.
kudio.STFT and kudio.waveform_to_spectrogram want the whole
clip. That is right for a file and impossible for a microphone, and the usual
workaround -- keep the last few seconds in a list and re-run the STFT over all
of it several times a second -- is wasteful in a way that shows: at 16 kHz with
a 3-second window and the default geometry, twenty redraws a second recompute
375 columns to gain about six, and 98% of that work redraws what was already
on screen.
SpectrogramStream computes each column once, when the samples for
it have arrived, and keeps the last N in a ring buffer. Feeding it a stream
in whatever block sizes the device happens to deliver produces exactly the
columns the offline transform would.
>>> spec = kudio.SpectrogramStream(sr=16000, seconds=3.0)
>>> while recorder.recording:
... spec.push(recorder.drain())
... draw(spec.columns()) # (columns, bins), dB
Columns are not centred. A file transform pads the start so that column
k is centred on sample k x hop; a stream cannot pad audio it has not heard
yet, so column k here covers samples [k*hop, k*hop + n_fft) -- the
center=False convention. The first column therefore needs a whole frame
before it exists, which is latency_seconds.
Values are dBFS on a linear axis. Amplitude is normalised by the window,
so a full-scale sine reads 0 dB whatever the frame size and window are. A mel
axis sums bins through librosa's area-normalised filters, so there the number
is band power in dB, not dBFS: a full-scale sine reads about -18 dB at 1 kHz. The number on screen
then means the same thing after someone changes n_fft -- which it does not
if the scaling is left to the FFT.
Classes:
| Name | Description |
|---|---|
SpectrogramStream |
A rolling log-magnitude spectrogram, one column per hop. |
SpectrogramStream
¶
SpectrogramStream(
sr: int,
*,
n_fft: int = 512,
hop_length: Optional[int] = None,
window: str = "hann",
seconds: float = 3.0,
top_db: float = 80.0,
ref: float = 1.0,
n_mels: int = 0,
fmin: float = 0.0,
fmax: Optional[float] = None,
)
A rolling log-magnitude spectrogram, one column per hop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sr
|
int
|
sample rate of the audio that will be pushed. |
required |
n_fft
|
int
|
analysis frame, in samples. Also the latency. |
512
|
hop_length
|
Optional[int]
|
samples between columns; defaults to |
None
|
window
|
str
|
any window name |
'hann'
|
seconds
|
float
|
how much history to keep on screen. Rounded to a whole number of columns, at least one. |
3.0
|
top_db
|
float
|
dynamic range. Anything quieter than top_db below the reference is clamped to the floor, so an empty room draws as one flat colour instead of amplified dither. |
80.0
|
ref
|
float
|
amplitude that reads 0 dB. |
1.0
|
n_mels
|
int
|
0 for a linear frequency axis, or a band count for a mel one. Speech at 16 kHz spends most of its energy in the bottom eighth of a linear axis; 64 mel bands spend about a third of the picture below 1 kHz. |
0
|
fmin
|
float
|
lowest mel band, ignored when n_mels is 0. |
0.0
|
fmax
|
Optional[float]
|
highest mel band; Nyquist when omitted. |
None
|
Methods:
| Name | Description |
|---|---|
to_stft |
The equivalent |
reset |
Empty the window and the part-filled frame, keeping the settings. |
push |
Feed newly-arrived audio in; get the columns it completed back. |
columns |
The window as an image: |
latest |
The most recent column, floor-filled if there is not one yet. |
Attributes:
| Name | Type | Description |
|---|---|---|
n_bins |
int
|
Rows in a column: mel bands, or linear frequency bins. |
freqs |
ndarray
|
Centre frequency of each row, in Hz. Mel bands included. |
floor_db |
float
|
What an empty column holds, and the clamp applied to every other. |
latency_seconds |
float
|
How long before the first column exists: one analysis frame. |
column_seconds |
float
|
Time between columns. |
window_seconds |
float
|
Span of |
frames_seen |
int
|
Columns computed since the last |
filled |
int
|
Columns of real audio in the window. Below |
Source code in kudio/core/spectrogram.py
floor_db
property
¶
What an empty column holds, and the clamp applied to every other.
latency_seconds
property
¶
How long before the first column exists: one analysis frame.
frames_seen
property
¶
Columns computed since the last reset, including the ones
that have already scrolled off.
filled
property
¶
Columns of real audio in the window. Below n_columns only
while the display is still filling up.
to_stft
¶
to_stft() -> STFT
The equivalent kudio.STFT geometry.
Useful for recording what a live view was computed with, or for reproducing a moment from the file afterwards -- with the caveat at the top of this module: the offline transform centres its frames and this does not.
Source code in kudio/core/spectrogram.py
reset
¶
Empty the window and the part-filled frame, keeping the settings.
Source code in kudio/core/spectrogram.py
push
¶
Feed newly-arrived audio in; get the columns it completed back.
Blocks may be any size, including zero: samples that do not fill a frame are held until the rest of it arrives, so the answer never depends on how the device chose to chop the stream up.
Feed this kudio.StreamRecorder.drain, not
tail. The tail overlaps between calls,
and every overlapping sample would be drawn twice, at the wrong time.
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in kudio/core/spectrogram.py
columns
¶
The window as an image: (n_columns, bins), newest on the right.
Always the full width, floor-filled on the left until enough audio has arrived. A display that grows from nothing rescales its own time axis on every frame, which reads as the picture jittering rather than as the recording starting.
Source code in kudio/core/spectrogram.py
latest
¶
The most recent column, floor-filled if there is not one yet.
A single column is a spectrum: this is what an instantaneous frequency readout draws.