Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
early_reflections.hpp
Go to the documentation of this file.
1#pragma once
2
5
10
11#include <algorithm>
12#include <array>
13#include <cmath>
14#include <cstddef>
15#include <limits>
16#include <new>
17#include <span>
18#include <stdexcept>
19#include <type_traits>
20#include <utility>
21
22namespace pulp::signal {
23
31template <typename SampleType = float> class EarlyReflectionsT {
32 public:
33 static_assert(std::is_floating_point_v<SampleType>);
34
35 static constexpr std::size_t kMaxTaps = 16;
36 static constexpr double kMinimumSampleRate = 1000.0;
37 static constexpr double kMaximumSampleRate = 768000.0;
38 static constexpr double kMaximumDelayMs = 1000.0;
39
40 struct Tap {
41 double delay_ms = 20.0;
42 double gain = 1.0;
44 double pan = 0.0;
47 double stereo_width = 1.0;
48 };
49
50 [[nodiscard]] bool prepare(double sample_rate, double maximum_delay_ms = 250.0) {
51 if (!valid_prepare_request(sample_rate, maximum_delay_ms) ||
52 !taps_fit(taps_, tap_count_, sample_rate, maximum_delay_ms))
53 return false;
54
55 const double required = std::ceil(sample_rate * maximum_delay_ms / 1000.0) + 1.0;
56 if (required > static_cast<double>(std::numeric_limits<int>::max() - 1))
57 return false;
58
59 DelayLineT<SampleType> replacement_left;
60 DelayLineT<SampleType> replacement_right;
61#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
62 try {
63#endif
64 replacement_left.prepare(static_cast<int>(required));
65 replacement_right.prepare(static_cast<int>(required));
66#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
67 } catch (const std::bad_alloc&) {
68 return false;
69 } catch (const std::length_error&) {
70 return false;
71 }
72#endif
73
74 auto replacement_derived = derive_taps(taps_, tap_count_, sample_rate);
75 apply_headroom(replacement_derived, tap_count_, headroom_policy_);
76 left_history_ = std::move(replacement_left);
77 right_history_ = std::move(replacement_right);
78 derived_ = replacement_derived;
79 sample_rate_ = sample_rate;
80 maximum_delay_ms_ = maximum_delay_ms;
81 prepared_ = true;
82 reset();
83 return true;
84 }
85
88 [[nodiscard]] bool
89 configure(std::span<const Tap> taps,
91 if (taps.size() > kMaxTaps)
92 return false;
93
94 std::array<Tap, kMaxTaps> replacement{};
95 for (std::size_t i = 0; i < taps.size(); ++i) {
96 if (!valid_tap(taps[i]))
97 return false;
98 replacement[i] = taps[i];
99 }
100 if (prepared_ && !taps_fit(replacement, taps.size(), sample_rate_, maximum_delay_ms_))
101 return false;
102
103 auto replacement_derived = derive_taps(replacement, taps.size(), sample_rate_);
104 apply_headroom(replacement_derived, taps.size(), policy);
105 taps_ = replacement;
106 derived_ = replacement_derived;
107 tap_count_ = taps.size();
108 headroom_policy_ = policy;
109 return true;
110 }
111
112 bool prepared() const noexcept {
113 return prepared_;
114 }
115 double sample_rate() const noexcept {
116 return sample_rate_;
117 }
118 double maximum_delay_ms() const noexcept {
119 return maximum_delay_ms_;
120 }
121 std::size_t tap_count() const noexcept {
122 return tap_count_;
123 }
124 Tap tap(std::size_t index) const noexcept {
125 return index < tap_count_ ? taps_[index] : Tap{};
126 }
128 return headroom_policy_;
129 }
130 std::size_t retained_bytes() const noexcept {
131 if (!prepared_)
132 return 0;
133 const auto samples = static_cast<std::size_t>(left_history_.max_delay() + 1);
134 return 2u * samples * sizeof(SampleType);
135 }
136
138 static constexpr int latency_samples() noexcept {
139 return 0;
140 }
141
143 int tail_samples() const noexcept {
144 if (!prepared_)
145 return 0;
146 double latest = 0.0;
147 for (std::size_t i = 0; i < tap_count_; ++i) {
148 const auto& route = derived_[i];
149 if (route.left_to_left != 0.0 || route.left_to_right != 0.0 ||
150 route.right_to_left != 0.0 || route.right_to_right != 0.0)
151 latest = std::max(latest, route.delay_samples);
152 }
153 return static_cast<int>(std::ceil(latest));
154 }
155
157 void reset() noexcept {
158 left_history_.discard_history();
159 right_history_.discard_history();
160 }
161
162 void process_sample(SampleType input_left, SampleType input_right, SampleType& output_left,
163 SampleType& output_right) noexcept {
164 output_left = SampleType{};
165 output_right = SampleType{};
166 if (!prepared_)
167 return;
168 if (!std::isfinite(static_cast<double>(input_left)) ||
169 !std::isfinite(static_cast<double>(input_right))) {
170 reset();
171 return;
172 }
173
174 long double wet_left = 0.0L;
175 long double wet_right = 0.0L;
176 for (std::size_t i = 0; i < tap_count_; ++i) {
177 const auto& route = derived_[i];
178 const auto history_delay = static_cast<SampleType>(route.delay_samples - 1.0);
179 const auto delayed_left = left_history_.read(history_delay);
180 const auto delayed_right = right_history_.read(history_delay);
181 wet_left += static_cast<long double>(route.left_to_left) * delayed_left +
182 static_cast<long double>(route.right_to_left) * delayed_right;
183 wet_right += static_cast<long double>(route.left_to_right) * delayed_left +
184 static_cast<long double>(route.right_to_right) * delayed_right;
185 }
186
187 const auto limit = static_cast<long double>(std::numeric_limits<SampleType>::max());
188 if (!std::isfinite(wet_left) || !std::isfinite(wet_right) || std::abs(wet_left) > limit ||
189 std::abs(wet_right) > limit) {
190 reset();
191 return;
192 }
193
194 output_left = snap_to_zero(static_cast<SampleType>(wet_left));
195 output_right = snap_to_zero(static_cast<SampleType>(wet_right));
196 left_history_.push(input_left);
197 right_history_.push(input_right);
198 }
199
200 void process_block(const SampleType* input_left, const SampleType* input_right,
201 SampleType* output_left, SampleType* output_right,
202 std::size_t sample_count) noexcept {
203 if (input_left == nullptr || input_right == nullptr || output_left == nullptr ||
204 output_right == nullptr)
205 return;
206 for (std::size_t i = 0; i < sample_count; ++i) {
207 const auto left = input_left[i];
208 const auto right = input_right[i];
209 process_sample(left, right, output_left[i], output_right[i]);
210 }
211 }
212
213 private:
214 struct DerivedTap {
215 double delay_samples = 1.0;
216 double left_to_left = 0.0;
217 double left_to_right = 0.0;
218 double right_to_left = 0.0;
219 double right_to_right = 0.0;
220 };
221
222 static bool valid_prepare_request(double sample_rate, double maximum_delay_ms) noexcept {
223 return std::isfinite(sample_rate) && sample_rate >= kMinimumSampleRate &&
224 sample_rate <= kMaximumSampleRate && std::isfinite(maximum_delay_ms) &&
226 }
227
228 static bool valid_tap(const Tap& tap) noexcept {
229 return std::isfinite(tap.delay_ms) && tap.delay_ms > 0.0 &&
230 tap.delay_ms <= kMaximumDelayMs && std::isfinite(tap.gain) &&
231 std::abs(tap.gain) <= 1.0 && std::isfinite(tap.pan) && tap.pan >= -1.0 &&
232 tap.pan <= 1.0 && std::isfinite(tap.stereo_width) && tap.stereo_width >= 0.0 &&
233 tap.stereo_width <= 1.0;
234 }
235
236 static bool taps_fit(const std::array<Tap, kMaxTaps>& taps, std::size_t count,
237 double sample_rate, double maximum_delay_ms) noexcept {
238 const double minimum_delay_ms = 1000.0 / sample_rate;
239 for (std::size_t i = 0; i < count; ++i)
240 if (!valid_tap(taps[i]) || taps[i].delay_ms < minimum_delay_ms ||
241 taps[i].delay_ms > maximum_delay_ms)
242 return false;
243 return true;
244 }
245
246 static double canonical_delay_samples(double delay_ms, double sample_rate) noexcept {
247 const double reconstructed = delay_ms * sample_rate / 1000.0;
248 const double nearest_integer = std::round(reconstructed);
249 const double tolerance =
250 8.0 * std::numeric_limits<double>::epsilon() * std::max(1.0, std::abs(reconstructed));
251 return std::abs(reconstructed - nearest_integer) <= tolerance ? nearest_integer
252 : reconstructed;
253 }
254
255 static std::array<DerivedTap, kMaxTaps> derive_taps(const std::array<Tap, kMaxTaps>& taps,
256 std::size_t count,
257 double sample_rate) noexcept {
258 std::array<DerivedTap, kMaxTaps> result{};
259 for (std::size_t i = 0; i < count; ++i) {
260 const auto& tap = taps[i];
261 auto& route = result[i];
262 route.delay_samples = canonical_delay_samples(tap.delay_ms, sample_rate);
263
264 const double left_position = std::clamp(tap.pan - tap.stereo_width, -1.0, 1.0);
265 const double right_position = std::clamp(tap.pan + tap.stereo_width, -1.0, 1.0);
266 PannerT<double> left_panner;
267 PannerT<double> right_panner;
268 left_panner.set_pan(left_position);
269 right_panner.set_pan(right_position);
270 auto left = left_panner.process(tap.gain);
271 auto right = right_panner.process(tap.gain);
272 // Preserve exact channel identity at the endpoints even if a
273 // trigonometric panner implementation has endpoint residue.
274 if (left_position == -1.0)
275 left = {tap.gain, 0.0};
276 else if (left_position == 1.0)
277 left = {0.0, tap.gain};
278 if (right_position == -1.0)
279 right = {tap.gain, 0.0};
280 else if (right_position == 1.0)
281 right = {0.0, tap.gain};
282 route.left_to_left = left.left;
283 route.left_to_right = left.right;
284 route.right_to_left = right.left;
285 route.right_to_right = right.right;
286 }
287 return result;
288 }
289
290 static void apply_headroom(std::array<DerivedTap, kMaxTaps>& taps, std::size_t count,
291 MatrixHeadroomPolicy policy) noexcept {
292 if (policy == MatrixHeadroomPolicy::Raw)
293 return;
294 long double left_sum = 0.0L;
295 long double right_sum = 0.0L;
296 for (std::size_t i = 0; i < count; ++i) {
297 left_sum += std::abs(taps[i].left_to_left) + std::abs(taps[i].right_to_left);
298 right_sum += std::abs(taps[i].left_to_right) + std::abs(taps[i].right_to_right);
299 }
300 const double left_scale = static_cast<double>(1.0L / std::max(1.0L, left_sum));
301 const double right_scale = static_cast<double>(1.0L / std::max(1.0L, right_sum));
302 for (std::size_t i = 0; i < count; ++i) {
303 taps[i].left_to_left *= left_scale;
304 taps[i].right_to_left *= left_scale;
305 taps[i].left_to_right *= right_scale;
306 taps[i].right_to_right *= right_scale;
307 }
308 }
309
310 double sample_rate_ = 48000.0;
311 double maximum_delay_ms_ = 250.0;
312 std::size_t tap_count_ = 0;
314 bool prepared_ = false;
315 std::array<Tap, kMaxTaps> taps_{};
316 std::array<DerivedTap, kMaxTaps> derived_{};
317 DelayLineT<SampleType> left_history_{};
318 DelayLineT<SampleType> right_history_{};
319};
320
323
324} // namespace pulp::signal
Definition delay_line.hpp:13
void prepare(int max_delay_samples)
Definition delay_line.hpp:16
A wet-only early-reflection renderer with a caller-authored tap pattern.
Definition early_reflections.hpp:31
bool configure(std::span< const Tap > taps, MatrixHeadroomPolicy policy=MatrixHeadroomPolicy::NormalizePeak) noexcept
Atomically replaces the complete tap pattern.
Definition early_reflections.hpp:89
std::size_t retained_bytes() const noexcept
Definition early_reflections.hpp:130
double maximum_delay_ms() const noexcept
Definition early_reflections.hpp:118
void process_sample(SampleType input_left, SampleType input_right, SampleType &output_left, SampleType &output_right) noexcept
Definition early_reflections.hpp:162
static constexpr int latency_samples() noexcept
This wet-only feed-forward renderer never delays a direct path.
Definition early_reflections.hpp:138
bool prepare(double sample_rate, double maximum_delay_ms=250.0)
Definition early_reflections.hpp:50
std::size_t tap_count() const noexcept
Definition early_reflections.hpp:121
static constexpr double kMinimumSampleRate
Definition early_reflections.hpp:36
MatrixHeadroomPolicy headroom_policy() const noexcept
Definition early_reflections.hpp:127
int tail_samples() const noexcept
The last possibly non-zero linearly interpolated tap sample.
Definition early_reflections.hpp:143
Tap tap(std::size_t index) const noexcept
Definition early_reflections.hpp:124
static constexpr std::size_t kMaxTaps
Definition early_reflections.hpp:35
void process_block(const SampleType *input_left, const SampleType *input_right, SampleType *output_left, SampleType *output_right, std::size_t sample_count) noexcept
Definition early_reflections.hpp:200
static constexpr double kMaximumSampleRate
Definition early_reflections.hpp:37
static constexpr double kMaximumDelayMs
Definition early_reflections.hpp:38
double sample_rate() const noexcept
Definition early_reflections.hpp:115
void reset() noexcept
Logically clears both histories in constant time.
Definition early_reflections.hpp:157
bool prepared() const noexcept
Definition early_reflections.hpp:112
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
Definition additive_bank.hpp:218
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
MatrixHeadroomPolicy
Raw applies the signed matrix exactly.
Definition audio_matrix_mixer.hpp:30
@ Raw
Definition audio_matrix_mixer.hpp:30
@ NormalizePeak
Definition audio_matrix_mixer.hpp:30
EarlyReflectionsT< float > EarlyReflections
Definition early_reflections.hpp:321
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
EarlyReflectionsT< double > EarlyReflections64
Definition early_reflections.hpp:322
Definition early_reflections.hpp:40
double gain
Definition early_reflections.hpp:42
double stereo_width
Separation of its left/right sources, in [0, 1].
Definition early_reflections.hpp:47
double delay_ms
Definition early_reflections.hpp:41
double pan
Centre of the tap's stereo image, in [-1, 1].
Definition early_reflections.hpp:44