Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
diffusion.hpp
Go to the documentation of this file.
1#pragma once
2
3// Two-stage diffusion: a cascade of Schroeder allpasses on the input path, and
4// a second, gentler cascade inside the loop.
5//
6// The split is the point. The input cascade can be aggressive — it is heard
7// once, and short geometrically-shrinking stage delays smear the early energy
8// without smearing the onset. The in-loop cascade is re-applied on every pass,
9// so the same coefficient there compounds into metal; it runs at a markedly
10// lower g. The pre-cascade also alternates its coefficient's SIGN between the
11// left and right input, so the two rails decorrelate from the very first
12// reflection instead of relying on the tank to do it.
13//
14// Block form. A stage whose delay is at least the block length reads only
15// samples written before the block began, so the whole block runs along time
16// as one vectorizable pass over two disjoint spans of the stage's ring. The
17// per-sample and block forms compute the same expression per sample. The block
18// form takes finite, bounded input — its callers sanitize at their entry — and
19// with |g| < 1 an allpass maps bounded input to bounded output, so it snaps
20// the stored value against denormals and carries no per-sample finiteness
21// test.
22//
23// RT contract: prepare() allocates; process/reset allocate nothing.
24
28#include <pulp/signal/detail/simd_lanes.hpp>
31
32#include <algorithm>
33#include <array>
34#include <cmath>
35#include <limits>
36#include <type_traits>
37
38namespace pulp::signal::fdn {
39
40namespace lanes = signal::detail::lanes;
41
42// One Schroeder allpass: y[n] = -g*x[n] + x[n-d] + g*y[n-d], in the
43// single-buffer form (one delay, one state).
44template <typename SampleType = float>
46 public:
47 void prepare(int max_delay_samples, std::size_t stagger = 0) {
48 line_.prepare(max_delay_samples, stagger);
49 }
50 void reset() { line_.reset(); }
51
52 void set_delay(int samples) {
53 delay_ = std::clamp(samples, 2, line_.max_delay());
54 }
55 int delay() const { return delay_; }
57 return line_;
58 }
59
60 SampleType process(SampleType x, SampleType g) {
61 x = finite_or_zero(x);
62 const SampleType delayed = finite_or_zero(line_.read(delay_));
63 const SampleType w =
64 finite_or_zero(snap_to_zero(static_cast<SampleType>(x + g * delayed)));
65 line_.push(w);
66 return finite_or_zero(static_cast<SampleType>(delayed - g * w));
67 }
68
69 // Fractional-delay path, used only when flutter is engaged. Kept separate
70 // from process() so the common case stays on the integer read.
71 SampleType process(SampleType x, SampleType g, double fractional_delay) {
72 x = finite_or_zero(x);
73 const SampleType delayed = finite_or_zero(line_.read(fractional_delay));
74 const SampleType w =
75 finite_or_zero(snap_to_zero(static_cast<SampleType>(x + g * delayed)));
76 line_.push(w);
77 return finite_or_zero(static_cast<SampleType>(delayed - g * w));
78 }
79
80 // Block of up to kMaxBlockSamples, in place. Requires n <= delay(), which
81 // makes the delayed span and the span being written disjoint. Both spans
82 // are contiguous: the ring mirrors its start past its end.
83 void process_block(SampleType* x, int n, SampleType g) {
84 SampleType* buf = line_.data();
85 const std::size_t w = line_.write_index();
86 const std::size_t r = (w - static_cast<std::size_t>(delay_)) & line_.mask();
87 run(x, buf + r, buf + w, n, g);
88 line_.advance(n);
89 }
90
91 // The stage expression over one contiguous piece.
92 static void run(SampleType* __restrict x, const SampleType* __restrict delayed,
93 SampleType* __restrict stored, int n, SampleType g) {
94 for (int k = 0; k < n; ++k) {
95 const SampleType v = snap_to_zero(static_cast<SampleType>(x[k] + g * delayed[k]));
96 stored[k] = v;
97 x[k] = static_cast<SampleType>(delayed[k] - g * v);
98 }
99 }
100
101 // Fluttered block: per-sample fractional delays. Requires every delay to
102 // be at least n + 1 samples.
103 void process_block(SampleType* x, int n, SampleType g, const double* delays) {
104 SampleType stored[kMaxBlockSamples];
105 for (int k = 0; k < n; ++k) {
106 const SampleType delayed = line_.read_hermite_at(k, delays[k]);
107 stored[k] = snap_to_zero(static_cast<SampleType>(x[k] + g * delayed));
108 x[k] = static_cast<SampleType>(delayed - g * stored[k]);
109 }
110 line_.push_block(stored, n);
111 }
112
113private:
115 int delay_ = 2;
116};
117
118// A cascade of up to kMaxDiffusionStages allpasses whose delays shrink
119// geometrically from a base length.
120template <typename SampleType = float>
122 public:
123 // `max_base_ms` sizes the allocation for the longest base the engine can
124 // ask for at the highest tank rate; configure() only re-derives lengths.
125 //
126 // `stagger` offsets this cascade's rings within their allocations (see
127 // FracDelayT::prepare); each stage is offset one more step past it.
128 void prepare(double max_base_ms, double max_sample_rate, int num_stages,
129 std::size_t stagger = 0) {
130 num_stages_ = std::clamp(num_stages, 0, kMaxDiffusionStages);
131 double base = max_base_ms;
132 for (int s = 0; s < num_stages_; ++s) {
133 const int cap =
134 static_cast<int>(base * 0.001 * max_sample_rate) + kHermiteGuard;
135 stages_[s].prepare(cap, stagger + static_cast<std::size_t>(s) * kRingStaggerSamples);
136 base *= kDiffusionStageShrink;
137 }
138 }
139
140 // Re-derive the stage delays for a tank rate. `offset_samples` staggers one
141 // cascade against another so two channels never share a stage length.
142 void configure(double base_ms, double sample_rate, int offset_samples) {
143 double base = base_ms;
144 for (int s = 0; s < num_stages_; ++s) {
145 const int len =
146 static_cast<int>(base * 0.001 * sample_rate) + offset_samples;
147 stages_[s].set_delay(len);
148 base_delay_[static_cast<std::size_t>(s)] = static_cast<double>(len);
149 base *= kDiffusionStageShrink;
150 }
151 min_delay_ = std::numeric_limits<int>::max();
152 for (int s = 0; s < num_stages_; ++s)
153 min_delay_ = std::min(min_delay_, stages_[s].delay());
154 }
155
156 void reset() {
157 for (int s = 0; s < num_stages_; ++s) stages_[s].reset();
158 }
159
160 int num_stages() const { return num_stages_; }
162 return stages_[s];
163 }
164
165 // Total delay the cascade contributes to a loop it sits in. An allpass is
166 // lossless but not delay-free, and a decay law that ignores it computes the
167 // per-pass gain for the wrong loop length.
168 double total_delay() const {
169 double sum = 0.0;
170 for (int s = 0; s < num_stages_; ++s)
171 sum += base_delay_[static_cast<std::size_t>(s)];
172 return sum;
173 }
174
175 SampleType process(SampleType x, SampleType g) {
176 for (int s = 0; s < num_stages_; ++s) x = stages_[s].process(x, g);
177 return x;
178 }
179
180 // Shortest stage delay — the longest block process_block() may take.
181 int min_delay() const {
182 return min_delay_;
183 }
184
185 // Block forms of the two paths below. Same per-sample arithmetic; see the
186 // file note for the length bound.
187 // Any n <= kMaxBlockSamples; split so each piece fits every stage.
188 void process_block(SampleType* x, int n, SampleType g) {
189 const int piece_max = std::max(1, min_delay());
190 for (int offset = 0; offset < n; offset += piece_max) {
191 const int piece = std::min(piece_max, n - offset);
192 if (num_stages_ == 4) {
193 fused4(x + offset, piece, g);
194 } else {
195 for (int s = 0; s < num_stages_; ++s)
196 stages_[s].process_block(x + offset, piece, g);
197 }
198 }
199 }
200
201 void process_block_fluttered(SampleType* x, int n, SampleType g, const double* lfo,
202 double depth) {
203 double delays[kMaxBlockSamples];
204 for (int s = 0; s < num_stages_; ++s) {
205 const double d = base_delay_[static_cast<std::size_t>(s)];
206 for (int k = 0; k < n; ++k)
207 delays[k] = d * (1.0 + depth * lfo[k]);
208 stages_[s].process_block(x, n, g, delays);
209 }
210 }
211
212 // Flutter path: every stage delay becomes fractional and wobbles by
213 // `depth` (a fraction of its own length) around a shared LFO value.
214 SampleType process_fluttered(SampleType x, SampleType g, double lfo, double depth) {
215 for (int s = 0; s < num_stages_; ++s) {
216 const double d = base_delay_[static_cast<std::size_t>(s)];
217 x = stages_[s].process(x, g, d * (1.0 + depth * lfo));
218 }
219 return x;
220 }
221
222 private:
223 // The four-stage cascade (the engine's only shape) as one pass along
224 // time. Each stage is the same expression AllpassStage::run applies; only
225 // the loop is shared.
226 void fused4(SampleType* x, int n, SampleType g) {
227 SampleType* d[4];
228 SampleType* o[4];
229 spans4(n, d, o);
230 run_spans4(x, n, g, d, o);
231 commit4(n);
232 }
233
234 public:
235 // The four stages' delayed and stored spans for the next n samples (the
236 // rings mirror their starts, so every span is contiguous). With
237 // commit4(), lets a caller run many cascades through run_spans4() without
238 // per-cascade setup. Requires four stages and n <= min_delay().
239 bool spans4(int n, SampleType** delayed, SampleType** stored) {
240 if (num_stages_ != 4 || n > min_delay_)
241 return false;
242 for (int s = 0; s < 4; ++s) {
243 FracDelayT<SampleType>& line = stages_[s].line();
244 const std::size_t w = line.write_index();
245 const std::size_t r = (w - static_cast<std::size_t>(stages_[s].delay())) & line.mask();
246 delayed[s] = line.data() + r;
247 stored[s] = line.data() + w;
248 }
249 return true;
250 }
251
252 void commit4(int n) {
253 for (int s = 0; s < 4; ++s)
254 stages_[s].line().advance(n);
255 }
256
257 // Four stages over one contiguous piece: the expression
258 // AllpassStage::run applies, stage after stage, four samples at a time
259 // as explicit vectors (the compiler cannot prove the eight spans
260 // disjoint, and its runtime checks cost more than a short block saves),
261 // then a scalar tail.
262 static void run_spans4(SampleType* x, int n, SampleType g, SampleType* const* d,
263 SampleType* const* o) {
264 int k = 0;
265 if constexpr (std::is_same_v<SampleType, float>) {
266 const lanes::F4 gv = lanes::splat(g);
267 for (; k + 4 <= n; k += 4) {
268 lanes::F4 v = lanes::load(x + k);
269 for (int s = 0; s < 4; ++s) {
270 const lanes::F4 delayed = lanes::load(d[s] + k);
271 const lanes::F4 u = snap(v + gv * delayed);
272 lanes::store(o[s] + k, u);
273 v = delayed - gv * u;
274 }
275 lanes::store(x + k, v);
276 }
277 }
278 for (; k < n; ++k) {
279 SampleType v = x[k];
280 for (int s = 0; s < 4; ++s) {
281 const SampleType u = snap_to_zero(static_cast<SampleType>(v + g * d[s][k]));
282 o[s][k] = u;
283 v = static_cast<SampleType>(d[s][k] - g * u);
284 }
285 x[k] = v;
286 }
287 }
288
289 private:
290 // snap_to_zero on four lanes.
291 static lanes::F4 snap(lanes::F4 v) {
292 return lanes::select(lanes::less(lanes::abs(v), lanes::splat(snap_threshold<float>())),
293 lanes::splat(0.0f), v);
294 }
295
296 std::array<AllpassStage<SampleType>, kMaxDiffusionStages> stages_{};
297 std::array<double, kMaxDiffusionStages> base_delay_{};
298 int num_stages_ = 0;
299 int min_delay_ = 1;
300};
301
302} // namespace pulp::signal::fdn
Definition diffusion.hpp:121
AllpassStage< SampleType > & stage(int s)
Definition diffusion.hpp:161
int min_delay() const
Definition diffusion.hpp:181
SampleType process(SampleType x, SampleType g)
Definition diffusion.hpp:175
int num_stages() const
Definition diffusion.hpp:160
void commit4(int n)
Definition diffusion.hpp:252
SampleType process_fluttered(SampleType x, SampleType g, double lfo, double depth)
Definition diffusion.hpp:214
static void run_spans4(SampleType *x, int n, SampleType g, SampleType *const *d, SampleType *const *o)
Definition diffusion.hpp:262
void process_block(SampleType *x, int n, SampleType g)
Definition diffusion.hpp:188
double total_delay() const
Definition diffusion.hpp:168
void reset()
Definition diffusion.hpp:156
bool spans4(int n, SampleType **delayed, SampleType **stored)
Definition diffusion.hpp:239
void process_block_fluttered(SampleType *x, int n, SampleType g, const double *lfo, double depth)
Definition diffusion.hpp:201
void prepare(double max_base_ms, double max_sample_rate, int num_stages, std::size_t stagger=0)
Definition diffusion.hpp:128
void configure(double base_ms, double sample_rate, int offset_samples)
Definition diffusion.hpp:142
Definition diffusion.hpp:45
static void run(SampleType *__restrict x, const SampleType *__restrict delayed, SampleType *__restrict stored, int n, SampleType g)
Definition diffusion.hpp:92
int delay() const
Definition diffusion.hpp:55
void reset()
Definition diffusion.hpp:50
void process_block(SampleType *x, int n, SampleType g, const double *delays)
Definition diffusion.hpp:103
void process_block(SampleType *x, int n, SampleType g)
Definition diffusion.hpp:83
SampleType process(SampleType x, SampleType g)
Definition diffusion.hpp:60
void set_delay(int samples)
Definition diffusion.hpp:52
FracDelayT< SampleType > & line()
Definition diffusion.hpp:56
SampleType process(SampleType x, SampleType g, double fractional_delay)
Definition diffusion.hpp:71
void prepare(int max_delay_samples, std::size_t stagger=0)
Definition diffusion.hpp:47
Definition frac_delay.hpp:36
Definition config.hpp:15
T finite_or_zero(T value)
Definition sanitize.hpp:12
constexpr int kMaxDiffusionStages
Definition config.hpp:19
constexpr double kDiffusionStageShrink
Definition config.hpp:69
constexpr std::size_t kRingStaggerSamples
Definition config.hpp:199
constexpr int kHermiteGuard
Definition config.hpp:171
constexpr int kMaxBlockSamples
Definition config.hpp:190
constexpr T snap_threshold()
Threshold in absolute value below which a sample is treated as zero.
Definition denormal.hpp:36
@ delay
Definition envelope.hpp:57
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ u
Definition formant_filter_bank.hpp:20
@ o
Definition formant_filter_bank.hpp:20