Loading…
Loading…
AI disclosure: This project was built with AI assistance (Claude, by Anthropic) for writing code and documentation. All creative and technical decisions (model design, training data, evaluation) are mine, and I reviewed everything before publishing.
A causal, real-time vocal denoiser for tracking vocals in a DAW: 48 kHz, 10 ms algorithmic latency, 1.25M parameters, 0.19 ms of CPU per 5 ms block. It targets the noise a home studio actually has: room tone, hiss, mains hum and headphone bleed, and it is built to leave clean takes alone.
Code, training recipe and full write-up: GitHub: Daw-Vocal-Denoiser
Trained on: about 39 minutes of my own vocal takes, the VocalSet singing dataset (CC BY 4.0) and DNS Challenge clean speech + noise, with simulated headphone bleed from my own instrumentals. Details below in Training data.
Held-out singing never used for training or model selection (a held-out song of mine plus held-out VocalSet singers), mixed with dev-split noise at 0-40 dB SNR, run through the streaming engine. 200 items.
| Metric | Noisy input | Enhanced | Change |
|---|---|---|---|
| SI-SDR (dB) | 19.04 | 22.95 | +3.91 |
| PESQ (WB, at 16 kHz) | 2.37 | 3.05 | +0.68 |
| Clean pass-through SI-SDR (dB) | 45.6 | clean takes are left essentially untouched |
| Real-time property | Value |
|---|---|
| Sample rate | 48 kHz mono |
| Algorithmic latency | 10 ms (32 ms analysis / 10 ms synthesis windows, 5 ms hop) |
| CPU per 5 ms block (ONNX Runtime, desktop CPU) | 0.19 ms (real-time factor 0.038) |
| Streaming vs offline parity | 111.9 dB |
| File | What it is |
|---|---|
daw_denoiser.onnx |
One-frame streaming model: magnitude [1, 1, 769] + GRU state [2, 1, 256] in, mask + new state out |
daw_denoiser.pth |
PyTorch checkpoint (EMA weights, step 77,000, dev loss 0.338) |
model.py |
Model definition and asymmetric-window STFT |
stream.py |
Real-time block engine (240-sample blocks in and out) |
# pip install onnxruntime numpy
from stream import StreamEngine # expects daw_denoiser.onnx next to it
eng = StreamEngine()
y_block = eng.process(x_block) # float32[240] at 48 kHz -> 240 denoised samples, 10 ms later
from stream import process_file # offline: whole mono 48 kHz array in, denoised array out
y = process_file(x)
Asymmetric analysis/synthesis windows (Wang et al. 2021) give long-window frequency resolution (31 Hz bins, enough for low sung harmonics) at short-window latency. Per 5 ms frame: compressed magnitude -> Linear -> LayerNorm -> 2-layer GRU (256) -> sigmoid magnitude mask. The noisy phase is kept.
Training: 108,773 steps (batch 32, 4 s segments) on an RTX 4060; best dev checkpoint at step 77,000. Compressed spectral loss (Braun & Tashev) + multi-resolution STFT; AdamW, warmup + cosine, EMA 0.999.
Clean targets are drawn 35% own vocals / 35% VocalSet / 30% DNS speech; every example is mixed on the fly with noise.
| Source | Amount (after trimming silence) | Split | License |
|---|---|---|---|
| My own vocal takes: individual takes from my recording sessions (not released) | 38.8 min train (54 takes, 8 songs) | By song: Mad = dev (5.2 min), Say Dat Name = test (10.3 min) | Mine |
| VocalSet (Wilkins et al., ISMIR 2018): 20 singers, many techniques | about 10 h before trimming | By singer: female8 + male10 dev, female9 + male11 test | CC BY 4.0 |
| DNS Challenge fullband clean speech (subset) | 13,733 clips, about 9.6 h | 5% of files held out for dev | Per source, see the DNS repo |
| DNS Challenge fullband noise (subset) | 8,000 clips, about 20.5 h | 5% of files held out for dev | Per source, see the DNS repo |
| Headphone bleed: my own instrumentals, high-passed | 5 songs | Mine | |
| Synthetic: room impulse responses, random EQ, white/pink hiss, 50/60 Hz hum | generated per example |
Mixing: SNR 0 to 40 dB, 10% clean-only examples (so clean takes pass through untouched), 30% get a synthetic room applied to input and target alike. 48 kHz mono throughout. No commercial sample packs (songs using Cymatics samples were left out of the bleed set).