B.Tech ENTC (5th semester) · Model-Based Design course project
Single-channel speech noise suppression with spectral subtraction (Boll, 1979), including Berouti's over-subtraction factor α and spectral floor β.
The assessed deliverable is the Simulink model in
simulink/. NoiseLab, the web app in this repo, re-implements the same algorithm in Python: same window, frame size, overlap, subtraction rule and noisy-phase reconstruction. It is for interactive exploration, live recording and comparison with Simulink. It does not replace the model.
| Part | What it is | Testing status |
|---|---|---|
simulink/ |
Block table, model builder, run/export scripts, plain-MATLAB reference | UNTESTED in MATLAB (written without MATLAB access). The block design is verified by a Python emulation (tests/test_simulink_chain.py): it matches the Python pipeline to 3 × 10⁻¹⁶ |
app/dsp/ |
Hand-written DSP in NumPy/SciPy: STFT, noise estimation, subtraction, metrics, test signals | Unit-tested (pytest) |
app/ |
Flask API + browser UI (vanilla JS, Plotly, KaTeX, all vendored) | API tests + headless-browser checks |
scripts/compare_simulink_python.py |
Aligns and compares Simulink output with Python | Tested on emulated Simulink output |
docs/ |
ALGORITHM.md (math, numbered equations cited in code), VIVA.md, REPORT_OUTLINE.md |
— |
No black-box denoisers are used (noisereduce, RNNoise, DeepFilterNet, Demucs, …). Every step is in app/dsp/ and can be explained line by line.
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
flask --app app run --debug # open http://127.0.0.1:5000
pytest -q # ~140 tests, ~5 sRequires Python 3.10+ (developed on 3.14). Dependencies: Flask, NumPy, SciPy, soundfile, pytest, plus matplotlib, which only the comparison script uses. Plotly.js 2.35.2, KaTeX 0.16.11 and the Inter / JetBrains Mono fonts are vendored in app/static/vendor/ with their licences. The app works offline and on lab networks that block CDNs.
% MATLAB, repo root (see simulink/README.md first; the scripts are UNTESTED)
addpath simulink
run_and_export('samples/noisy.wav', 'out/cleaned_simulink.wav')python scripts/compare_simulink_python.py samples/noisy.wav out/cleaned_simulink.wavThe script prints the alignment lag, RMS error, correlation and the SNR between the two outputs (> 40 dB means "MATCH"), and saves a comparison figure. samples/README.md shows how to generate samples/noisy.wav; no audio is committed.
Layout, desktop (≥ 1280 px): left = source, demo mode, parameters; centre = player and plots; right = metrics and noise spectrum. It collapses to two columns, then one on narrow screens.
- Load audio. Drag and drop or pick a WAV/FLAC/OGG file (MP3 if your libsndfile supports it; see
/api/health). Limits: 25 MB, 120 s. The browser draws the waveform immediately, then the server processes the file. Formats the server can't read but the browser can (e.g. M4A, WebM) are decoded in the browser and uploaded as 16-bit WAV. - Or record. Record (
R) shows a level meter, timer and live spectrum, and the clip is processed when you stop. Leave about 1 s of silence first: the default noise estimate uses the first 0.5 s. Capture noise profile (2 s) records room noise only and switches to the "Noise profile clip" mode. - Or run a demo. Choose a noise type (white, pink, 50 Hz mains hum, fan/AC, babble-like, clicks) and an input SNR from −5 to 20 dB. The clean reference is known, so true SNR, SI-SDR and segmental SNR improvements are shown, and "Clean ref" becomes playable. You can also supply your own clean WAV.
- Tune. Every slider reprocesses after 250 ms, reusing the cached STFT (about 50 ms for a 30 s clip). Match Simulink basic sets the exact Simulink parameters. Other presets: Faithful (light), Balanced, Aggressive, Mains hum 50 Hz, Fan/AC. Options marked extended (smoothing, Wiener gain, min-statistics) are not in the basic Simulink model.
- Listen. The A/B player switches Original / Cleaned / Removed (/ Clean ref) on one shared playhead (
Spaceplay,Aswitch). - Inspect. Plots: waveform; noisy/cleaned/removed spectrograms on one colour scale; frame energy with VAD shading; gain mask G(k, m); noise estimate with mean spectra. Click any time plot to open the STFT frame inspector, which shows that frame's |Y|, the subtracted level α|N̂|, the floor and the result |S̃|.
,and.step frames. Drag to zoom (all time plots stay in sync); double-click to reset. - Live denoise (preview). Hear the basic algorithm in real time on your microphone, through an AudioWorklet with ≈ N samples of algorithmic latency (10.7 ms at N = 512, 48 kHz). The Denoised/Bypass toggle gives a direct A/B. α, β and p apply instantly. Use headphones. The noise estimate comes from your recorded profile, or is learned from the first T s (stay quiet). The live path implements only the basic algorithm (Hann, 50 %, subtraction rule); its JS core is parity-tested against Python (< 10⁻⁹).
- Parameter sweep (demo mode). A heatmap of ground-truth ΔSI-SDR or ΔSNR over 11 α × 7 β values, with every other parameter as currently set. ★ marks the best cell and an outline marks the current one. Click a cell to apply it.
- Export. The ZIP contains the noisy, cleaned and removed-noise WAVs (and the clean reference in demo mode),
params.json,metrics.jsonand every plot as a titled PNG. - Method. The header Method dialog gives the equations (KaTeX), what α and β do, limitations and references.
?lists all shortcuts.
Honest metrics. SNR, SI-SDR and segmental SNR need the clean signal, so for uploads and recordings they show "n/a (no clean reference)". They are never estimated. Reference-free indicators are shown instead: noise reduction over VAD-detected noise frames, and a rough VAD-based SNR estimate.
Browsers allow microphone access only in a secure context:
- ✅
http://127.0.0.1:5000/http://localhost:5000on the same machine - ❌
http://192.168.x.x:5000from another device. The Record buttons are disabled and the app says why. Use HTTPS (e.g. a reverse proxy with a certificate) for remote recording.
The app asks the browser to turn off its own noise suppression, echo cancellation and auto-gain while recording, so the browser doesn't denoise the signal first; some browsers or OS audio stacks may ignore this. Recordings pass through MediaRecorder (usually Opus, which is lossy). For exact comparisons with Simulink, use uploaded WAV files.
SI-SDR improvement in dB at 5 dB input SNR, mean of 5 seeds (6 s synthetic voiced signal; app/dsp/synth.py). Higher is better; bold = best per row. Measured with the pipeline in this repo.
| Noise (5 dB input SNR) | Defaults (p=2) | Match Simulink basic | Balanced | Aggressive | Mains hum 50 Hz | Fan / AC |
|---|---|---|---|---|---|---|
| white | +6.1 | +11.3 | +8.6 | +10.6 | +9.0 | +11.0 |
| pink | +5.3 | +8.0 | +6.3 | +7.3 | +8.0 | +8.7 |
| hum50 | +7.6 | +5.2 | +6.6 | +5.7 | +12.3 | +9.7 |
| fan | +3.9 | +4.2 | +4.5 | +4.4 | +7.1 | +7.1 |
| babble | +4.8 | +9.7 | +7.3 | +9.3 | +7.6 | +9.1 |
| clicks | +0.7 | +2.1 | +0.8 | +1.4 | +4.6 | +3.1 |
How to read this:
- Magnitude subtraction (p = 1, the Simulink preset) with α = 2 is more aggressive than power subtraction (p = 2) with α = 2. That is why it scores higher on broadband noise.
- Long frames (N = 2048, the hum preset) resolve the 50 Hz harmonic lines, which is why that preset wins on hum and on narrow-band noise.
- Clicks are impulsive and non-stationary. Spectral subtraction assumes stationary noise, so it barely helps there. This is an expected limitation (see
docs/VIVA.md). - SI-SDR does not measure musical noise. Higher α / lower β often scores well but sounds "watery". Listen with the A/B player.
- The synthetic "babble" is far more stationary than real babble. Real multi-talker babble is much harder.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
Versions, supported formats (MP3 detected at startup), limits |
| GET | /api/presets |
Presets + parameter schema (ranges, defaults) |
| POST | /api/process |
multipart audio + params (JSON) → job_id, sr, duration, metrics, viz, audio URLs |
| POST | /api/reprocess/<job_id> |
{"params": {...}}; reuses the cached STFT |
| POST | /api/noise-profile |
multipart audio (noise only) → profile_id for noise_mode=profile |
| POST | /api/demo |
{noise_type, snr_db, duration, seed, params} (or multipart with a clean file) → as process + ground truth |
| GET | /api/frame/<job_id>?t= |
STFT frame inspector data |
| POST | /api/sweep/<job_id> |
α × β grid of ground-truth improvements (demo jobs only) |
| GET | /api/noise-profile/<id>/spectrum?sr=&n_fft= |
Profile |N̂| for the live preview |
| GET | /api/audio/<job_id>/<original|cleaned|noise|clean>.wav |
noise = removed component y − ŝ; clean = demo only |
| GET/POST | /api/export/<job_id> |
ZIP (POST adds plot PNGs) |
Parameters are validated against the table in app/params.py. Out-of-range values are rejected with a 400, not silently clamped. Uploads are identified by content, not file name or MIME type. Jobs live in memory plus a per-job temp folder with random IDs, and are deleted after 30 minutes idle.
app/
__init__.py create_app()
config.py limits, paths
params.py parameter schema, validation, presets
routes.py page + /api/*
dsp/ pure NumPy/SciPy, no Flask: stft, noise_estimation, spectral_subtraction,
metrics, synth, viz_data
services/ audio_io (safe load/save), jobs (LRU+TTL cache), processing (run + metrics + viz)
templates/index.html the UI
static/{css,js,vendor} vanilla ES modules, no build step
simulink/ model builder, run/export, helpers, reference loop, README (block table)
scripts/compare_simulink_python.py
tests/ pytest: DSP, params, API, Simulink-chain emulation, JS helpers (via Node, optional)
docs/ ALGORITHM.md, VIVA.md, REPORT_OUTLINE.md
samples/ no audio committed; see samples/README.md
pytest -qruns everything.tests/test_js_helpers.py(browser WAV encoder/decoders) andtests/test_js_parity.py(live-preview DSP core vs Python) run under Node, and are skipped if Node isn't installed.- DSP functions are pure and typed, and their comments cite equation numbers in
docs/ALGORITHM.md. - The UI was checked in headless Chrome at 1366×768, 1920×1080 and 390 px wide (upload, demo, sliders, player, recording and live preview with a fake microphone, sweep, export, themes). The AudioWorklet was also rendered offline and compared with Python (93.8 dB SNR between outputs). Real microphone input and audible playback were not testable headlessly.
- Minimum statistics (
minstats) is bias-compensated (Eq. 6). A raw 10th percentile underestimates the noise by 8.7 dB, and correcting it raised the mean demo SI-SDR gain from +1.5 to +4.0 dB.
- S. F. Boll, "Suppression of acoustic noise in speech using spectral subtraction," IEEE Trans. ASSP, 27(2), 113–120, 1979. doi:10.1109/TASSP.1979.1163209
- M. Berouti, R. Schwartz, J. Makhoul, "Enhancement of speech corrupted by acoustic noise," Proc. ICASSP, 208–211, 1979. doi:10.1109/ICASSP.1979.1170788
- J. Le Roux, S. Wisdom, H. Erdogan, J. R. Hershey, "SDR — half-baked or well done?," Proc. ICASSP, 2019.
- R. Martin, "Noise power spectral density estimation based on optimal smoothing and minimum statistics," IEEE Trans. Speech Audio Process., 9(5), 2001 (background for the simplified
minstatsmode).