DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Tremolo.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
25#include "../Core/AudioBuffer.h"
26#include "../Core/AudioSpec.h"
27#include "../Core/DenormalGuard.h"
28#include "../Core/DspMath.h"
29#include "../Core/Phasor.h"
30#include "../Core/Smoothers.h"
31#include "../Core/StateBlob.h"
32
33#include <algorithm>
34#include <atomic>
35#include <cmath>
36#include <cstddef>
37#include <cstdint>
38#include <vector>
39
40namespace dspark {
41
51template <FloatType T>
53{
54public:
55 enum class Shape
56 {
57 Sine,
58 Triangle,
59 Square
60 };
61
70 void prepare(const AudioSpec& spec)
71 {
72 if (!spec.isValid()) return; // release-safe: keep previous state
73
74 sampleRate_ = spec.sampleRate;
75 numChannels_ = spec.numChannels;
76
77 T initialRate = rate_.load(std::memory_order_relaxed);
78 currentRate_ = initialRate;
79
80 for (int ch = 0; ch < kMaxChannels; ++ch)
81 {
82 phasors_[ch].prepare(sampleRate_);
83 phasors_[ch].setFrequency(initialRate);
84 }
85
86 depthSmoother_.reset(sampleRate_, kDepthRampMs,
87 static_cast<float>(depth_.load(std::memory_order_relaxed)));
88
89 isStereoActive_ = stereo_.load(std::memory_order_relaxed);
90 if (isStereoActive_ && numChannels_ >= 2)
91 phasors_[1].setPhase(T(0.5));
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 Shape currentShape = shape_.load(std::memory_order_relaxed);
112
113 // Template dispatching eliminates branching inside the hot path
114 switch (currentShape)
115 {
116 case Shape::Sine: processBlockShape<Shape::Sine>(buffer, numCh, numSamples); break;
117 case Shape::Triangle: processBlockShape<Shape::Triangle>(buffer, numCh, numSamples); break;
118 case Shape::Square: processBlockShape<Shape::Square>(buffer, numCh, numSamples); break;
119 }
120 }
121
126 void reset() noexcept
127 {
128 phasors_[0].reset();
129 phasors_[1].reset();
130 if (isStereoActive_)
131 phasors_[1].setPhase(T(0.5));
132 }
133
139 void setRate(T hz) noexcept
140 {
141 if (!std::isfinite(hz)) return;
142 rate_.store(std::max(T(0), hz), std::memory_order_relaxed);
143 }
144
150 void setDepth(T depth) noexcept
151 {
152 if (!std::isfinite(depth)) return;
153 depth_.store(std::clamp(depth, T(0), T(1)), std::memory_order_relaxed);
154 }
155
160 void setShape(Shape shape) noexcept
161 {
162 const int s = std::clamp(static_cast<int>(shape), 0,
163 static_cast<int>(Shape::Square));
164 shape_.store(static_cast<Shape>(s), std::memory_order_relaxed);
165 }
166
171 void setStereo(bool enabled) noexcept { stereo_.store(enabled, std::memory_order_relaxed); }
172
173 [[nodiscard]] T getRate() const noexcept { return rate_.load(std::memory_order_relaxed); }
174 [[nodiscard]] T getDepth() const noexcept { return depth_.load(std::memory_order_relaxed); }
175 [[nodiscard]] Shape getShape() const noexcept { return shape_.load(std::memory_order_relaxed); }
176 [[nodiscard]] bool isStereo() const noexcept { return stereo_.load(std::memory_order_relaxed); }
177
178
180 [[nodiscard]] std::vector<uint8_t> getState() const
181 {
182 StateWriter w(stateId("TREM"), 1);
183 w.write("rate", rate_.load(std::memory_order_relaxed));
184 w.write("depth", depth_.load(std::memory_order_relaxed));
185 w.write("shape", static_cast<int32_t>(shape_.load(std::memory_order_relaxed)));
186 w.write("stereo", stereo_.load(std::memory_order_relaxed));
187 return w.blob();
188 }
189
191 bool setState(const uint8_t* data, size_t size)
192 {
193 StateReader r(data, size);
194 if (!r.isValid() || r.processorId() != stateId("TREM")) return false;
195 setRate(static_cast<T>(r.read("rate", 4.0f)));
196 setDepth(static_cast<T>(r.read("depth", 0.5f)));
197 setShape(static_cast<Shape>(r.read("shape", 0)));
198 setStereo(r.read("stereo", false));
199 return true;
200 }
201
202private:
203 static constexpr int kMaxChannels = 2;
204 static constexpr float kDepthRampMs = 5.0f;
205 static constexpr T kSquareSlewTime = T(0.02);
206
207 double sampleRate_ = 44100.0;
208 int numChannels_ = 2;
209
210 std::atomic<T> rate_{ T(4) };
211 std::atomic<T> depth_{ T(0.5) };
212 std::atomic<Shape> shape_{ Shape::Sine };
213 std::atomic<bool> stereo_{ false };
214
215 // Internal state owned by Audio Thread
216 T currentRate_ = T(4);
217 bool isStereoActive_ = false;
218
219 Smoothers::LinearSmoother depthSmoother_;
220 Phasor<T> phasors_[kMaxChannels]{};
221
225 inline void updateInternalState() noexcept
226 {
227 T targetRate = rate_.load(std::memory_order_relaxed);
228 if (targetRate != currentRate_)
229 {
230 currentRate_ = targetRate;
231 phasors_[0].setFrequency(currentRate_);
232 phasors_[1].setFrequency(currentRate_);
233 }
234
235 bool targetStereo = stereo_.load(std::memory_order_relaxed);
236 if (targetStereo != isStereoActive_)
237 {
238 isStereoActive_ = targetStereo;
239 if (isStereoActive_) {
240 // Wrap phase to keep synchronization logic intact
241 T newPhase = std::fmod(phasors_[0].getPhase() + T(0.5), T(1));
242 phasors_[1].setPhase(newPhase);
243 } else {
244 phasors_[1].setPhase(phasors_[0].getPhase());
245 }
246 }
247
248 depthSmoother_.setTargetValue(static_cast<float>(depth_.load(std::memory_order_relaxed)));
249 }
250
254 template <Shape S>
255 [[nodiscard]] inline T computeShape(T phase) const noexcept
256 {
257 if constexpr (S == Shape::Sine)
258 {
259 // fastSin: > 100 dB accurate - far beyond audibility for an LFO.
260 return fastSin(phase * twoPi<T>);
261 }
262 else if constexpr (S == Shape::Triangle)
263 {
264 T t = phase * T(4);
265 if (t < T(1)) return t;
266 if (t < T(3)) return T(2) - t;
267 return t - T(4);
268 }
269 else if constexpr (S == Shape::Square)
270 {
271 // Trapezoidal anti-aliased square wave (click-free)
272 if (phase < kSquareSlewTime) return T(-1) + (phase / kSquareSlewTime) * T(2);
273 if (phase < T(0.5)) return T(1);
274 if (phase < T(0.5) + kSquareSlewTime) return T(1) - ((phase - T(0.5)) / kSquareSlewTime) * T(2);
275 return T(-1);
276 }
277 }
278
282 template <Shape S>
283 void processBlockShape(AudioBufferView<T>& buffer, int numCh, int numSamples) noexcept
284 {
285 T* const channelL = buffer.getChannel(0);
286 T* const channelR = (numCh > 1) ? buffer.getChannel(1) : nullptr;
287
288 for (int i = 0; i < numSamples; ++i)
289 {
290 T depthVal = static_cast<T>(depthSmoother_.getNextValue());
291 T phaseL = phasors_[0].advance();
292
293 T modL = computeShape<S>(phaseL);
294 T gainL = T(1) - depthVal * (T(1) - modL) * T(0.5);
295
296 channelL[i] *= gainL;
297
298 if (channelR != nullptr)
299 {
300 if (isStereoActive_)
301 {
302 T phaseR = phasors_[1].advance();
303 T modR = computeShape<S>(phaseR);
304 T gainR = T(1) - depthVal * (T(1) - modR) * T(0.5);
305 channelR[i] *= gainR;
306 }
307 else
308 {
309 // If not stereo, Phasor 1 is kept in sync but we reuse GainL to save CPU
310 (void)phasors_[1].advance();
311 channelR[i] *= gainL;
312 }
313 }
314
315 // Fallback for multi-channel beyond Stereo (surround routed as mono modulation)
316 for (int ch = 2; ch < numCh; ++ch)
317 {
318 buffer.getChannel(ch)[i] *= gainL;
319 }
320 }
321 }
322};
323
324} // 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: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
LFO-driven amplitude modulation with stereo auto-pan option.
Definition Tremolo.h:53
T getRate() const noexcept
Definition Tremolo.h:173
bool isStereo() const noexcept
Definition Tremolo.h:176
void prepare(const AudioSpec &spec)
Prepares the tremolo processor and allocates internal states.
Definition Tremolo.h:70
void setDepth(T depth) noexcept
Sets the modulation depth. Thread-safe.
Definition Tremolo.h:150
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Tremolo.h:180
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Tremolo.h:191
void setShape(Shape shape) noexcept
Sets the LFO waveform shape. Thread-safe.
Definition Tremolo.h:160
@ 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:175
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:174
void setRate(T hz) noexcept
Sets the LFO rate. Thread-safe (can be called from UI thread).
Definition Tremolo.h:139
void reset() noexcept
Hard-resets the internal LFO phase to zero.
Definition Tremolo.h:126
void setStereo(bool enabled) noexcept
Enables auto-pan by offsetting the right channel LFO phase by 180 degrees.
Definition Tremolo.h:171
Main namespace for the DSPark framework.
T fastSin(T x) noexcept
Fast sine approximation (degree-9 odd minimax polynomial).
Definition DspMath.h:213
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
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
void reset(double sampleRate, float rampTimeMilliseconds, float initialValue=0.0f) noexcept
Definition Smoothers.h:327