DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
DryWetMixer.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
42#include "AudioBuffer.h"
43#include "AudioSpec.h"
44
45#include <algorithm>
46#include <atomic>
47#include <cmath>
48
49// DSPARK_RESTRICT is normally provided by SimdOps.h (pulled in via AudioBuffer.h).
50// Guard against redefinition so this header stays self-contained if included alone.
51#ifndef DSPARK_RESTRICT
52 #if defined(__clang__) || defined(__GNUC__)
53 #define DSPARK_RESTRICT __restrict__
54 #elif defined(_MSC_VER)
55 #define DSPARK_RESTRICT __restrict
56 #else
57 #define DSPARK_RESTRICT
58 #endif
59#endif
60
61namespace dspark {
62
70template <typename T, int MaxChannels = 16>
72{
73public:
75 enum class MixRule {
77 Linear,
80 };
81
82 DryWetMixer() noexcept = default;
83
84 // The atomic mix rule makes the implicit move operations vanish. Provide
85 // manual moves so the mixer keeps working inside movable owners; moves
86 // are single-threaded setup-time relocations, so plain relaxed transfers
87 // of the atomic are sufficient. Copying is deleted by the owned buffers.
88 DryWetMixer(DryWetMixer&& other) noexcept
89 : dryBuffer_(std::move(other.dryBuffer_)),
90 capturedSamples_(other.capturedSamples_),
91 delayHist_(std::move(other.delayHist_)),
92 latencySamples_(other.latencySamples_),
93 histPos_(other.histPos_),
94 mixRule_(other.mixRule_.load(std::memory_order_relaxed)),
95 currentMix_(other.currentMix_)
96 {}
97
99 {
100 if (this == &other) return *this;
101 dryBuffer_ = std::move(other.dryBuffer_);
102 capturedSamples_ = other.capturedSamples_;
103 delayHist_ = std::move(other.delayHist_);
104 latencySamples_ = other.latencySamples_;
105 histPos_ = other.histPos_;
106 mixRule_.store(other.mixRule_.load(std::memory_order_relaxed),
107 std::memory_order_relaxed);
108 currentMix_ = other.currentMix_;
109 return *this;
110 }
111
112 DryWetMixer(const DryWetMixer&) = delete;
114
119 void prepare(const AudioSpec& spec)
120 {
121 dryBuffer_.resize(spec.numChannels, spec.maxBlockSize);
122
123 // If latency compensation was configured before prepare(), the delay
124 // history was sized with 0 channels; rebuild it now that the channel
125 // count is known (otherwise pushDry would index a 0-channel buffer).
126 if (latencySamples_ > 0)
127 {
128 delayHist_.resize(spec.numChannels, latencySamples_);
129 histPos_ = 0;
130 }
131
132 reset();
133 }
134
136 void reset() noexcept
137 {
138 dryBuffer_.clear();
139 if (delayHist_.getNumSamples() > 0) delayHist_.clear();
140 histPos_ = 0;
141 capturedSamples_ = 0; // No valid dry snapshot until the next pushDry().
142 currentMix_ = T(-1); // Forces immediate jump on first use
143 }
144
153 void setMixRule(MixRule rule) noexcept
154 {
155 mixRule_.store(rule, std::memory_order_relaxed);
156 }
157
170 void setLatencyCompensation(int samples)
171 {
172 samples = std::max(0, samples);
173 if (samples == latencySamples_) return;
174 latencySamples_ = samples;
175 delayHist_.resize(dryBuffer_.getNumChannels(), samples);
176 histPos_ = 0;
177 }
178
180 [[nodiscard]] int getLatencyCompensation() const noexcept { return latencySamples_; }
181
192 void pushDry(const AudioBufferView<const T>& input) noexcept
193 {
194 if (dryBuffer_.getNumSamples() == 0) return; // Prevent ops if unprepared
195
196 const int chCount = std::min(input.getNumChannels(), dryBuffer_.getNumChannels());
197 const int nSamples = std::min(input.getNumSamples(), dryBuffer_.getNumSamples());
198
199 if (latencySamples_ <= 0)
200 {
201 for (int ch = 0; ch < chCount; ++ch)
202 {
203 const T* DSPARK_RESTRICT src = input.getChannel(ch);
204 T* DSPARK_RESTRICT dst = dryBuffer_.getChannel(ch);
205 std::copy_n(src, nSamples, dst);
206 }
207 }
208 else
209 {
210 // Latency-compensated capture: route the dry through a per-channel
211 // circular delay of exactly latencySamples_ so it stays time-aligned
212 // with a wet path that incurs the same delay.
213 const int D = latencySamples_;
214 for (int ch = 0; ch < chCount; ++ch)
215 {
216 const T* DSPARK_RESTRICT src = input.getChannel(ch);
217 T* DSPARK_RESTRICT dst = dryBuffer_.getChannel(ch);
218 T* DSPARK_RESTRICT hist = delayHist_.getChannel(ch);
219 int pos = histPos_;
220 for (int i = 0; i < nSamples; ++i)
221 {
222 dst[i] = hist[pos]; // sample written D steps ago
223 hist[pos] = src[i];
224 if (++pos == D) pos = 0;
225 }
226 }
227 histPos_ = (histPos_ + nSamples) % D;
228 }
229
230 capturedSamples_ = nSamples;
231 }
232
247 void mixWet(AudioBufferView<T> wetBuffer, T targetMix) noexcept
248 {
249 // Handle invalid floating point inputs (NaN) safely
250 if (std::isnan(targetMix)) targetMix = T(0);
251 targetMix = std::clamp(targetMix, T(0), T(1));
252
253 const int chCount = std::min(wetBuffer.getNumChannels(), dryBuffer_.getNumChannels());
254 const int nSamples = std::min(wetBuffer.getNumSamples(), capturedSamples_);
255
256 if (nSamples == 0 || chCount == 0) return;
257
258 // Initialize smoothing state on first run
259 if (currentMix_ < T(0)) currentMix_ = targetMix;
260
261 const bool needsSmoothing = std::abs(currentMix_ - targetMix) > T(1e-5);
262 const T mixStep = needsSmoothing ? (targetMix - currentMix_) / T(nSamples) : T(0);
263 const MixRule rule = mixRule_.load(std::memory_order_relaxed);
264
265 // Every channel ramps from the same smoothed origin. The ramps below
266 // are indexed (mix0 + mixStep * i) rather than accumulated: one
267 // rounding per sample instead of a drift that grows with the block
268 // (a serial accumulator can overshoot the [0, 1] range on long
269 // blocks), and no loop-carried dependency in the way of the
270 // auto-vectoriser.
271 const T mix0 = currentMix_;
272
273 for (int ch = 0; ch < chCount; ++ch)
274 {
275 T* DSPARK_RESTRICT wetData = wetBuffer.getChannel(ch);
276 const T* DSPARK_RESTRICT dryData = dryBuffer_.getChannel(ch);
277
278 if (rule == MixRule::EqualPower)
279 {
280 if (!needsSmoothing)
281 {
282 // Static mix: hoist both square roots out of the loop;
283 // the body becomes a single FMA per sample (vectorizable).
284 const T w = std::sqrt(mix0);
285 const T d = std::sqrt(T(1) - mix0);
286 for (int i = 0; i < nSamples; ++i)
287 wetData[i] = dryData[i] * d + wetData[i] * w;
288 }
289 else
290 {
291 for (int i = 0; i < nSamples; ++i)
292 {
293 // Exact constant-power law: w^2 + d^2 = 1. The max()
294 // guards keep float rounding at the ramp ends from
295 // pushing a radicand a few ulp negative, where sqrt()
296 // would inject NaN into the output.
297 const T m = mix0 + mixStep * T(i);
298 const T w = std::sqrt(std::max(T(0), m));
299 const T d = std::sqrt(std::max(T(0), T(1) - m));
300 wetData[i] = dryData[i] * d + wetData[i] * w;
301 }
302 }
303 }
304 else // MixRule::Linear
305 {
306 if (!needsSmoothing)
307 {
308 const T w = mix0;
309 const T d = T(1) - w;
310 for (int i = 0; i < nSamples; ++i)
311 wetData[i] = dryData[i] * d + wetData[i] * w;
312 }
313 else
314 {
315 for (int i = 0; i < nSamples; ++i)
316 {
317 const T w = mix0 + mixStep * T(i);
318 const T d = T(1) - w;
319 wetData[i] = dryData[i] * d + wetData[i] * w;
320 }
321 }
322 }
323 }
324
325 // Update the persistent state after the block is processed
326 if (needsSmoothing) currentMix_ = targetMix;
327 }
328
334 [[nodiscard]] const T* getDryChannel(int ch) const noexcept
335 {
336 if (ch < 0 || ch >= dryBuffer_.getNumChannels()) return nullptr;
337 return dryBuffer_.getChannel(ch);
338 }
339
341 [[nodiscard]] int getDryNumChannels() const noexcept { return dryBuffer_.getNumChannels(); }
342
344 [[nodiscard]] int getDryCapturedSamples() const noexcept { return capturedSamples_; }
345
346private:
348 int capturedSamples_ = 0;
349
350 // Optional dry-path latency compensation (circular delay). Disabled (0) by default.
352 int latencySamples_ = 0;
353 int histPos_ = 0;
354
355 std::atomic<MixRule> mixRule_ { MixRule::Linear };
356 T currentMix_ = T(-1); // Internal state for parameter smoothing
357};
358
359} // namespace dspark
Owning audio buffer and non-owning view for real-time DSP processing.
Describes the audio processing environment (sample rate, block size, channels).
#define DSPARK_RESTRICT
Definition SimdOps.h:60
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Owning audio buffer with contiguous, 32-byte aligned storage.
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:72
void setLatencyCompensation(int samples)
Delays the captured dry signal to compensate for an effect's internal latency (e.g....
int getLatencyCompensation() const noexcept
Returns the configured dry-path latency compensation in samples.
MixRule
Defines the mathematical curve used for mixing.
Definition DryWetMixer.h:75
int getDryCapturedSamples() const noexcept
Returns the number of samples valid from the last pushDry() call.
void mixWet(AudioBufferView< T > wetBuffer, T targetMix) noexcept
Blends the stored dry signal with the current (wet) buffer in-place.
void reset() noexcept
Resets the internal buffer and smoothing states to zero.
int getDryNumChannels() const noexcept
Returns the internal capacity of channels in the dry buffer.
DryWetMixer() noexcept=default
DryWetMixer & operator=(DryWetMixer &&other) noexcept
Definition DryWetMixer.h:98
void pushDry(const AudioBufferView< const T > &input) noexcept
Captures a snapshot of the dry (unprocessed) signal.
void setMixRule(MixRule rule) noexcept
Sets the mathematical curve to use during the mix phase.
void prepare(const AudioSpec &spec)
Allocates the internal dry buffer for the given audio spec.
DryWetMixer & operator=(const DryWetMixer &)=delete
const T * getDryChannel(int ch) const noexcept
Retrieves a read-only pointer to the captured dry channel data.
DryWetMixer(const DryWetMixer &)=delete
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:51