Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
diffusion_network.hpp
Go to the documentation of this file.
1#pragma once
2
5
7#include <pulp/signal/detail/schroeder_allpass.hpp>
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <cstddef>
13#include <limits>
14#include <new>
15#include <numbers>
16#include <stdexcept>
17#include <type_traits>
18#include <utility>
19#include <vector>
20
21namespace pulp::signal {
22
39template <typename SampleType = float> class DiffusionNetworkT {
40 public:
41 static_assert(std::is_floating_point_v<SampleType>);
42
43 static constexpr std::size_t kMaxStages = 8;
44 static constexpr double kMinimumSampleRate = 1000.0;
45 static constexpr double kMaximumSampleRate = 768000.0;
46 static constexpr double kMaximumDelayMs = 100.0;
47 // Across all eight stages, 0.02^8 / sqrt(2) remains above the shared
48 // 1e-15 snap threshold. Therefore every accepted non-zero coefficient
49 // contributes observable unit-impulse feed-through on at least one output.
50 static constexpr double kMinimumNonzeroAllpassGain = 0.02;
51 static constexpr double kMaximumAllpassGain = 0.95;
52 // sin(width*pi/4)^8 remains above the shared 1e-15 snap threshold, so
53 // latency-defining routes remain observable through the deepest cascade.
54 static constexpr double kMinimumNonzeroStereoWidth = 0.02;
55
56 struct StageConfig {
57 double delay_ms = 3.1;
58 double gain = 0.65;
59 };
60
61 struct Config {
62 std::size_t stage_count = 4;
63 std::array<StageConfig, kMaxStages> stages{{
64 {3.1, 0.65},
65 {4.7, 0.62},
66 {6.9, 0.59},
67 {10.1, 0.56},
68 {13.7, 0.53},
69 {17.3, 0.50},
70 {22.1, 0.47},
71 {29.9, 0.44},
72 }};
73
76 double stereo_width = 0.75;
77 };
78
79 [[nodiscard]] bool prepare(double sample_rate, double maximum_delay_ms = 50.0) {
80 if (!valid_prepare_arguments(sample_rate, maximum_delay_ms))
81 return false;
82
83 const double required = std::ceil(sample_rate * maximum_delay_ms / 1000.0) + 1.0;
84 if (required >
85 static_cast<double>(std::numeric_limits<std::size_t>::max() / (2u * kMaxStages)) ||
86 required > static_cast<double>(std::numeric_limits<int>::max()))
87 return false;
88 const auto capacity = static_cast<std::size_t>(required);
89
90 DerivedConfig replacement_derived;
91 if (!derive_config(config_, sample_rate, maximum_delay_ms, capacity, replacement_derived))
92 return false;
93
94 std::vector<SampleType> replacement;
95#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
96 try {
97#endif
98 replacement.assign(2u * kMaxStages * capacity, SampleType{});
99#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
100 } catch (const std::bad_alloc&) {
101 return false;
102 } catch (const std::length_error&) {
103 return false;
104 }
105#endif
106
107 storage_ = std::move(replacement);
108 sample_rate_ = sample_rate;
109 maximum_delay_ms_ = maximum_delay_ms;
110 capacity_ = capacity;
111 derived_ = replacement_derived;
112 prepared_ = true;
113 reset();
114 derived_.latency_samples = calculate_latency_samples();
115 reset();
116 return true;
117 }
118
119 [[nodiscard]] bool configure(const Config& candidate) noexcept {
120 if (!valid_config_shape(candidate))
121 return false;
122 if (!prepared_) {
123 config_ = candidate;
124 return true;
125 }
126 DerivedConfig replacement;
127 if (!derive_config(candidate, sample_rate_, maximum_delay_ms_, capacity_, replacement))
128 return false;
129 config_ = candidate;
130 derived_ = replacement;
131 reset();
132 derived_.latency_samples = calculate_latency_samples();
133 reset();
134 return true;
135 }
136
137 bool prepared() const noexcept {
138 return prepared_;
139 }
140 double sample_rate() const noexcept {
141 return sample_rate_;
142 }
143 double maximum_delay_ms() const noexcept {
144 return maximum_delay_ms_;
145 }
146 const Config& config() const noexcept {
147 return config_;
148 }
149
150 int stage_delay_samples(std::size_t stage, std::size_t channel) const noexcept {
151 if (stage >= config_.stage_count || channel >= 2u)
152 return 0;
153 return channel == 0u ? derived_.left_delay[stage] : derived_.right_delay[stage];
154 }
155
156 std::size_t retained_bytes() const noexcept {
157 return storage_.size() * sizeof(SampleType);
158 }
159
162 int latency_samples() const noexcept {
163 return prepared_ ? derived_.latency_samples : 0;
164 }
165
169 int tail_samples() const noexcept {
170 if (!prepared_ || config_.stage_count == 0u)
171 return 0;
172 int finite_tail = 0;
173 for (std::size_t i = 0; i < config_.stage_count; ++i) {
174 if (config_.stages[i].gain != 0.0)
175 return -1;
176 finite_tail += std::max(derived_.left_delay[i], derived_.right_delay[i]);
177 }
178 return finite_tail;
179 }
180
181 void reset() noexcept {
182 for (auto& positions : write_positions_)
183 positions = {0u, 0u};
184 for (auto& valid : valid_samples_)
185 valid = {0u, 0u};
186 }
187
188 void process_sample(SampleType input_left, SampleType input_right, SampleType& output_left,
189 SampleType& output_right) noexcept {
190 output_left = SampleType{};
191 output_right = SampleType{};
192 if (!prepared_)
193 return;
194 if (!finite(input_left) || !finite(input_right)) {
195 reset();
196 return;
197 }
198
199 SampleType left = input_left;
200 SampleType right = input_right;
201 for (std::size_t stage = 0; stage < config_.stage_count; ++stage) {
202 if (!process_stage(stage, left, right)) {
203 reset();
204 return;
205 }
206 }
207 output_left = snap_to_zero(left);
208 output_right = snap_to_zero(right);
209 }
210
211 void process_block(const SampleType* input_left, const SampleType* input_right,
212 SampleType* output_left, SampleType* output_right,
213 std::size_t sample_count) noexcept {
214 if (input_left == nullptr || input_right == nullptr || output_left == nullptr ||
215 output_right == nullptr)
216 return;
217 for (std::size_t i = 0; i < sample_count; ++i) {
218 const SampleType left = input_left[i];
219 const SampleType right = input_right[i];
220 process_sample(left, right, output_left[i], output_right[i]);
221 }
222 }
223
224 private:
225 struct DerivedConfig {
226 std::array<int, kMaxStages> left_delay{};
227 std::array<int, kMaxStages> right_delay{};
228 SampleType rotation_cos = SampleType{1};
229 SampleType rotation_sin = SampleType{};
230 int latency_samples = 0;
231 };
232
233 static constexpr std::array<int, kMaxStages> kRightOffsets{{3, 5, 7, 11, 13, 17, 19, 23}};
234
235 static bool valid_prepare_arguments(double sample_rate, double maximum_delay_ms) noexcept {
236 return std::isfinite(sample_rate) && sample_rate >= kMinimumSampleRate &&
237 sample_rate <= kMaximumSampleRate && std::isfinite(maximum_delay_ms) &&
239 }
240
241 static bool valid_config_shape(const Config& candidate) noexcept {
242 if (candidate.stage_count > kMaxStages || !std::isfinite(candidate.stereo_width) ||
243 candidate.stereo_width < 0.0 || candidate.stereo_width > 1.0 ||
244 (candidate.stereo_width != 0.0 &&
246 return false;
247 for (std::size_t i = 0; i < candidate.stage_count; ++i) {
248 const auto& stage = candidate.stages[i];
249 const double magnitude = std::abs(stage.gain);
250 if (!std::isfinite(stage.delay_ms) || stage.delay_ms <= 0.0 ||
251 stage.delay_ms > kMaximumDelayMs || !std::isfinite(stage.gain) ||
252 magnitude > kMaximumAllpassGain ||
253 (magnitude != 0.0 && magnitude < kMinimumNonzeroAllpassGain))
254 return false;
255 }
256 return true;
257 }
258
259 static bool derive_config(const Config& candidate, double sample_rate, double maximum_delay_ms,
260 std::size_t capacity, DerivedConfig& result) noexcept {
261 if (!valid_config_shape(candidate))
262 return false;
263 const int maximum_delay = static_cast<int>(capacity - 1u);
264 for (std::size_t i = 0; i < candidate.stage_count; ++i) {
265 const auto& stage = candidate.stages[i];
266 if (!std::isfinite(stage.delay_ms) || stage.delay_ms < 1000.0 / sample_rate ||
267 stage.delay_ms > maximum_delay_ms)
268 return false;
269 const int left =
270 std::clamp(static_cast<int>(std::llround(stage.delay_ms * sample_rate / 1000.0)), 1,
271 maximum_delay);
272 const int offset = kRightOffsets[i];
273 int right = left;
274 if (left + offset <= maximum_delay)
275 right = left + offset;
276 else if (left - offset >= 1)
277 right = left - offset;
278 result.left_delay[i] = left;
279 result.right_delay[i] = right;
280 }
281 const double angle = candidate.stereo_width * std::numbers::pi / 4.0;
282 result.rotation_cos = static_cast<SampleType>(std::cos(angle));
283 result.rotation_sin = static_cast<SampleType>(std::sin(angle));
284 return finite(result.rotation_cos) && finite(result.rotation_sin);
285 }
286
287 static bool finite(SampleType value) noexcept {
288 return std::isfinite(static_cast<double>(value));
289 }
290
291 int calculate_latency_samples() noexcept {
292 int direct_path_horizon = 0;
293 for (std::size_t stage = 0; stage < config_.stage_count; ++stage) {
294 if (config_.stages[stage].gain == 0.0) {
295 direct_path_horizon +=
296 std::max(derived_.left_delay[stage], derived_.right_delay[stage]);
297 }
298 }
299
300 int earliest = std::numeric_limits<int>::max();
301 for (std::size_t input_channel = 0; input_channel < 2u; ++input_channel) {
302 reset();
303 for (int sample = 0; sample <= direct_path_horizon; ++sample) {
304 SampleType left{};
305 SampleType right{};
306 const SampleType impulse = sample == 0 ? SampleType{1} : SampleType{};
307 process_sample(input_channel == 0u ? impulse : SampleType{},
308 input_channel == 1u ? impulse : SampleType{}, left, right);
309 if (left != SampleType{} || right != SampleType{}) {
310 earliest = std::min(earliest, sample);
311 break;
312 }
313 }
314 if (earliest == 0)
315 break;
316 }
317 return earliest == std::numeric_limits<int>::max() ? 0 : earliest;
318 }
319
320 std::size_t base_index(std::size_t stage, std::size_t channel) const noexcept {
321 return (2u * stage + channel) * capacity_;
322 }
323
324 SampleType delayed(std::size_t stage, std::size_t channel, int delay) const noexcept {
325 if (static_cast<std::size_t>(delay) > valid_samples_[stage][channel])
326 return SampleType{};
327 const auto position = write_positions_[stage][channel];
328 const auto read_position =
329 (position + capacity_ - static_cast<std::size_t>(delay)) % capacity_;
330 return storage_[base_index(stage, channel) + read_position];
331 }
332
333 bool process_stage(std::size_t stage, SampleType& left, SampleType& right) noexcept {
334 const SampleType delayed_left = delayed(stage, 0u, derived_.left_delay[stage]);
335 const SampleType delayed_right = delayed(stage, 1u, derived_.right_delay[stage]);
336 const long double gain = static_cast<long double>(config_.stages[stage].gain);
337 const long double write_left = detail::schroeder_allpass_write(
338 static_cast<long double>(left), static_cast<long double>(delayed_left), gain);
339 const long double write_right = detail::schroeder_allpass_write(
340 static_cast<long double>(right), static_cast<long double>(delayed_right), gain);
341 const long double allpass_left =
342 detail::schroeder_allpass_output(static_cast<long double>(delayed_left), write_left,
343 gain);
344 const long double allpass_right =
345 detail::schroeder_allpass_output(static_cast<long double>(delayed_right), write_right,
346 gain);
347 const long double limit = static_cast<long double>(std::numeric_limits<SampleType>::max());
348 if (!std::isfinite(write_left) || !std::isfinite(write_right) ||
349 !std::isfinite(allpass_left) || !std::isfinite(allpass_right) ||
350 std::abs(write_left) > limit || std::abs(write_right) > limit ||
351 std::abs(allpass_left) > limit || std::abs(allpass_right) > limit)
352 return false;
353
354 const auto left_position = write_positions_[stage][0];
355 const auto right_position = write_positions_[stage][1];
356 storage_[base_index(stage, 0u) + left_position] =
357 snap_to_zero(static_cast<SampleType>(write_left));
358 storage_[base_index(stage, 1u) + right_position] =
359 snap_to_zero(static_cast<SampleType>(write_right));
360 write_positions_[stage][0] = (left_position + 1u) % capacity_;
361 write_positions_[stage][1] = (right_position + 1u) % capacity_;
362 valid_samples_[stage][0] = std::min(valid_samples_[stage][0] + 1u, capacity_);
363 valid_samples_[stage][1] = std::min(valid_samples_[stage][1] + 1u, capacity_);
364
365 const long double c = static_cast<long double>(derived_.rotation_cos);
366 const long double s = static_cast<long double>(derived_.rotation_sin);
367 const long double sign = (stage & 1u) == 0u ? 1.0L : -1.0L;
368 const long double rotated_left = c * allpass_left - sign * s * allpass_right;
369 const long double rotated_right = sign * s * allpass_left + c * allpass_right;
370 if (!std::isfinite(rotated_left) || !std::isfinite(rotated_right) ||
371 std::abs(rotated_left) > limit || std::abs(rotated_right) > limit)
372 return false;
373 left = static_cast<SampleType>(rotated_left);
374 right = static_cast<SampleType>(rotated_right);
375 return true;
376 }
377
378 Config config_{};
379 DerivedConfig derived_{};
380 std::vector<SampleType> storage_{};
381 std::array<std::array<std::size_t, 2>, kMaxStages> write_positions_{};
382 std::array<std::array<std::size_t, 2>, kMaxStages> valid_samples_{};
383 double sample_rate_ = 48000.0;
384 double maximum_delay_ms_ = 50.0;
385 std::size_t capacity_ = 0u;
386 bool prepared_ = false;
387};
388
391
392} // namespace pulp::signal
A bounded cascade of stereo Schroeder allpasses with energy-preserving inter-channel coupling.
Definition diffusion_network.hpp:39
static constexpr double kMinimumNonzeroStereoWidth
Definition diffusion_network.hpp:54
double maximum_delay_ms() const noexcept
Definition diffusion_network.hpp:143
std::size_t retained_bytes() const noexcept
Definition diffusion_network.hpp:156
int stage_delay_samples(std::size_t stage, std::size_t channel) const noexcept
Definition diffusion_network.hpp:150
bool prepared() const noexcept
Definition diffusion_network.hpp:137
bool prepare(double sample_rate, double maximum_delay_ms=50.0)
Definition diffusion_network.hpp:79
static constexpr double kMaximumAllpassGain
Definition diffusion_network.hpp:51
static constexpr double kMaximumSampleRate
Definition diffusion_network.hpp:45
static constexpr double kMinimumNonzeroAllpassGain
Definition diffusion_network.hpp:50
const Config & config() const noexcept
Definition diffusion_network.hpp:146
void process_sample(SampleType input_left, SampleType input_right, SampleType &output_left, SampleType &output_right) noexcept
Definition diffusion_network.hpp:188
static constexpr double kMinimumSampleRate
Definition diffusion_network.hpp:44
void reset() noexcept
Definition diffusion_network.hpp:181
void process_block(const SampleType *input_left, const SampleType *input_right, SampleType *output_left, SampleType *output_right, std::size_t sample_count) noexcept
Definition diffusion_network.hpp:211
bool configure(const Config &candidate) noexcept
Definition diffusion_network.hpp:119
int tail_samples() const noexcept
-1 denotes the mathematical infinite tail of any recursive allpass.
Definition diffusion_network.hpp:169
static constexpr double kMaximumDelayMs
Definition diffusion_network.hpp:46
double sample_rate() const noexcept
Definition diffusion_network.hpp:140
static constexpr std::size_t kMaxStages
Definition diffusion_network.hpp:43
int latency_samples() const noexcept
Earliest non-zero response.
Definition diffusion_network.hpp:162
Definition additive_bank.hpp:218
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ c
Definition spectrum_trace.hpp:35
@ delay
Definition envelope.hpp:57
DiffusionNetworkT< float > DiffusionNetwork
Definition diffusion_network.hpp:389
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
@ i
Definition formant_filter_bank.hpp:20
DiffusionNetworkT< double > DiffusionNetwork64
Definition diffusion_network.hpp:390
@ s
Definition input_events.hpp:41
@ candidate
Definition waveform_editor_primitives.hpp:193
Definition diffusion_network.hpp:61
std::array< StageConfig, kMaxStages > stages
Definition diffusion_network.hpp:63
std::size_t stage_count
Definition diffusion_network.hpp:62
double stereo_width
Zero leaves two decorrelated mono cascades.
Definition diffusion_network.hpp:76
Definition diffusion_network.hpp:56
double delay_ms
Definition diffusion_network.hpp:57
double gain
Definition diffusion_network.hpp:58