A dependency-free C++17 port of noisereduce, the spectral gating noise reduction library for audio. Optional CMake flags enable KissFFT, xsimd/Eigen and multithreading for extra speed (see Optional acceleration).
Birdsong-like chirps in heavy broadband noise (top), denoised with the
default non-stationary settings — no noise clip needed (bottom). The
spectrograms share the same dB color scale. Regenerate with
tools/generate_readme_figure.py.
Noise reduction works by computing a spectrogram of the signal, estimating a noise threshold for each frequency band, and gating out everything below it:
- Non-stationary (default): the threshold tracks the signal over time, so the algorithm adapts to a changing noise floor and needs no noise recording.
- Stationary: the threshold is estimated once, either from a separate noise recording or from the signal itself.
The port is numerically faithful: the test suite compares its output against
reference data generated by the Python library and requires agreement within
1e-9 (see Fidelity).
#include <noisereduce/noisereduce.hpp>
std::vector<double> samples = /* mono audio, any sample rate */;
// Non-stationary (default), like nr.reduce_noise(y=samples, sr=48000)
std::vector<double> denoised = noisereduce::reduce_noise(samples, 48000.0);
// Stationary, with a noise-only recording and custom parameters
noisereduce::Parameters params;
params.stationary = true;
params.prop_decrease = 0.9;
std::vector<double> denoised2 =
noisereduce::reduce_noise(samples, noise_clip, 48000.0, params);
// Multi-channel (planar): channels are denoised independently
noisereduce::MultiChannel stereo = {left, right};
noisereduce::MultiChannel out = noisereduce::reduce_noise(stereo, 48000.0);Invalid parameters throw std::invalid_argument.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build # run the testsThe default build is dependency-free. For more speed, each of these CMake
flags (all OFF by default) swaps in an optimized implementation; any
combination still passes the full test suite, including the Python reference
comparisons:
| Flag | Effect | Dependency |
|---|---|---|
NOISEREDUCE_WITH_KISSFFT |
FFTs via KissFFT (double precision) | find_package, else fetched automatically |
NOISEREDUCE_WITH_XSIMD |
SIMD-vectorized spectral kernels (magnitude, dB, sigmoid mask) | find_package, else fetched automatically |
NOISEREDUCE_WITH_EIGEN |
Same kernels vectorized with Eigen instead | requires installed Eigen ≥ 3.3 |
NOISEREDUCE_WITH_STL_PARALLEL |
Channels and chunks processed concurrently with std::async |
none (C++ standard library threads) |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DNOISEREDUCE_WITH_KISSFFT=ON \
-DNOISEREDUCE_WITH_XSIMD=ON \
-DNOISEREDUCE_WITH_STL_PARALLEL=ONNotes:
- If both
XSIMDandEIGENare enabled, xsimd takes precedence for the kernels. n_fftmust be a power of two in every configuration (kept for consistent behavior across builds, even though KissFFT would allow other sizes).- Ballpark on an Apple M-series laptop, 2 minutes of stereo 44.1 kHz audio: ~1.8 s with the default build, ~0.4 s with KissFFT + xsimd + STL parallel.
- Installed accelerated builds record their dependencies in the exported CMake
config (
find_dependency), so consumers need those packages discoverable; the dependency-free default has no such requirement.
As a subdirectory (vendored or via FetchContent):
add_subdirectory(noisereduce-cpp)
target_link_libraries(your_target PRIVATE noisereduce::noisereduce)Or install it (cmake --install build) and use:
find_package(noisereduce REQUIRED)
target_link_libraries(your_target PRIVATE noisereduce::noisereduce)examples/denoise_wav.cpp denoises a WAV file (16-bit PCM or 32-bit float):
./build/examples/denoise_wav noisy.wav clean.wav
./build/examples/denoise_wav noisy.wav clean.wav --noise noise_only.wav
./build/examples/denoise_wav noisy.wav clean.wav --stationary --prop 0.8noisereduce::Parameters mirrors the keyword arguments of the Python
reduce_noise function, with the same defaults:
| Field | Default | Description |
|---|---|---|
stationary |
false |
Stationary instead of non-stationary gating |
prop_decrease |
1.0 |
Proportion of noise to remove (1.0 = 100%) |
time_constant_s |
2.0 |
Noise-floor smoothing time constant (non-stationary) |
freq_mask_smooth_hz |
500.0 |
Mask smoothing range across frequency; std::nullopt disables |
time_mask_smooth_ms |
50.0 |
Mask smoothing range across time; std::nullopt disables |
thresh_n_mult_nonstationary |
2.0 |
Threshold multiplier over the noise floor (non-stationary) |
sigmoid_slope_nonstationary |
10.0 |
Slope of the sigmoid mask (non-stationary) |
n_std_thresh_stationary |
1.5 |
Std deviations above the mean noise level (stationary) |
chunk_size |
600000 |
Samples per processing chunk; 0 disables chunking |
padding |
30000 |
Zero-padding around each chunk |
n_fft |
1024 |
FFT size (must be a power of two) |
win_length |
n_fft |
STFT window length (0 selects the default) |
hop_length |
win_length / 4 |
STFT hop (0 selects the default) |
clip_noise_stationary |
true |
Limit the noise recording to chunk_size samples |
A tip carried over from the Python library: the default mask smoothing
(freq_mask_smooth_hz = 500) deliberately smears the mask across neighboring
frequency bands, which strongly attenuates very narrow-band content such as
pure test tones. For such signals, reduce or disable freq_mask_smooth_hz.
The STFT/ISTFT follows the scipy.signal.stft/istft conventions used by
noisereduce (periodic Hann window, zero boundary extension, 1/sum(window)
spectrum scaling), and the gating math replicates
SpectralGateStationary/SpectralGateNonStationary step for step, including
chunked processing. tests/data/ contains reference outputs generated by the
Python library (tools/generate_reference.py); tests/test_reference.cpp
checks both algorithms, custom parameters, chunked processing and stereo input
against them with a 1e-9 absolute tolerance.
Intentional differences:
n_fftmust be a power of two (the built-in FFT is radix-2; the Python docs recommend powers of two anyway).- Where the Python library produces NaN on 0/0 (a frequency band that is zero for an entire chunk in the non-stationary algorithm), this port gates the band instead.
- Processing is single-threaded (
n_jobs/use_torchhave no equivalent); theuse_tqdm/tmp_folderoptions do not apply.
To regenerate the reference data:
pip install noisereduce
python tools/generate_reference.pyThis is an offline/batch library, like the Python original — it is not
suitable for real-time processing. The algorithm itself is non-causal:
the non-stationary noise floor is smoothed forward and backward in time
(filtfilt), the dB conversion clips against a per-frequency maximum taken
over the whole spectrogram, and the mask is smoothed across future frames.
The implementation reflects that: it takes whole buffers, pads chunks with
30,000 samples, and allocates freely — none of which belongs on an audio
callback.
A causal, streaming variant of the stationary mode (precomputed noise
threshold, one-sided mask smoothing, fixed dB reference) is feasible but would
produce different output from the Python library and still carry latency of
roughly n_fft plus the smoothing lookahead. For genuinely real-time
denoising, consider purpose-built causal solutions such as RNNoise or the
WebRTC audio processing module.
MIT, like the original library. See LICENSE; algorithm by Tim Sainburg.
