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