Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
wavefold_aa.hpp
Go to the documentation of this file.
1// sbl/dsp/comp/wavefold_aa.hpp — ADAA Wavefolder (Audio Stack — Compositions)
2//
3// Second-order antialiased wavefolder for real-time audio.
4// Reflects a signal at +/-1.0 boundaries, creating harmonics through folding
5// rather than filtering. The core West Coast synthesis technique — a sine
6// through a folder is the Buchla 259 sound.
7//
8// Second-order ADAA (Esqueda/Bilbao, DAFx 2016) integrates over the input
9// change twice, giving ~12 dB/octave alias rejection vs ~6 dB for first-order.
10// Cost is ~3-4x the naive version — negligible on M7.
11//
12// For the stateless mathematical fold without antialiasing, see Folder in dsp/folder.hpp.
13//
14// Depth is applied externally: folder.process(input * depth)
15// depth 1.0 = clean passthrough (for input in [-1,1])
16// depth 2.0 = first fold
17// depth 4.0+ = heavy folding, rich harmonics
18//
19// Usage:
20// sbl::dsp::comp::WavefoldAA folder;
21// folder.process(buf, frames, 4.0f); // constant depth
22// folder.process(buf, frames, 0.0f, depth_buf); // per-sample depth
23
24#ifndef SBL_DSP_COMP_WAVEFOLD_AA_HPP_
25#define SBL_DSP_COMP_WAVEFOLD_AA_HPP_
26
27#include <cmath>
28#include <cstdint>
29
31
32namespace sbl::dsp::comp {
33
34/// First antiderivative of the wavefold triangle-wave function (F1).
35///
36/// The fold function f(x) on its fundamental period [-1, 3):
37/// [-1, 1): f(x) = x (slope +1)
38/// [ 1, 3): f(x) = 2 - x (slope -1)
39///
40/// F1(x), continuous and periodic (period 4):
41/// [-1, 1): F1(x) = x²/2 + 1
42/// [ 1, 3): F1(x) = 2x - x²/2
43inline float wavefold_antideriv(float x) {
44 // Reduce to fundamental period [-1, 3)
45 float q = x + 1.0f;
46 q = q - 4.0f * floorf(q * 0.25f); // mod 4, [0, 4)
47 float xr = q - 1.0f; // [-1, 3)
48
49 if (xr < 1.0f) {
50 return xr * xr * 0.5f + 1.0f;
51 } else {
52 return 2.0f * xr - xr * xr * 0.5f;
53 }
54}
55
56/// Second antiderivative of the wavefold triangle-wave function (F2).
57///
58/// F2(x) = integral of F1(x) from 0 to x.
59/// On the fundamental period [-1, 3):
60/// [-1, 1): F2(x) = x³/6 + x
61/// [ 1, 3): F2(x) = x² - x³/6 + 1/3
62///
63/// F1 has nonzero mean (3/2) over one period, so F2 grows by 6 per period.
64/// For arbitrary x: F2(x) = F2_core(x_reduced) + 6 * periods.
65inline float wavefold_antideriv2(float x) {
66 float q = x + 1.0f;
67 float k = floorf(q * 0.25f); // Number of complete periods
68 q = q - 4.0f * k; // Reduce to [0, 4)
69 float xr = q - 1.0f; // [-1, 3)
70
71 float core;
72 if (xr < 1.0f) {
73 // integral of (x²/2 + 1) = x³/6 + x
74 core = xr * xr * xr * (1.0f / 6.0f) + xr;
75 } else {
76 // integral of (2x - x²/2) = x² - x³/6 + 1/3
77 core = xr * xr - xr * xr * xr * (1.0f / 6.0f) + (1.0f / 3.0f);
78 }
79
80 return core + 6.0f * k;
81}
82
83/// Second-order ADAA wavefolder for real-time audio.
84///
85/// Uses the integrated linear interpolation formulation (Bilbao et al.):
86///
87/// d1(a, b) = (F2(a) - F2(b)) / (a - b) when |a - b| > ε
88/// = F1(a) when |a - b| ≤ ε
89///
90/// y[n] = 2 * (d1(x[n], x[n-1]) - d1(x[n-1], x[n-2])) / (x[n] - x[n-2])
91///
92/// The outer denominator spans two samples (x[n] - x[n-2]) for centered
93/// differencing. When well-conditioned, this gives ~12 dB/octave alias
94/// rejection — double first-order.
95///
96/// Near signal peaks where dx is small, the outer double-division suffers
97/// catastrophic cancellation. The fallback is first-order ADAA (single
98/// division of F1), which stays numerically stable and still antialiases.
100public:
101 /// @note All public methods are ISR-safe — bounded computation, no I/O.
102
103 void reset() {
104 x1_ = 0.0f;
105 x2_ = 0.0f;
106 F2_x1_ = wavefold_antideriv2(0.0f);
107 F1_x1_ = wavefold_antideriv(0.0f);
108 D1_prev_ = wavefold_antideriv(0.0f); // d1(0, 0) → F1(0) = 1.0
109 }
110
111 /// Process a single sample (pre-multiplied by depth)
112 float process(float x) {
113 float F2_x = wavefold_antideriv2(x);
114 float F1_x = wavefold_antideriv(x);
115
116 // Inner: d1(x[n], x[n-1])
117 float dx1 = x - x1_;
118 float D1;
119 constexpr float EPS = 1e-5f;
120 if (fabsf(dx1) > EPS) {
121 D1 = (F2_x - F2_x1_) / dx1;
122 } else {
123 D1 = F1_x; // Limit: F2'(x) = F1(x)
124 }
125
126 // Outer: y = 2 * (D1[n] - D1[n-1]) / (x[n] - x[n-2])
127 // Use a larger threshold here because the double division amplifies
128 // float cancellation errors quadratically. When ill-conditioned,
129 // fall back to first-order ADAA which only divides once.
130 float dx2 = x - x2_;
131 float y;
132 constexpr float OUTER_EPS = 1e-2f;
133 if (fabsf(dx2) > OUTER_EPS) {
134 y = 2.0f * (D1 - D1_prev_) / dx2;
135 } else if (fabsf(dx1) > EPS) {
136 // First-order ADAA fallback: single division, always stable
137 y = (F1_x - F1_x1_) / dx1;
138 } else {
139 y = primitives::Folder::fold(x); // True degenerate: DC input
140 }
141
142 x2_ = x1_;
143 x1_ = x;
144 F2_x1_ = F2_x;
145 F1_x1_ = F1_x;
146 D1_prev_ = D1;
147 return y;
148 }
149
150 /// Process a block in-place with constant or per-sample fold depth
151 ///
152 /// @param buf Float audio buffer (modified in-place)
153 /// @param frames Number of samples
154 /// @param depth Constant fold depth (used when depth_buf is nullptr)
155 /// @param depth_buf Per-sample depth buffer (nullptr = use constant depth)
156 void process(float* buf, uint16_t frames, float depth,
157 const float* depth_buf = nullptr) {
158 if (depth_buf != nullptr) {
159 for (uint16_t i = 0; i < frames; ++i) {
160 buf[i] = process(buf[i] * depth_buf[i]);
161 }
162 } else {
163 for (uint16_t i = 0; i < frames; ++i) {
164 buf[i] = process(buf[i] * depth);
165 }
166 }
167 }
168
169private:
170 float x1_ = 0.0f; // x[n-1]
171 float x2_ = 0.0f; // x[n-2]
172 float F2_x1_ = wavefold_antideriv2(0.0f); // Cached F2(x[n-1])
173 float F1_x1_ = wavefold_antideriv(0.0f); // Cached F1(x[n-1])
174 float D1_prev_ = wavefold_antideriv(0.0f); // d1(x[n-1], x[n-2])
175};
176
177} // namespace sbl::dsp::comp
178
179#endif // SBL_DSP_COMP_WAVEFOLD_AA_HPP_
void process(float *buf, uint16_t frames, float depth, const float *depth_buf=nullptr)
float process(float x)
Process a single sample (pre-multiplied by depth)
static float fold(float x)
The fold function, available for math use (e.g., WavefoldAA antiderivatives)
Definition folder.hpp:34
Wavefold primitive.
Compositions: subcircuits of primitives.
float wavefold_antideriv(float x)
float wavefold_antideriv2(float x)