DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
RingModulator.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
33#include "../Core/AudioBuffer.h"
34#include "../Core/AudioSpec.h"
35#include "../Core/DenormalGuard.h"
36#include "../Core/DspMath.h"
37#include "../Core/Phasor.h"
38#include "../Core/StateBlob.h"
39
40#include <algorithm>
41#include <array>
42#include <atomic>
43#include <cmath>
44#include <cstddef>
45#include <cstdint>
46#include <numbers>
47#include <vector>
48
49namespace dspark {
50
65template <FloatType T>
67{
68public:
70 enum class Mode
71 {
72 Classic,
74 };
75
85 void prepare(const AudioSpec& spec) noexcept
86 {
87 if (!spec.isValid()) return; // release-safe: keep previous state
88
89 phasor_.prepare(spec.sampleRate);
90
91 const T initialFreq = frequency_.load(std::memory_order_relaxed);
92 const T initialMix = mix_.load(std::memory_order_relaxed);
93
94 phasor_.setFrequency(initialFreq);
95 currentFreq_ = initialFreq;
96 currentMix_ = initialMix;
97 numChannels_ = spec.numChannels;
98 }
99
104 void processBlock(AudioBufferView<T> buffer) noexcept
105 {
106 const int numCh = std::min(buffer.getNumChannels(), numChannels_);
107 const int numSamples = buffer.getNumSamples();
108 if (numSamples <= 0 || numCh <= 0) return;
109
110 // RT denormal hygiene: consistent with the framework's other audio-path
111 // processors (the GeometricMean sqrt/copysign chain can carry denormal
112 // inputs into the slow path).
113 DenormalGuard guard;
114
115 // Fetch targets for smoothing
116 const T targetFreq = frequency_.load(std::memory_order_relaxed);
117 const T targetMix = mix_.load(std::memory_order_relaxed);
118 const T soarVal = soar_.load(std::memory_order_relaxed);
119 const auto modeVal = mode_.load(std::memory_order_relaxed);
120
121 // Calculate smoothing steps
122 const T invSamples = T(1) / static_cast<T>(numSamples);
123 const T freqStep = (targetFreq - currentFreq_) * invSamples;
124 const T mixStep = (targetMix - currentMix_) * invSamples;
125
126 // Process in L1-cache friendly chunks to allow outer channel loop
127 constexpr int CHUNK_SIZE = 64;
128 std::array<T, CHUNK_SIZE> carrierChunk;
129 std::array<T, CHUNK_SIZE> mixChunk;
130
131 const T twoPi = T(2) * static_cast<T>(std::numbers::pi);
132
133 for (int start = 0; start < numSamples; start += CHUNK_SIZE)
134 {
135 const int chunkLen = std::min(CHUNK_SIZE, numSamples - start);
136
137 // 1. Precalculate shared carrier and mix block
138 for (int i = 0; i < chunkLen; ++i)
139 {
140 currentFreq_ += freqStep;
141 currentMix_ += mixStep;
142
143 phasor_.setFrequency(currentFreq_);
144 T phase = phasor_.advance();
145
146 // fastSin: error > 100 dB below the carrier - inaudible even
147 // though the carrier itself is audible in ring modulation.
148 carrierChunk[i] = fastSin(phase * twoPi);
149 mixChunk[i] = currentMix_;
150 }
151
152 // 2. Process channels with hoisted branches and SIMD-friendly loops
153 if (modeVal == Mode::GeometricMean)
154 {
155 for (int ch = 0; ch < numCh; ++ch)
156 {
157 T* data = buffer.getChannel(ch) + start;
158
159 for (int i = 0; i < chunkLen; ++i)
160 {
161 const T dry = data[i];
162 const T carrier = carrierChunk[i];
163
164 const T absIn = std::abs(dry);
165 const T absCarrier = std::abs(carrier);
166
167 // Scaled soarVal prevents DC offset when input is zero
168 const T gm = std::sqrt(absIn * absCarrier + soarVal * absIn);
169
170 // Ultra-fast sign evaluation without branching
171 const T wet = gm * std::copysign(T(1), dry * carrier);
172
173 data[i] = dry + (wet - dry) * mixChunk[i];
174 }
175 }
176 }
177 else // Mode::Classic
178 {
179 for (int ch = 0; ch < numCh; ++ch)
180 {
181 T* data = buffer.getChannel(ch) + start;
182
183 for (int i = 0; i < chunkLen; ++i)
184 {
185 const T dry = data[i];
186 const T wet = dry * carrierChunk[i];
187 data[i] = dry + (wet - dry) * mixChunk[i];
188 }
189 }
190 }
191 }
192
193 // Land exactly on the published targets: the accumulated ramp ends
194 // within rounding of them, and the next block must start settled
195 // (matches the framework's smoothing convention).
196 currentFreq_ = targetFreq;
197 currentMix_ = targetMix;
198 }
199
201 void reset() noexcept { phasor_.reset(); }
202
209 void setFrequency(T hz) noexcept
210 {
211 if (!std::isfinite(hz)) return; // NaN/Inf would poison the smoothed ramp
212 frequency_.store(hz, std::memory_order_relaxed);
213 }
214
220 void setMix(T mix) noexcept
221 {
222 if (!std::isfinite(mix)) return;
223 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
224 }
225
234 void setMode(Mode m) noexcept
235 {
236 const int v = std::clamp(static_cast<int>(m),
237 static_cast<int>(Mode::Classic),
238 static_cast<int>(Mode::GeometricMean));
239 mode_.store(static_cast<Mode>(v), std::memory_order_relaxed);
240 }
241
254 void setSoar(T amount) noexcept
255 {
256 if (!std::isfinite(amount)) return;
257 soar_.store(std::max(amount, T(0)), std::memory_order_relaxed);
258 }
259
260 [[nodiscard]] T getFrequency() const noexcept { return frequency_.load(std::memory_order_relaxed); }
261 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
262 [[nodiscard]] Mode getMode() const noexcept { return mode_.load(std::memory_order_relaxed); }
263 [[nodiscard]] T getSoar() const noexcept { return soar_.load(std::memory_order_relaxed); }
264
266 [[nodiscard]] std::vector<uint8_t> getState() const
267 {
268 // The blob stores float (setState reads float back); the explicit
269 // casts also keep this overload resolvable when T is double.
270 StateWriter w(stateId("RING"), 1);
271 w.write("frequency", static_cast<float>(frequency_.load(std::memory_order_relaxed)));
272 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
273 w.write("mode", static_cast<int32_t>(mode_.load(std::memory_order_relaxed)));
274 w.write("soar", static_cast<float>(soar_.load(std::memory_order_relaxed)));
275 return w.blob();
276 }
277
279 bool setState(const uint8_t* data, size_t size)
280 {
281 StateReader r(data, size);
282 if (!r.isValid() || r.processorId() != stateId("RING")) return false;
283 setFrequency(static_cast<T>(r.read("frequency", 440.0f)));
284 setMix(static_cast<T>(r.read("mix", 1.0f)));
285 setMode(static_cast<Mode>(r.read("mode", 0)));
286 setSoar(static_cast<T>(r.read("soar", 0.0f)));
287 return true;
288 }
289
290private:
291 int numChannels_ = 0;
292
293 // Atomic targets for lock-free UI/Thread communication
294 std::atomic<T> frequency_ { T(440) };
295 std::atomic<T> mix_ { T(1) };
296 std::atomic<Mode> mode_ { Mode::Classic };
297 std::atomic<T> soar_ { T(0) };
298
299 // DSP State (Internal audio-thread only, no atomics required)
300 T currentFreq_ { T(440) };
301 T currentMix_ { T(1) };
302 Phasor<T> phasor_;
303};
304
305} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Signal x carrier ring modulation with mix control and zero-latency smoothing.
Mode getMode() const noexcept
void reset() noexcept
Resets the internal phase of the carrier oscillator.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes a block of audio in-place.
T getSoar() const noexcept
void setMix(T mix) noexcept
Sets the target dry/wet mix. Smoothed internally.
T getFrequency() const noexcept
Mode
Modulation mathematical mode.
@ GeometricMean
Geometric-mean mode: sqrt(|in|*|carrier|) * sign - richer, more musical.
@ Classic
Standard multiplication (sum & difference frequencies).
void prepare(const AudioSpec &spec) noexcept
Prepares the modulator for audio processing.
T getMix() const noexcept
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
void setSoar(T amount) noexcept
Sets the soar threshold for Geometric Mean mode.
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
void setMode(Mode m) noexcept
Sets the modulation mode.
void setFrequency(T hz) noexcept
Sets the target carrier frequency. Smoothed internally.
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 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
constexpr T twoPi
2 * Pi (6.28318...).
Definition DspMath.h:39
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35