DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
SpectralDenoiser.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
37#include "../Core/AudioBuffer.h"
38#include "../Core/AudioSpec.h"
39#include "../Core/DspMath.h"
40#include "../Core/SpectralProcessor.h"
41#include "../Core/StateBlob.h"
42
43#include <algorithm>
44#include <atomic>
45#include <cmath>
46#include <cstddef>
47#include <cstdint>
48#include <vector>
49
50namespace dspark {
51
58template <FloatType T>
60{
61public:
62 // -- Lifecycle ---------------------------------------------------------------
63
75 void prepare(const AudioSpec& spec, int fftSize = 2048)
76 {
77 if (!spec.isValid()) return;
78 prepared_.store(false, std::memory_order_relaxed);
79 numChannels_ = spec.numChannels;
80 stft_.prepare(spec, fftSize, fftSize / 4);
81 numBins_ = stft_.getNumBins();
82
83 profile_.assign(static_cast<size_t>(numBins_), 0.0f);
84 gains_.assign(static_cast<size_t>(numChannels_),
85 std::vector<float>(static_cast<size_t>(numBins_), 1.0f));
86 smooth_.assign(static_cast<size_t>(numBins_), 1.0f);
87
88 prepared_.store(true, std::memory_order_relaxed);
89 reset();
90 }
91
93 void reset() noexcept
94 {
95 if (!prepared_.load(std::memory_order_relaxed)) return;
96 stft_.reset();
97 for (auto& g : gains_)
98 std::fill(g.begin(), g.end(), 1.0f);
99 callCounter_ = 0;
100 }
101
103 void clearProfile() noexcept
104 {
105 std::fill(profile_.begin(), profile_.end(), 0.0f);
106 }
107
108 // -- Parameters (thread-safe) ---------------------------------------------------
109
111 void setLearning(bool learning) noexcept
112 {
113 learning_.store(learning, std::memory_order_relaxed);
114 }
115
118 void setReduction(T db) noexcept
119 {
120 if (!std::isfinite(db)) return;
121 reduction_.store(std::clamp(db, T(0), T(40)), std::memory_order_relaxed);
122 }
123
126 void setThreshold(T factor) noexcept
127 {
128 if (!std::isfinite(factor)) return;
129 threshold_.store(std::clamp(factor, T(1), T(8)), std::memory_order_relaxed);
130 }
131
132 [[nodiscard]] bool getLearning() const noexcept { return learning_.load(std::memory_order_relaxed); }
133 [[nodiscard]] T getReduction() const noexcept { return reduction_.load(std::memory_order_relaxed); }
134 [[nodiscard]] T getThreshold() const noexcept { return threshold_.load(std::memory_order_relaxed); }
135
137 [[nodiscard]] int getLatency() const noexcept { return stft_.getLatency(); }
138
141 [[nodiscard]] std::vector<uint8_t> getState() const
142 {
143 StateWriter w(stateId("DNSE"), 1);
144 // Explicit float casts: the blob stores float, and with T = double the
145 // unqualified write(key, double) would be ambiguous (float/int32/bool).
146 w.write("reduction", static_cast<float>(reduction_.load(std::memory_order_relaxed)));
147 w.write("threshold", static_cast<float>(threshold_.load(std::memory_order_relaxed)));
148 return w.blob();
149 }
150
152 bool setState(const uint8_t* data, size_t size)
153 {
154 StateReader r(data, size);
155 if (!r.isValid() || r.processorId() != stateId("DNSE")) return false;
156 setReduction(static_cast<T>(r.read("reduction", 18.0f)));
157 setThreshold(static_cast<T>(r.read("threshold", 2.0f)));
158 return true;
159 }
160
161 // -- Processing -------------------------------------------------------------------
162
164 void processBlock(AudioBufferView<T> buffer) noexcept
165 {
166 if (!prepared_.load(std::memory_order_relaxed)) return;
167
168 const bool learning = learning_.load(std::memory_order_relaxed);
169 const float floorGain = std::pow(
170 10.0f, -static_cast<float>(reduction_.load(std::memory_order_relaxed)) / 20.0f);
171 const float thresh = static_cast<float>(threshold_.load(std::memory_order_relaxed));
172
173 // The STFT invokes the callback once per PROCESSED channel per hop
174 // (in channel order) - and it processes min(buffer, prepared)
175 // channels. Modulo by that same effective count, reset per block, or
176 // a narrow buffer over a wider spec would rotate the per-channel
177 // gain memories between hops (channel 0 alternating onto channel 1's
178 // release state - measured 0.004 divergence versus a mono-prepared
179 // twin before this fix).
180 const int nChEff = std::max(1, std::min(buffer.getNumChannels(), numChannels_));
181 callCounter_ = 0;
182
183 stft_.processBlock(buffer, [this, learning, floorGain, thresh, nChEff](T* bins, int numBins)
184 {
185 auto& chGain = gains_[static_cast<size_t>(callCounter_ % nChEff)];
186 ++callCounter_;
187
188 // Per-bin raw gate decision.
189 for (int k = 0; k < numBins; ++k)
190 {
191 const float re = static_cast<float>(bins[2 * k]);
192 const float im = static_cast<float>(bins[2 * k + 1]);
193 const float mag = std::sqrt(re * re + im * im);
194
195 if (learning)
196 {
197 // Profile: peak-hold with a gentle average pull-up.
198 auto& p = profile_[static_cast<size_t>(k)];
199 p = std::max(p * 0.995f + mag * 0.005f, std::max(p, mag * 0.8f));
200 }
201
202 const float open = profile_[static_cast<size_t>(k)] * thresh;
203 smooth_[static_cast<size_t>(k)] = (mag > open) ? 1.0f : floorGain;
204 }
205
206 // Frequency smoothing (3-bin average) defends against isolated
207 // flickering bins - the source of musical noise.
208 for (int k = 0; k < numBins; ++k)
209 {
210 const float a = smooth_[static_cast<size_t>(std::max(k - 1, 0))];
211 const float b = smooth_[static_cast<size_t>(k)];
212 const float c = smooth_[static_cast<size_t>(std::min(k + 1, numBins - 1))];
213 float target = (a + b + c) * (1.0f / 3.0f);
214
215 // Temporal smoothing: instant attack (open fast), slow release.
216 auto& g = chGain[static_cast<size_t>(k)];
217 g = (target > g) ? target : (g * 0.85f + target * 0.15f);
218
219 bins[2 * k] = static_cast<T>(static_cast<float>(bins[2 * k]) * g);
220 bins[2 * k + 1] = static_cast<T>(static_cast<float>(bins[2 * k + 1]) * g);
221 }
222 });
223 }
224
225private:
227 int numChannels_ = 0;
228 int numBins_ = 0;
229 std::atomic<bool> prepared_ { false };
230
231 std::vector<float> profile_;
232 std::vector<std::vector<float>> gains_;
233 std::vector<float> smooth_;
234 int callCounter_ = 0;
235
236 std::atomic<bool> learning_ { false };
237 std::atomic<T> reduction_ { T(18) };
238 std::atomic<T> threshold_ { T(2) };
239};
240
241} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Learn-a-profile spectral gate (hiss/hum/room-tone reduction).
void setThreshold(T factor) noexcept
Gate threshold over the learned profile [1, 8] (default 2). Non-finite values are ignored.
void setReduction(T db) noexcept
Maximum attenuation of gated bins in dB [0, 40] (default 18). Non-finite values are ignored.
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
T getReduction() const noexcept
T getThreshold() const noexcept
std::vector< uint8_t > getState() const
Serializes the parameter state (the learned profile is material- dependent content,...
void processBlock(AudioBufferView< T > buffer) noexcept
Processes a block in-place. Pass-through until prepare() succeeds.
void clearProfile() noexcept
Forgets the learned noise profile (stream-owner thread).
int getLatency() const noexcept
Latency in samples (the STFT pipeline's).
bool getLearning() const noexcept
void prepare(const AudioSpec &spec, int fftSize=2048)
Prepares the STFT pipeline and the per-channel bin state.
void reset() noexcept
Clears signal state and per-bin gain memories (keeps profile).
void setLearning(bool learning) noexcept
While true, incoming audio trains the noise profile.
High-performance STFT analysis-modification-synthesis pipeline.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56