DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Phaser.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
38#include "../Core/Oscillator.h"
39#include "../Core/DryWetMixer.h"
40#include "../Core/AudioSpec.h"
41#include "../Core/AudioBuffer.h"
42#include "../Core/DspMath.h" // fastExp, fastTan, fastTanh
43#include "../Core/StateBlob.h"
44
45#include <algorithm>
46#include <array>
47#include <atomic>
48#include <cmath>
49#include <cstddef>
50#include <cstdint>
51#include <numbers>
52#include <vector>
53
54namespace dspark {
55
65template <FloatType T>
66class Phaser
67{
68public:
69 ~Phaser() = default; // non-virtual: leaf class (no virtual dispatch)
70
71 static constexpr int kMaxStages = 12;
72
73 // -- Lifecycle --------------------------------------------------------------
74
83 void prepare(const AudioSpec& spec)
84 {
85 if (!spec.isValid()) return; // release-safe: keep previous state
86
87 spec_ = spec;
88 mixer_.prepare(spec);
89
90 lfo_.prepare(spec.sampleRate);
91 lfo_.setFrequency(rate_.load(std::memory_order_relaxed));
92 lfo_.setWaveform(lfoWaveform_.load(std::memory_order_relaxed));
93
94 lfoR_.prepare(spec.sampleRate);
95 lfoR_.setFrequency(rate_.load(std::memory_order_relaxed));
96 lfoR_.setWaveform(lfoWaveform_.load(std::memory_order_relaxed));
97
98 // One-pole parameter smoothing at a 20 Hz corner (~8 ms time constant)
99 smoothCoef_ = T(1) - std::exp(static_cast<T>(-2.0 * std::numbers::pi * 20.0) / static_cast<T>(spec_.sampleRate));
100
101 prepared_ = true;
102 reset();
103 }
104
109 void processBlock(AudioBufferView<T> buffer) noexcept
110 {
111 if (!prepared_) return; // without prepare(), fsInv below would be 1/0 -> NaN chain
112
113 const int nCh = std::min(buffer.getNumChannels(), kMaxChannels);
114 const int nS = buffer.getNumSamples();
115
116 // Front-door non-finite guard (M-006 C1): the allpass recursion
117 // (yPrev/xPrev) latches a NaN/Inf input permanently -- even with feedback
118 // off -- so a single glitch would silence the effect forever. Replace
119 // non-finite input with 0 before it reaches the dry snapshot or any
120 // allpass/feedback state. No-op on finite input (metrics byte-identical).
121 for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
122 {
123 T* d = buffer.getChannel(ch);
124 for (int i = 0; i < nS; ++i)
125 if (!std::isfinite(d[i])) d[i] = T(0);
126 }
127
128 // Target parameters loaded once per block
129 const T targetDepth = depth_.load(std::memory_order_relaxed);
130 const T targetMix = mix_.load(std::memory_order_relaxed);
131 const T targetFb = feedback_.load(std::memory_order_relaxed);
132 const int stagesVal = numStages_.load(std::memory_order_relaxed);
133 const T minF = minFreq_.load(std::memory_order_relaxed);
134 const T maxF = maxFreq_.load(std::memory_order_relaxed);
135 const T spreadVal = stereoSpread_.load(std::memory_order_relaxed);
136
137 // Apply LFO rate/waveform on the AUDIO thread (the setters only publish
138 // atomics) so the non-atomic Oscillator state is never written concurrently.
139 lfo_.setFrequency(rate_.load(std::memory_order_relaxed));
140 lfo_.setWaveform(lfoWaveform_.load(std::memory_order_relaxed));
141 lfoR_.setFrequency(rate_.load(std::memory_order_relaxed));
142 lfoR_.setWaveform(lfoWaveform_.load(std::memory_order_relaxed));
143
144 // Re-phase the right-channel LFO when the spread changes (audio thread
145 // only; Oscillator state is not thread-safe).
146 if (std::abs(spreadVal - lastSpread_) > T(0.0001))
147 {
148 lastSpread_ = spreadVal;
149 T ph = lfo_.getPhase() + spreadVal * T(0.5);
150 ph -= std::floor(ph);
151 lfoR_.setPhase(ph);
152 }
153 const bool useSpread = (spreadVal > T(0.0001)) && (nCh >= 2);
154
155 // Stages activated by a live increase start from cleared state: their
156 // history is from whenever they were last active (arbitrarily old),
157 // and replaying it injects an unrelated step into the chain.
158 if (stagesVal > lastStages_)
159 for (int s = lastStages_; s < stagesVal; ++s)
160 {
161 stages_[s].xPrev.fill(T(0));
162 stages_[s].yPrev.fill(T(0));
163 }
164 lastStages_ = stagesVal;
165
166 mixer_.pushDry(buffer);
167
168 const T logMin = std::log(minF);
169 const T logMax = std::log(maxF);
170 const T fsInv = T(1) / static_cast<T>(spec_.sampleRate);
171 const T nyquist = static_cast<T>(spec_.sampleRate) * T(0.499);
172 constexpr T kPi = static_cast<T>(std::numbers::pi);
173
174 auto coeffFromLfo = [&](T lfoVal) noexcept -> T
175 {
176 T modAmount = (lfoVal + T(1)) * T(0.5) * currentDepth_;
177 T logFreq = logMin + modAmount * (logMax - logMin);
178 // Convert log-frequency back to linear Hz (fastExp, ~2x std::exp)
179 T cutoff = std::min(fastExp(logFreq), nyquist);
180 // Bilinear-transform coefficient: tan(pi * f / Fs). fastTan is safe:
181 // `cutoff` stays below Nyquist thanks to the clamp above.
182 T tanVal = fastTan(kPi * cutoff * fsInv);
183 return (tanVal - T(1)) / (tanVal + T(1));
184 };
185
186 for (int i = 0; i < nS; ++i)
187 {
188 // Parameter smoothing (1-pole lowpass) to prevent zipper noise
189 currentDepth_ += smoothCoef_ * (targetDepth - currentDepth_);
190 currentFb_ += smoothCoef_ * (targetFb - currentFb_);
191
192 const T cL = coeffFromLfo(lfo_.getNextSample());
193 const T lfoRVal = lfoR_.getNextSample(); // keep phase advancing always
194 const T cR = useSpread ? coeffFromLfo(lfoRVal) : cL;
195
196 // Process active channels (odd channels follow the spread LFO)
197 for (int ch = 0; ch < nCh; ++ch)
198 {
199 const T c = (ch & 1) ? cR : cL;
200 T sample = buffer.getChannel(ch)[i];
201
202 // Analog-modeled feedback with soft clipping to prevent digital
203 // blowups. fastTanh is internally clamped; at typical levels
204 // the argument stays below 1, where its error is < 0.05%.
205 T fbSignal = fastTanh(fbState_[ch] * currentFb_);
206 sample += fbSignal;
207
208 // Series first-order allpass chain
209 for (int s = 0; s < stagesVal; ++s)
210 {
211 auto& st = stages_[s];
212 T y = c * sample + st.xPrev[ch] - c * st.yPrev[ch];
213 st.xPrev[ch] = sample;
214 st.yPrev[ch] = y;
215 sample = y;
216 }
217
218 fbState_[ch] = sample; // 1-sample delay for next iteration
219 buffer.getChannel(ch)[i] = sample;
220 }
221 }
222
223 // DryWetMixer smooths the mix parameter internally.
224 mixer_.mixWet(buffer, targetMix);
225 }
226
231 void reset() noexcept
232 {
233 for (auto& stage : stages_)
234 {
235 stage.xPrev.fill(T(0));
236 stage.yPrev.fill(T(0));
237 }
238 for (auto& fb : fbState_) fb = T(0);
239
240 currentDepth_ = depth_.load(std::memory_order_relaxed);
241 currentFb_ = feedback_.load(std::memory_order_relaxed);
242
243 lfo_.reset();
244 lfoR_.reset();
245 lastSpread_ = T(-1); // force re-phase of the spread LFO on next block
246 mixer_.reset();
247 }
248
249 // -- Level 1: Simple API ----------------------------------------------------
250
256 void setRate(T hz) noexcept
257 {
258 if (!std::isfinite(hz)) return;
259 rate_.store(std::clamp(hz, T(0.01), T(20)), std::memory_order_relaxed); // applied on the audio thread
260 }
261
267 void setDepth(T amount) noexcept
268 {
269 if (!std::isfinite(amount)) return;
270 depth_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
271 }
272
278 void setMix(T dryWet) noexcept
279 {
280 if (!std::isfinite(dryWet)) return;
281 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
282 }
283
284 // -- Level 2: Intermediate API ----------------------------------------------
285
290 void setStages(int count) noexcept
291 {
292 numStages_.store(std::clamp(count, 1, kMaxStages), std::memory_order_relaxed);
293 }
294
300 void setFeedback(T amount) noexcept
301 {
302 if (!std::isfinite(amount)) return;
303 feedback_.store(std::clamp(amount, T(-0.99), T(0.99)), std::memory_order_relaxed);
304 }
305
316 void setStereoSpread(T amount) noexcept
317 {
318 if (!std::isfinite(amount)) return;
319 stereoSpread_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
320 }
321
332 void setCenterFrequency(T hz) noexcept
333 {
334 if (!std::isfinite(hz) || !(hz > T(0))) return;
335 T curMin = minFreq_.load(std::memory_order_relaxed);
336 T curMax = maxFreq_.load(std::memory_order_relaxed);
337 T halfRange = std::sqrt(curMax / (curMin > T(0) ? curMin : T(1)));
338
339 const T ceiling = (spec_.sampleRate > 0)
340 ? static_cast<T>(spec_.sampleRate) * T(0.499) : T(20000);
341 const T mn = std::clamp(hz / halfRange, T(20), ceiling - T(1));
342 minFreq_.store(mn, std::memory_order_relaxed);
343 maxFreq_.store(std::clamp(hz * halfRange, mn + T(1), ceiling),
344 std::memory_order_relaxed);
345 }
346
353 void setFrequencyRange(T minHz, T maxHz) noexcept
354 {
355 if (!std::isfinite(minHz) || !std::isfinite(maxHz)) return;
356 T mn = std::max(minHz, T(20));
357 minFreq_.store(mn, std::memory_order_relaxed);
358 maxFreq_.store(std::max(maxHz, mn + T(1)), std::memory_order_relaxed);
359 }
360
361 // -- Level 3: Expert API ----------------------------------------------------
362
367 void setLfoWaveform(typename Oscillator<T>::Waveform wf) noexcept
368 {
369 const int w = std::clamp(static_cast<int>(wf), 0,
370 static_cast<int>(Oscillator<T>::Waveform::Triangle));
371 lfoWaveform_.store(static_cast<typename Oscillator<T>::Waveform>(w),
372 std::memory_order_relaxed); // applied on the audio thread
373 }
374
376 [[nodiscard]] int getStages() const noexcept { return numStages_.load(std::memory_order_relaxed); }
377
379 [[nodiscard]] T getRate() const noexcept { return rate_.load(std::memory_order_relaxed); }
380
381
383 [[nodiscard]] std::vector<uint8_t> getState() const
384 {
385 StateWriter w(stateId("PHSR"), 1);
386 w.write("rate", rate_.load(std::memory_order_relaxed));
387 w.write("depth", depth_.load(std::memory_order_relaxed));
388 w.write("mix", mix_.load(std::memory_order_relaxed));
389 w.write("feedback", feedback_.load(std::memory_order_relaxed));
390 w.write("minFreq", minFreq_.load(std::memory_order_relaxed));
391 w.write("maxFreq", maxFreq_.load(std::memory_order_relaxed));
392 w.write("stages", numStages_.load(std::memory_order_relaxed));
393 w.write("spread", stereoSpread_.load(std::memory_order_relaxed));
394 w.write("waveform",
395 static_cast<int32_t>(lfoWaveform_.load(std::memory_order_relaxed)));
396 return w.blob();
397 }
398
400 bool setState(const uint8_t* data, size_t size)
401 {
402 StateReader r(data, size);
403 if (!r.isValid() || r.processorId() != stateId("PHSR")) return false;
404 setRate(static_cast<T>(r.read("rate", 0.5f)));
405 setDepth(static_cast<T>(r.read("depth", 0.8f)));
406 setMix(static_cast<T>(r.read("mix", 0.5f)));
407 setFeedback(static_cast<T>(r.read("feedback", 0.0f)));
408 setFrequencyRange(static_cast<T>(r.read("minFreq", 200.0f)),
409 static_cast<T>(r.read("maxFreq", 6000.0f)));
410 setStages(r.read("stages", 4));
411 setStereoSpread(static_cast<T>(r.read("spread", 0.0f)));
412 setLfoWaveform(static_cast<typename Oscillator<T>::Waveform>(
413 r.read("waveform", 0)));
414 return true;
415 }
416
417protected:
418 static constexpr int kMaxChannels = 16;
419
421
422 // Parameters
423 std::atomic<T> rate_ { T(0.5) };
424 std::atomic<T> depth_ { T(0.8) };
425 std::atomic<T> mix_ { T(0.5) };
426 std::atomic<T> feedback_ { T(0) };
427 std::atomic<T> minFreq_ { T(200) };
428 std::atomic<T> maxFreq_ { T(6000) };
429 std::atomic<int> numStages_ { 4 };
430 std::atomic<typename Oscillator<T>::Waveform> lfoWaveform_ { Oscillator<T>::Waveform::Sine };
431 std::atomic<T> stereoSpread_ { T(0) };
432
433 // Smoothed state parameters
434 bool prepared_ { false };
435 T currentDepth_ { T(0.8) };
436 T currentFb_ { T(0) };
437 T smoothCoef_ { T(0.01) };
438 T lastSpread_ { T(-1) };
439 int lastStages_ { kMaxStages }; // audio-thread: clears newly activated stages
440
442 {
443 std::array<T, kMaxChannels> xPrev {};
444 std::array<T, kMaxChannels> yPrev {};
445 };
446
447 // Processing state
448 std::array<FirstOrderAllpass, kMaxStages> stages_ {};
449 std::array<T, kMaxChannels> fbState_ {};
451 Oscillator<T> lfoR_; // right-channel LFO for stereo spread
453};
454
455} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:72
Band-limited oscillator featuring PolyBLEP anti-aliasing and analog-modeled integration.
Definition Oscillator.h:64
Zero-latency, highly optimized allpass-based phaser.
Definition Phaser.h:67
std::atomic< T > rate_
Definition Phaser.h:423
void setFeedback(T amount) noexcept
Injects phase-shifted signal back into the input for resonance.
Definition Phaser.h:300
std::atomic< int > numStages_
Definition Phaser.h:429
std::atomic< T > feedback_
Definition Phaser.h:426
std::atomic< T > mix_
Definition Phaser.h:425
void setMix(T dryWet) noexcept
Balances the unprocessed and processed signal.
Definition Phaser.h:278
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Phaser.h:400
int lastStages_
Definition Phaser.h:439
~Phaser()=default
int getStages() const noexcept
Retrieves the active stage count.
Definition Phaser.h:376
static constexpr int kMaxStages
Definition Phaser.h:71
void setDepth(T amount) noexcept
Sets how wide the frequency sweep is.
Definition Phaser.h:267
void setFrequencyRange(T minHz, T maxHz) noexcept
Explicitly defines the sweep boundaries.
Definition Phaser.h:353
void setCenterFrequency(T hz) noexcept
Defines the midpoint of the logarithmic frequency sweep.
Definition Phaser.h:332
void setStages(int count) noexcept
Configures the intensity/color of the phaser via filter stages.
Definition Phaser.h:290
void reset() noexcept
Clears internal DSP state and history buffers. Call this when transport stops or playback jumps.
Definition Phaser.h:231
void setStereoSpread(T amount) noexcept
Sets the stereo phase spread of the sweep LFO.
Definition Phaser.h:316
T getRate() const noexcept
Retrieves the current sweep rate.
Definition Phaser.h:379
std::atomic< T > stereoSpread_
Definition Phaser.h:431
std::array< FirstOrderAllpass, kMaxStages > stages_
Definition Phaser.h:448
std::atomic< T > maxFreq_
Definition Phaser.h:428
std::array< T, kMaxChannels > fbState_
Definition Phaser.h:449
std::atomic< typename Oscillator< T >::Waveform > lfoWaveform_
Definition Phaser.h:430
std::atomic< T > depth_
Definition Phaser.h:424
void prepare(const AudioSpec &spec)
Prepares the phaser and allocates internal state blocks.
Definition Phaser.h:83
Oscillator< T > lfoR_
Definition Phaser.h:451
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Phaser.h:383
bool prepared_
Definition Phaser.h:434
Oscillator< T > lfo_
Definition Phaser.h:450
DryWetMixer< T > mixer_
Definition Phaser.h:452
std::atomic< T > minFreq_
Definition Phaser.h:427
void setRate(T hz) noexcept
Sets the speed of the phaser sweep.
Definition Phaser.h:256
void setLfoWaveform(typename Oscillator< T >::Waveform wf) noexcept
Changes the geometric shape of the LFO modulation.
Definition Phaser.h:367
AudioSpec spec_
Definition Phaser.h:420
static constexpr int kMaxChannels
Definition Phaser.h:418
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio block in-place.
Definition Phaser.h:109
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.
T fastTan(T x) noexcept
Fast approximation of tan(x) using a Pade [5,4] rational approximant.
Definition DspMath.h:187
T fastTanh(T x) noexcept
Fast tanh approximation using Pade rational function.
Definition DspMath.h:126
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
T fastExp(T x) noexcept
Fast approximation of e^x via std::exp2 (~2x faster than std::exp on MSVC).
Definition DspMath.h:167
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
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
std::array< T, kMaxChannels > xPrev
Definition Phaser.h:443
std::array< T, kMaxChannels > yPrev
Definition Phaser.h:444