DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Crossfade.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
42#include "../Core/AudioSpec.h"
43#include "../Core/DspMath.h"
44
45#include <algorithm>
46#include <atomic>
47#include <cassert>
48#include <cmath>
49
50namespace dspark {
51
61template <FloatType T>
62class Crossfade final
63{
64public:
66 enum class Curve
67 {
68 Linear,
70 SCurve
71 };
72
73 Crossfade() = default;
74 ~Crossfade() = default;
75
87 void prepare(double sampleRate) noexcept
88 {
89 if (!(sampleRate > 0.0) || !std::isfinite(sampleRate)) return;
90 maxPosStep_ = static_cast<T>(1.0 / (sampleRate * kMinGlideSeconds));
91 reset();
92 }
93
98 void prepare(const AudioSpec& spec) noexcept { prepare(spec.sampleRate); }
99
105 void reset() noexcept
106 {
107 settle();
108 needsSettle_ = true;
109 }
110
118 void setCurve(Curve curve) noexcept
119 {
120 curve = static_cast<Curve>(std::clamp(static_cast<int>(curve), 0,
121 static_cast<int>(Curve::SCurve)));
122 curve_.store(curve, std::memory_order_relaxed);
123 }
124
133 void setPosition(T position) noexcept
134 {
135 if (!std::isfinite(position)) return;
136 position_.store(std::clamp(position, T(0), T(1)), std::memory_order_relaxed);
137 }
138
143 [[nodiscard]] T getPosition() const noexcept
144 {
145 return position_.load(std::memory_order_relaxed);
146 }
147
159 static void gainsFor(Curve curve, T position, T& gainA, T& gainB) noexcept
160 {
161 // min/max with this argument order also resolves NaN to 0.
162 position = std::min(T(1), std::max(T(0), position));
163 fillGains(curve, &position, &gainA, &gainB, 1);
164 }
165
178 [[nodiscard]] inline T process(T a, T b) noexcept
179 {
180 settleIfFresh();
181 const T target = position_.load(std::memory_order_relaxed);
182 const Curve curve = curve_.load(std::memory_order_relaxed);
183 if (target != curPos_ || curve != lastCurve_)
184 {
185 curPos_ = maxPosStep_ > T(0) ? moveTowards(curPos_, target, maxPosStep_) : target;
186 lastCurve_ = curve;
187 gainsFor(curve, curPos_, gainA_, gainB_);
188 publishGains();
189 }
190 return a * gainA_ + b * gainB_;
191 }
192
207 void process(const T* inputA, const T* inputB, T* output, int numSamples) noexcept
208 {
209 assert(inputA != nullptr && inputB != nullptr && output != nullptr);
210 assert(numSamples > 0);
211 if (numSamples <= 0) return;
212
213 settleIfFresh();
214 const T target = position_.load(std::memory_order_relaxed);
215 const Curve curve = curve_.load(std::memory_order_relaxed);
216
217 if (target == curPos_ && curve == lastCurve_)
218 {
219 // Hot path: settled. Ideal for SIMD autovectorization.
220 const T gA = gainA_;
221 const T gB = gainB_;
222 for (int i = 0; i < numSamples; ++i)
223 output[i] = inputA[i] * gA + inputB[i] * gB;
224 return;
225 }
226
227 const T startPos = curPos_;
228 const Curve oldCurve = lastCurve_;
229 const bool prepared = maxPosStep_ > T(0);
230 const T step = prepared ? maxPosStep_
231 : std::abs(target - startPos) / static_cast<T>(numSamples);
232 const T invSamples = T(1) / static_cast<T>(numSamples);
233
234 T pos[kChunk], gA[kChunk], gB[kChunk], oldA[kChunk], oldB[kChunk];
235 for (int start = 0; start < numSamples; start += kChunk)
236 {
237 const int n = std::min(kChunk, numSamples - start);
238 for (int i = 0; i < n; ++i)
239 pos[i] = moveTowards(startPos, target, step * static_cast<T>(start + i + 1));
240 fillGains(curve, pos, gA, gB, n);
241
242 if (curve != oldCurve)
243 {
244 fillGains(oldCurve, pos, oldA, oldB, n);
245 for (int i = 0; i < n; ++i)
246 {
247 const T w = static_cast<T>(start + i + 1) * invSamples;
248 gA[i] = oldA[i] + w * (gA[i] - oldA[i]);
249 gB[i] = oldB[i] + w * (gB[i] - oldB[i]);
250 }
251 }
252
253 for (int i = 0; i < n; ++i)
254 output[start + i] = inputA[start + i] * gA[i] + inputB[start + i] * gB[i];
255 }
256
257 // The block ramp of an unprepared crossfader always lands on the target.
258 curPos_ = prepared ? moveTowards(startPos, target, step * static_cast<T>(numSamples))
259 : target;
260 lastCurve_ = curve;
261 gainsFor(curve, curPos_, gainA_, gainB_);
262 publishGains();
263 }
264
279 void processAutomated(const T* inputA, const T* inputB,
280 const T* positions, T* output, int numSamples) noexcept
281 {
282 assert(inputA && inputB && positions && output);
283 assert(numSamples > 0);
284 if (numSamples <= 0) return;
285
286 const Curve curve = curve_.load(std::memory_order_relaxed);
287
288 T pos[kChunk], gA[kChunk], gB[kChunk];
289 for (int start = 0; start < numSamples; start += kChunk)
290 {
291 const int n = std::min(kChunk, numSamples - start);
292 // min/max with this argument order also resolves a NaN in the
293 // automation buffer to 0 (100% A) instead of poisoning the output.
294 for (int i = 0; i < n; ++i)
295 pos[i] = std::min(T(1), std::max(T(0), positions[start + i]));
296 fillGains(curve, pos, gA, gB, n);
297 for (int i = 0; i < n; ++i)
298 output[start + i] = inputA[start + i] * gA[i] + inputB[start + i] * gB[i];
299 }
300
301 // Update the audio-thread state to the end of the automation block.
302 // The atomic position_ is NOT written back: that would race with the
303 // GUI thread.
304 needsSettle_ = false;
305 curPos_ = std::min(T(1), std::max(T(0), positions[numSamples - 1]));
306 lastCurve_ = curve;
307 gainsFor(curve, curPos_, gainA_, gainB_);
308 publishGains();
309 }
310
314 [[nodiscard]] T getGainA() const noexcept
315 {
316 return publishedGainA_.load(std::memory_order_relaxed);
317 }
318
322 [[nodiscard]] T getGainB() const noexcept
323 {
324 return publishedGainB_.load(std::memory_order_relaxed);
325 }
326
327private:
328 static constexpr int kChunk = 64;
329 static constexpr double kMinGlideSeconds = 0.02;
330
339 static inline void equalPowerGains(T t, T& gA, T& gB) noexcept
340 {
341 const bool upper = t > T(0.5);
342 const T u = upper ? T(1) - t : t;
343 const T x = halfPi<T> * u;
344 const T x2 = x * x;
345 const T s = x * (T(1) + x2 * (T(-1.0 / 6.0) + x2 * (T(1.0 / 120.0)
346 + x2 * (T(-1.0 / 5040.0) + x2 * T(1.0 / 362880.0)))));
347 const T c = T(1) + x2 * (T(-0.5) + x2 * (T(1.0 / 24.0) + x2 * (T(-1.0 / 720.0)
348 + x2 * (T(1.0 / 40320.0) + x2 * T(-1.0 / 3628800.0)))));
349 gB = upper ? c : s;
350 gA = upper ? s : c;
351 }
352
357 static void fillGains(Curve curve, const T* pos, T* gA, T* gB, int n) noexcept
358 {
359 switch (curve)
360 {
362 for (int i = 0; i < n; ++i)
363 equalPowerGains(pos[i], gA[i], gB[i]);
364 break;
365
366 case Curve::SCurve:
367 for (int i = 0; i < n; ++i)
368 {
369 const T t = pos[i] * pos[i] * (T(3) - T(2) * pos[i]);
370 gA[i] = T(1) - t;
371 gB[i] = t;
372 }
373 break;
374
375 case Curve::Linear:
376 default:
377 // default: unreachable through the clamped setter; keeps the
378 // outputs defined if the enum ever grows without a case.
379 for (int i = 0; i < n; ++i)
380 {
381 gA[i] = T(1) - pos[i];
382 gB[i] = pos[i];
383 }
384 break;
385 }
386 }
387
389 void settle() noexcept
390 {
391 curPos_ = position_.load(std::memory_order_relaxed);
392 lastCurve_ = curve_.load(std::memory_order_relaxed);
393 gainsFor(lastCurve_, curPos_, gainA_, gainB_);
394 publishGains();
395 }
396
398 void settleIfFresh() noexcept
399 {
400 if (!needsSettle_) return;
401 needsSettle_ = false;
402 settle();
403 }
404
406 void publishGains() noexcept
407 {
408 publishedGainA_.store(gainA_, std::memory_order_relaxed);
409 publishedGainB_.store(gainB_, std::memory_order_relaxed);
410 }
411
412 // Communication: GUI -> Audio Thread
413 std::atomic<Curve> curve_ { Curve::EqualPower };
414 std::atomic<T> position_ { T(0) };
415
416 // Audio-thread state: the applied (gliding) position, its curve and gains.
417 // The working pair stays audio-thread-private; the getters read the
418 // published copies.
419 T curPos_ = T(0);
420 Curve lastCurve_ = Curve::EqualPower;
421 T gainA_ = T(1);
422 T gainB_ = T(0);
423 T maxPosStep_ = T(0);
424 bool needsSettle_ = true;
425 std::atomic<T> publishedGainA_ { T(1) };
426 std::atomic<T> publishedGainB_ { T(0) };
427};
428
429} // namespace dspark
Artifact-free, SIMD-friendly crossfader for two audio signals.
Definition Crossfade.h:63
void processAutomated(const T *inputA, const T *inputB, const T *positions, T *output, int numSamples) noexcept
Processes crossfading using a per-sample automation buffer.
Definition Crossfade.h:279
void prepare(double sampleRate) noexcept
Enables the time-based glide (optional).
Definition Crossfade.h:87
void setPosition(T position) noexcept
Sets the target crossfade blend position.
Definition Crossfade.h:133
static void gainsFor(Curve curve, T position, T &gainA, T &gainB) noexcept
The gains a curve applies at a position.
Definition Crossfade.h:159
Curve
Defines the amplitude response of the crossfade transition.
Definition Crossfade.h:67
@ Linear
Linear interpolation. Constant amplitude, drops power at center.
@ EqualPower
Sine/cosine law. Constant power, no volume drop, smooth at both ends.
@ SCurve
Smoothstep interpolation. Slower progression at extremes.
T process(T a, T b) noexcept
Crossfades two individual samples.
Definition Crossfade.h:178
Crossfade()=default
T getGainB() const noexcept
Gets the current internal gain multiplier for signal B. Safe from any thread: loads a published atomi...
Definition Crossfade.h:322
T getPosition() const noexcept
Retrieves the last requested position.
Definition Crossfade.h:143
T getGainA() const noexcept
Gets the current internal gain multiplier for signal A. Safe from any thread: loads a published atomi...
Definition Crossfade.h:314
void setCurve(Curve curve) noexcept
Sets the crossfade curve type. Thread-safe. Can be called from the GUI thread. A curve change is blen...
Definition Crossfade.h:118
void reset() noexcept
Lands the applied position and curve on the requested ones, with no glide (for example at transport s...
Definition Crossfade.h:105
void process(const T *inputA, const T *inputB, T *output, int numSamples) noexcept
Crossfades two audio buffers into an output buffer with automatic parameter smoothing.
Definition Crossfade.h:207
void prepare(const AudioSpec &spec) noexcept
Enables the time-based glide from an audio spec (see prepare(double)).
Definition Crossfade.h:98
~Crossfade()=default
Main namespace for the DSPark framework.
T moveTowards(T from, T to, T maxDelta) noexcept
Moves a value toward a target by at most a given distance.
Definition DspMath.h:134
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37