DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Tremolo.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
27#include "../Core/AudioBuffer.h"
28#include "../Core/AudioSpec.h"
29#include "../Core/DenormalGuard.h"
30#include "../Core/DspMath.h"
31#include "../Core/Phasor.h"
32#include "../Core/Smoothers.h"
33#include "../Core/StateBlob.h"
34
35#include <algorithm>
36#include <atomic>
37#include <cmath>
38#include <cstddef>
39#include <cstdint>
40#include <vector>
41
42namespace dspark {
43
53template <FloatType T>
55{
56public:
57 enum class Shape
58 {
59 Sine,
60 Triangle,
61 Square
62 };
63
72 void prepare(const AudioSpec& spec)
73 {
74 if (!spec.isValid()) return; // release-safe: keep previous state
75
76 sampleRate_ = spec.sampleRate;
77 numChannels_ = spec.numChannels;
78
79 T initialRate = rate_.load(std::memory_order_relaxed);
80 currentRate_ = initialRate;
81 lfo_.prepare(sampleRate_);
82 lfo_.setFrequency(initialRate);
83
84 depthSmoother_.reset(sampleRate_, kDepthRampMs,
85 static_cast<float>(depth_.load(std::memory_order_relaxed)));
86
87 // Start settled on the current shape and stereo mode (no fade).
88 activeShape_ = shape_.load(std::memory_order_relaxed);
89 offsetR_ = stereo_.load(std::memory_order_relaxed) ? T(0.5) : T(0);
90 fadeLength_ = std::max(1, static_cast<int>(sampleRate_ * (kFadeMs / 1000.0)));
91 fadeRemaining_ = 0;
92 }
93
98 void processBlock(AudioBufferView<T> buffer) noexcept
99 {
100 const int numCh = std::min(buffer.getNumChannels(), numChannels_);
101 const int numSamples = buffer.getNumSamples();
102
103 if (numCh == 0 || numSamples == 0)
104 return;
105
106 DenormalGuard guard;
107
108 // Thread-safe parameter polling (Audio Thread owns the state)
109 updateInternalState();
110
111 // A shape or stereo-mode change crossfades from the old modulation
112 // to the new one; both used to switch at once, stepping the gain
113 // (up to the full depth when the right LFO jumped half a cycle).
114 int start = 0;
115 if (fadeRemaining_ > 0)
116 {
117 start = std::min(numSamples, fadeRemaining_);
118 processFade(buffer, numCh, start);
119 }
120 if (start == numSamples) return;
121
122 // Template dispatching eliminates branching inside the hot path
123 switch (activeShape_)
124 {
125 case Shape::Sine: processBlockShape<Shape::Sine>(buffer, numCh, start, numSamples); break;
126 case Shape::Triangle: processBlockShape<Shape::Triangle>(buffer, numCh, start, numSamples); break;
127 case Shape::Square: processBlockShape<Shape::Square>(buffer, numCh, start, numSamples); break;
128 }
129 }
130
135 void reset() noexcept
136 {
137 lfo_.reset();
138 fadeRemaining_ = 0;
139 }
140
146 void setRate(T hz) noexcept
147 {
148 if (!std::isfinite(hz)) return;
149 rate_.store(std::max(T(0), hz), std::memory_order_relaxed);
150 }
151
157 void setDepth(T depth) noexcept
158 {
159 if (!std::isfinite(depth)) return;
160 depth_.store(std::clamp(depth, T(0), T(1)), std::memory_order_relaxed);
161 }
162
167 void setShape(Shape shape) noexcept
168 {
169 const int s = std::clamp(static_cast<int>(shape), 0,
170 static_cast<int>(Shape::Square));
171 shape_.store(static_cast<Shape>(s), std::memory_order_relaxed);
172 }
173
178 void setStereo(bool enabled) noexcept { stereo_.store(enabled, std::memory_order_relaxed); }
179
180 [[nodiscard]] T getRate() const noexcept { return rate_.load(std::memory_order_relaxed); }
181 [[nodiscard]] T getDepth() const noexcept { return depth_.load(std::memory_order_relaxed); }
182 [[nodiscard]] Shape getShape() const noexcept { return shape_.load(std::memory_order_relaxed); }
183 [[nodiscard]] bool isStereo() const noexcept { return stereo_.load(std::memory_order_relaxed); }
184
185
187 [[nodiscard]] std::vector<uint8_t> getState() const
188 {
189 StateWriter w(stateId("TREM"), 1);
190 w.write("rate", rate_.load(std::memory_order_relaxed));
191 w.write("depth", depth_.load(std::memory_order_relaxed));
192 w.write("shape", static_cast<int32_t>(shape_.load(std::memory_order_relaxed)));
193 w.write("stereo", stereo_.load(std::memory_order_relaxed));
194 return w.blob();
195 }
196
198 bool setState(const uint8_t* data, size_t size)
199 {
200 StateReader r(data, size);
201 if (!r.isValid() || r.processorId() != stateId("TREM")) return false;
202 setRate(static_cast<T>(r.read("rate", 4.0f)));
203 setDepth(static_cast<T>(r.read("depth", 0.5f)));
204 setShape(static_cast<Shape>(r.read("shape", 0)));
205 setStereo(r.read("stereo", false));
206 return true;
207 }
208
209private:
210 static constexpr float kDepthRampMs = 5.0f;
211 static constexpr double kFadeMs = 5.0;
212 static constexpr T kSquareSlewTime = T(0.02);
213
214 double sampleRate_ = 44100.0;
215 int numChannels_ = 2;
216
217 std::atomic<T> rate_{ T(4) };
218 std::atomic<T> depth_{ T(0.5) };
219 std::atomic<Shape> shape_{ Shape::Sine };
220 std::atomic<bool> stereo_{ false };
221
222 // Internal state owned by Audio Thread
223 T currentRate_ = T(4);
224 Shape activeShape_ = Shape::Sine;
225 T offsetR_ = T(0);
226 Shape fadeShape_ = Shape::Sine;
227 T fadeOffsetR_ = T(0);
228 int fadeLength_ = 1;
229 int fadeRemaining_ = 0;
230
231 Smoothers::LinearSmoother depthSmoother_;
232 Phasor<T> lfo_;
233
237 inline void updateInternalState() noexcept
238 {
239 T targetRate = rate_.load(std::memory_order_relaxed);
240 if (targetRate != currentRate_)
241 {
242 currentRate_ = targetRate;
243 lfo_.setFrequency(currentRate_);
244 }
245
246 const Shape shape = shape_.load(std::memory_order_relaxed);
247 const T offset = stereo_.load(std::memory_order_relaxed) ? T(0.5) : T(0);
248 if (shape != activeShape_ || offset != offsetR_)
249 {
250 // A change during a fade restarts it from the latest settled law.
251 fadeShape_ = activeShape_;
252 fadeOffsetR_ = offsetR_;
253 activeShape_ = shape;
254 offsetR_ = offset;
255 fadeRemaining_ = fadeLength_;
256 }
257
258 depthSmoother_.setTargetValue(static_cast<float>(depth_.load(std::memory_order_relaxed)));
259 }
260
262 [[nodiscard]] static inline T wrapPhase(T phase) noexcept
263 {
264 return phase >= T(1) ? phase - T(1) : phase;
265 }
266
268 [[nodiscard]] inline T evalShape(Shape shape, T phase) const noexcept
269 {
270 switch (shape)
271 {
272 case Shape::Triangle: return computeShape<Shape::Triangle>(phase);
273 case Shape::Square: return computeShape<Shape::Square>(phase);
274 case Shape::Sine:
275 default: return computeShape<Shape::Sine>(phase);
276 }
277 }
278
284 void processFade(AudioBufferView<T>& buffer, int numCh, int n) noexcept
285 {
286 T* const channelL = buffer.getChannel(0);
287 T* const channelR = (numCh > 1) ? buffer.getChannel(1) : nullptr;
288 const T invLength = T(1) / static_cast<T>(fadeLength_);
289
290 for (int i = 0; i < n; ++i)
291 {
292 const T depthVal = static_cast<T>(depthSmoother_.getNextValue());
293 const T phaseL = lfo_.advance();
294 const T w = static_cast<T>(fadeLength_ - fadeRemaining_ + 1) * invLength;
295 --fadeRemaining_;
296
297 const T oldL = evalShape(fadeShape_, phaseL);
298 const T modL = oldL + w * (evalShape(activeShape_, phaseL) - oldL);
299 const T gainL = T(1) - depthVal * (T(1) - modL) * T(0.5);
300 channelL[i] *= gainL;
301
302 if (channelR != nullptr)
303 {
304 const T oldR = evalShape(fadeShape_, wrapPhase(phaseL + fadeOffsetR_));
305 const T modR = oldR + w * (evalShape(activeShape_, wrapPhase(phaseL + offsetR_)) - oldR);
306 channelR[i] *= T(1) - depthVal * (T(1) - modR) * T(0.5);
307 }
308
309 for (int ch = 2; ch < numCh; ++ch)
310 buffer.getChannel(ch)[i] *= gainL;
311 }
312 }
313
317 template <Shape S>
318 [[nodiscard]] inline T computeShape(T phase) const noexcept
319 {
320 if constexpr (S == Shape::Sine)
321 {
322 // fastSin: > 100 dB accurate - far beyond audibility for an LFO.
323 return fastSin(phase * twoPi<T>);
324 }
325 else if constexpr (S == Shape::Triangle)
326 {
327 T t = phase * T(4);
328 if (t < T(1)) return t;
329 if (t < T(3)) return T(2) - t;
330 return t - T(4);
331 }
332 else if constexpr (S == Shape::Square)
333 {
334 // Trapezoidal anti-aliased square wave (click-free)
335 if (phase < kSquareSlewTime) return T(-1) + (phase / kSquareSlewTime) * T(2);
336 if (phase < T(0.5)) return T(1);
337 if (phase < T(0.5) + kSquareSlewTime) return T(1) - ((phase - T(0.5)) / kSquareSlewTime) * T(2);
338 return T(-1);
339 }
340 }
341
345 template <Shape S>
346 void processBlockShape(AudioBufferView<T>& buffer, int numCh, int start, int end) noexcept
347 {
348 T* const channelL = buffer.getChannel(0);
349 T* const channelR = (numCh > 1) ? buffer.getChannel(1) : nullptr;
350 const bool stereo = offsetR_ != T(0);
351
352 for (int i = start; i < end; ++i)
353 {
354 T depthVal = static_cast<T>(depthSmoother_.getNextValue());
355 T phaseL = lfo_.advance();
356
357 T modL = computeShape<S>(phaseL);
358 T gainL = T(1) - depthVal * (T(1) - modL) * T(0.5);
359
360 channelL[i] *= gainL;
361
362 if (channelR != nullptr)
363 {
364 if (stereo)
365 {
366 T modR = computeShape<S>(wrapPhase(phaseL + offsetR_));
367 T gainR = T(1) - depthVal * (T(1) - modR) * T(0.5);
368 channelR[i] *= gainR;
369 }
370 else
371 {
372 channelR[i] *= gainL; // mono modulation: same gain
373 }
374 }
375
376 // Fallback for multi-channel beyond Stereo (surround routed as mono modulation)
377 for (int ch = 2; ch < numCh; ++ch)
378 {
379 buffer.getChannel(ch)[i] *= gainL;
380 }
381 }
382 }
383};
384
385} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
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
LFO-driven amplitude modulation with stereo auto-pan option.
Definition Tremolo.h:55
T getRate() const noexcept
Definition Tremolo.h:180
bool isStereo() const noexcept
Definition Tremolo.h:183
void prepare(const AudioSpec &spec)
Prepares the tremolo processor and allocates internal states.
Definition Tremolo.h:72
void setDepth(T depth) noexcept
Sets the modulation depth. Thread-safe.
Definition Tremolo.h:157
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Tremolo.h:187
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Tremolo.h:198
void setShape(Shape shape) noexcept
Sets the LFO waveform shape. Thread-safe.
Definition Tremolo.h:167
@ Triangle
Linear modulation, sharper peaks.
@ Sine
Classic smooth tremolo (opto-isolator style)
@ Square
Slew-rate limited gating effect (analog-style, click-free)
Shape getShape() const noexcept
Definition Tremolo.h:182
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio block in-place with SIMD-friendly dispatch.
Definition Tremolo.h:98
T getDepth() const noexcept
Definition Tremolo.h:181
void setRate(T hz) noexcept
Sets the LFO rate. Thread-safe (can be called from UI thread).
Definition Tremolo.h:146
void reset() noexcept
Hard-resets the internal LFO phase to zero.
Definition Tremolo.h:135
void setStereo(bool enabled) noexcept
Enables auto-pan by offsetting the right channel LFO phase by 180 degrees.
Definition Tremolo.h:178
Main namespace for the DSPark framework.
T fastSin(T x) noexcept
Fast sine approximation (degree-9 odd minimax polynomial).
Definition DspMath.h:248
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
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
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
void reset(double sampleRate, float rampTimeMilliseconds, float initialValue=0.0f) noexcept
Definition Smoothers.h:327