Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
cross_feedback_multitap_delay.hpp
Go to the documentation of this file.
1#pragma once
2
5
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
20namespace pulp::signal {
21
29template <typename SampleType = float> class CrossFeedbackMultitapDelayT {
30 public:
31 static_assert(std::is_floating_point_v<SampleType>);
32
33 static constexpr std::size_t kMaxTaps = 8;
34 static constexpr double kMinimumSampleRate = 1000.0;
35 static constexpr double kMaximumSampleRate = 768000.0;
36 static constexpr double kMaximumDelayMs = 10000.0;
37 static constexpr double kMaximumFeedbackGain = 0.95;
38
39 struct Tap {
40 double delay_ms = 250.0;
41 double level = 1.0;
42 double pan = 0.0;
43 double stereo_width = 1.0;
44 double feedback_weight = 1.0;
45 };
46
48 update_all_taps();
49 }
50
51 [[nodiscard]] bool prepare(double sample_rate, double maximum_delay_ms = 2000.0) {
52 if (!std::isfinite(sample_rate) || sample_rate < kMinimumSampleRate ||
53 sample_rate > kMaximumSampleRate || !std::isfinite(maximum_delay_ms) ||
54 maximum_delay_ms < 1000.0 / sample_rate ||
56 return false;
57
58 const double required = std::ceil(sample_rate * maximum_delay_ms / 1000.0) + 1.0;
59 if (required > static_cast<double>(std::numeric_limits<int>::max() - 1))
60 return false;
61 const int maximum_delay_samples = static_cast<int>(required);
62
63 DelayLineT<SampleType> replacement_left;
64 DelayLineT<SampleType> replacement_right;
65#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
66 try {
67#endif
68 replacement_left.prepare(maximum_delay_samples);
69 replacement_right.prepare(maximum_delay_samples);
70#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
71 } catch (const std::bad_alloc&) {
72 return false;
73 } catch (const std::length_error&) {
74 return false;
75 }
76#endif
77
78 left_history_ = std::move(replacement_left);
79 right_history_ = std::move(replacement_right);
80 sample_rate_ = sample_rate;
81 maximum_delay_ms_ = maximum_delay_ms;
82 prepared_ = true;
83 update_all_taps();
84 reset();
85 return true;
86 }
87
88 bool prepared() const noexcept {
89 return prepared_;
90 }
91 double sample_rate() const noexcept {
92 return sample_rate_;
93 }
94 double maximum_delay_ms() const noexcept {
95 return maximum_delay_ms_;
96 }
97 std::size_t retained_bytes() const noexcept {
98 if (!prepared_)
99 return 0;
100 const auto samples = static_cast<std::size_t>(left_history_.max_delay() + 1);
101 return 2u * samples * sizeof(SampleType);
102 }
103
104 void set_active_tap_count(std::size_t count) noexcept {
105 active_tap_count_ = std::min(count, kMaxTaps);
106 update_feedback_weights();
107 }
108 std::size_t active_tap_count() const noexcept {
109 return active_tap_count_;
110 }
111
112 [[nodiscard]] bool set_tap(std::size_t index, Tap tap) noexcept {
113 if (index >= kMaxTaps || !std::isfinite(tap.delay_ms) ||
114 !std::isfinite(tap.level) || !std::isfinite(tap.pan) ||
115 !std::isfinite(tap.stereo_width) || !std::isfinite(tap.feedback_weight))
116 return false;
117 if (prepared_)
118 tap.delay_ms = std::clamp(tap.delay_ms, minimum_delay_ms(), maximum_delay_ms_);
119 else
120 tap.delay_ms = std::clamp(tap.delay_ms, 1000.0 / kMaximumSampleRate,
122 tap.level = std::clamp(tap.level, -1.0, 1.0);
123 tap.pan = std::clamp(tap.pan, -1.0, 1.0);
124 tap.stereo_width = std::clamp(tap.stereo_width, 0.0, 1.0);
125 tap.feedback_weight = std::clamp(tap.feedback_weight, -1.0, 1.0);
126 taps_[index] = tap;
127 if (prepared_)
128 update_tap(index);
129 update_feedback_weights();
130 return true;
131 }
132
133 Tap tap(std::size_t index) const noexcept {
134 return index < kMaxTaps ? taps_[index] : Tap{};
135 }
136
137 void set_feedback_gain(double gain) noexcept {
138 if (std::isfinite(gain))
139 feedback_gain_ = std::clamp(gain, -kMaximumFeedbackGain, kMaximumFeedbackGain);
140 }
141 double feedback_gain() const noexcept {
142 return feedback_gain_;
143 }
144
147 void set_cross_feedback(double amount01) noexcept {
148 if (std::isfinite(amount01))
149 cross_feedback_ = std::clamp(amount01, 0.0, 1.0);
150 }
151 double cross_feedback() const noexcept {
152 return cross_feedback_;
153 }
154
155 double worst_case_feedback_gain() const noexcept {
156 return std::abs(feedback_gain_);
157 }
158
159 static constexpr int latency_samples() noexcept {
160 return 0;
161 }
162
163 int tail_samples() const noexcept {
164 if (!prepared_ || active_tap_count_ == 0)
165 return 0;
166 bool has_feedback_route = false;
167 bool has_audible_route = false;
168 double latest_delay = 0.0;
169 for (std::size_t i = 0; i < active_tap_count_; ++i) {
170 has_feedback_route |= derived_[i].normalized_feedback_weight != 0.0;
171 if (taps_[i].level != 0.0) {
172 has_audible_route = true;
173 latest_delay = std::max(latest_delay, derived_[i].delay_samples);
174 }
175 }
176 if (!has_audible_route)
177 return 0;
178 if (feedback_gain_ != 0.0 && has_feedback_route)
179 return -1;
180 return static_cast<int>(std::ceil(latest_delay));
181 }
182
183 void reset() noexcept {
184 left_history_.discard_history();
185 right_history_.discard_history();
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 (!std::isfinite(static_cast<double>(input_left)) ||
195 !std::isfinite(static_cast<double>(input_right))) {
196 reset();
197 return;
198 }
199
200 long double wet_left = 0.0L;
201 long double wet_right = 0.0L;
202 long double feedback_left = 0.0L;
203 long double feedback_right = 0.0L;
204
205 for (std::size_t i = 0; i < active_tap_count_; ++i) {
206 const auto& tap = taps_[i];
207 const auto& derived = derived_[i];
208 const auto history_delay = static_cast<SampleType>(derived.delay_samples - 1.0);
209 const SampleType delayed_left = left_history_.read(history_delay);
210 const SampleType delayed_right = right_history_.read(history_delay);
211
212 wet_left += static_cast<long double>(tap.level) *
213 (derived.left_to_left * delayed_left +
214 derived.right_to_left * delayed_right);
215 wet_right += static_cast<long double>(tap.level) *
216 (derived.left_to_right * delayed_left +
217 derived.right_to_right * delayed_right);
218 feedback_left += derived.normalized_feedback_weight * delayed_left;
219 feedback_right += derived.normalized_feedback_weight * delayed_right;
220 }
221
222 const long double self = 1.0L - static_cast<long double>(cross_feedback_);
223 const long double cross = static_cast<long double>(cross_feedback_);
224 const long double gain = static_cast<long double>(feedback_gain_);
225 const long double write_left =
226 static_cast<long double>(input_left) + gain * (self * feedback_left + cross * feedback_right);
227 const long double write_right =
228 static_cast<long double>(input_right) + gain * (self * feedback_right + cross * feedback_left);
229 const long double limit = static_cast<long double>(std::numeric_limits<SampleType>::max());
230
231 if (!std::isfinite(wet_left) || !std::isfinite(wet_right) ||
232 !std::isfinite(write_left) || !std::isfinite(write_right) ||
233 std::abs(wet_left) > limit || std::abs(wet_right) > limit ||
234 std::abs(write_left) > limit || std::abs(write_right) > limit) {
235 reset();
236 return;
237 }
238
239 output_left = snap_to_zero(static_cast<SampleType>(wet_left));
240 output_right = snap_to_zero(static_cast<SampleType>(wet_right));
241 left_history_.push(snap_to_zero(static_cast<SampleType>(write_left)));
242 right_history_.push(snap_to_zero(static_cast<SampleType>(write_right)));
243 }
244
245 void process_block(const SampleType* input_left, const SampleType* input_right,
246 SampleType* output_left, SampleType* output_right,
247 std::size_t sample_count) noexcept {
248 if (input_left == nullptr || input_right == nullptr || output_left == nullptr ||
249 output_right == nullptr)
250 return;
251 for (std::size_t i = 0; i < sample_count; ++i) {
252 const SampleType left = input_left[i];
253 const SampleType right = input_right[i];
254 process_sample(left, right, output_left[i], output_right[i]);
255 }
256 }
257
258 private:
259 struct DerivedTap {
260 double delay_samples = 12000.0;
261 double left_to_left = 1.0;
262 double left_to_right = 0.0;
263 double right_to_left = 0.0;
264 double right_to_right = 1.0;
265 double normalized_feedback_weight = 1.0;
266 };
267
268 double minimum_delay_ms() const noexcept {
269 return 1000.0 / sample_rate_;
270 }
271
272 void update_tap(std::size_t index) noexcept {
273 auto& tap = taps_[index];
274 auto& derived = derived_[index];
275 tap.delay_ms = std::clamp(tap.delay_ms, minimum_delay_ms(), maximum_delay_ms_);
276 // A sample-exact millisecond value can reconstruct a few ULPs to either
277 // side of its integer. Canonicalize only that representational fringe
278 // so delay indexing and tail metadata agree at exact-sample settings.
279 const double reconstructed = tap.delay_ms * sample_rate_ / 1000.0;
280 const double nearest_integer = std::round(reconstructed);
281 const double integer_tolerance =
282 8.0 * std::numeric_limits<double>::epsilon() *
283 std::max(1.0, std::abs(reconstructed));
284 const double canonical =
285 std::abs(reconstructed - nearest_integer) <= integer_tolerance
286 ? nearest_integer
287 : reconstructed;
288 derived.delay_samples = std::max(1.0, canonical);
289
290 const double left_position = std::clamp(tap.pan - tap.stereo_width, -1.0, 1.0);
291 const double right_position = std::clamp(tap.pan + tap.stereo_width, -1.0, 1.0);
292 const auto gains = [](double position) {
293 const double angle = (position + 1.0) * std::numbers::pi / 4.0;
294 return std::array<double, 2>{std::cos(angle), std::sin(angle)};
295 };
296 const auto left_gains = gains(left_position);
297 const auto right_gains = gains(right_position);
298 derived.left_to_left = left_gains[0];
299 derived.left_to_right = left_gains[1];
300 derived.right_to_left = right_gains[0];
301 derived.right_to_right = right_gains[1];
302 }
303
304 void update_feedback_weights() noexcept {
305 double sum = 0.0;
306 for (std::size_t i = 0; i < active_tap_count_; ++i)
307 sum += std::abs(taps_[i].feedback_weight);
308 const double denominator = std::max(1.0, sum);
309 for (std::size_t i = 0; i < kMaxTaps; ++i)
310 derived_[i].normalized_feedback_weight =
311 i < active_tap_count_ ? taps_[i].feedback_weight / denominator : 0.0;
312 }
313
314 void update_all_taps() noexcept {
315 for (std::size_t i = 0; i < kMaxTaps; ++i)
316 update_tap(i);
317 update_feedback_weights();
318 }
319
320 double sample_rate_ = 48000.0;
321 double maximum_delay_ms_ = 2000.0;
322 double feedback_gain_ = 0.0;
323 double cross_feedback_ = 1.0;
324 std::size_t active_tap_count_ = 1;
325 bool prepared_ = false;
326 std::array<Tap, kMaxTaps> taps_{};
327 std::array<DerivedTap, kMaxTaps> derived_{};
328 DelayLineT<SampleType> left_history_{};
329 DelayLineT<SampleType> right_history_{};
330};
331
334
335} // namespace pulp::signal
A wet-only stereo multitap delay with normalized tap feedback.
Definition cross_feedback_multitap_delay.hpp:29
static constexpr double kMaximumDelayMs
Definition cross_feedback_multitap_delay.hpp:36
double cross_feedback() const noexcept
Definition cross_feedback_multitap_delay.hpp:151
double sample_rate() const noexcept
Definition cross_feedback_multitap_delay.hpp:91
void reset() noexcept
Definition cross_feedback_multitap_delay.hpp:183
std::size_t retained_bytes() const noexcept
Definition cross_feedback_multitap_delay.hpp:97
bool prepare(double sample_rate, double maximum_delay_ms=2000.0)
Definition cross_feedback_multitap_delay.hpp:51
static constexpr double kMinimumSampleRate
Definition cross_feedback_multitap_delay.hpp:34
std::size_t active_tap_count() const noexcept
Definition cross_feedback_multitap_delay.hpp:108
void set_feedback_gain(double gain) noexcept
Definition cross_feedback_multitap_delay.hpp:137
static constexpr double kMaximumFeedbackGain
Definition cross_feedback_multitap_delay.hpp:37
bool prepared() const noexcept
Definition cross_feedback_multitap_delay.hpp:88
void process_sample(SampleType input_left, SampleType input_right, SampleType &output_left, SampleType &output_right) noexcept
Definition cross_feedback_multitap_delay.hpp:188
bool set_tap(std::size_t index, Tap tap) noexcept
Definition cross_feedback_multitap_delay.hpp:112
CrossFeedbackMultitapDelayT() noexcept
Definition cross_feedback_multitap_delay.hpp:47
double worst_case_feedback_gain() const noexcept
Definition cross_feedback_multitap_delay.hpp:155
Tap tap(std::size_t index) const noexcept
Definition cross_feedback_multitap_delay.hpp:133
static constexpr double kMaximumSampleRate
Definition cross_feedback_multitap_delay.hpp:35
static constexpr int latency_samples() noexcept
Definition cross_feedback_multitap_delay.hpp:159
void set_active_tap_count(std::size_t count) noexcept
Definition cross_feedback_multitap_delay.hpp:104
static constexpr std::size_t kMaxTaps
Definition cross_feedback_multitap_delay.hpp:33
int tail_samples() const noexcept
Definition cross_feedback_multitap_delay.hpp:163
double maximum_delay_ms() const noexcept
Definition cross_feedback_multitap_delay.hpp:94
void set_cross_feedback(double amount01) noexcept
Zero is independent same-channel feedback; one is pure cross-channel feedback.
Definition cross_feedback_multitap_delay.hpp:147
double feedback_gain() const noexcept
Definition cross_feedback_multitap_delay.hpp:141
void process_block(const SampleType *input_left, const SampleType *input_right, SampleType *output_left, SampleType *output_right, std::size_t sample_count) noexcept
Definition cross_feedback_multitap_delay.hpp:245
Definition delay_line.hpp:13
void prepare(int max_delay_samples)
Definition delay_line.hpp:16
Definition additive_bank.hpp:218
CrossFeedbackMultitapDelayT< float > CrossFeedbackMultitapDelay
Definition cross_feedback_multitap_delay.hpp:332
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ i
Definition formant_filter_bank.hpp:20
CrossFeedbackMultitapDelayT< double > CrossFeedbackMultitapDelay64
Definition cross_feedback_multitap_delay.hpp:333
float sum(const float *data, std::size_t count) noexcept
Definition cross_feedback_multitap_delay.hpp:39
double stereo_width
Definition cross_feedback_multitap_delay.hpp:43
double pan
Definition cross_feedback_multitap_delay.hpp:42
double feedback_weight
Definition cross_feedback_multitap_delay.hpp:44
double delay_ms
Definition cross_feedback_multitap_delay.hpp:40
double level
Definition cross_feedback_multitap_delay.hpp:41