33 static_assert(std::is_floating_point_v<SampleType>);
56 if (required >
static_cast<double>(std::numeric_limits<int>::max() - 1))
61#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
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&) {
69 }
catch (
const std::length_error&) {
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;
94 std::array<Tap, kMaxTaps> replacement{};
95 for (std::size_t
i = 0;
i < taps.size(); ++
i) {
96 if (!valid_tap(taps[
i]))
98 replacement[
i] = taps[
i];
100 if (prepared_ && !taps_fit(replacement, taps.size(), sample_rate_, maximum_delay_ms_))
103 auto replacement_derived = derive_taps(replacement, taps.size(), sample_rate_);
104 apply_headroom(replacement_derived, taps.size(), policy);
106 derived_ = replacement_derived;
107 tap_count_ = taps.size();
108 headroom_policy_ = policy;
119 return maximum_delay_ms_;
124 Tap
tap(std::size_t index)
const noexcept {
125 return index < tap_count_ ? taps_[index] : Tap{};
128 return headroom_policy_;
133 const auto samples =
static_cast<std::size_t
>(left_history_.max_delay() + 1);
134 return 2u * samples *
sizeof(SampleType);
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);
153 return static_cast<int>(std::ceil(latest));
158 left_history_.discard_history();
159 right_history_.discard_history();
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{};
168 if (!std::isfinite(
static_cast<double>(input_left)) ||
169 !std::isfinite(
static_cast<double>(input_right))) {
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;
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) {
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);
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)
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];
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;
228 static bool valid_tap(
const Tap&
tap)
noexcept {
236 static bool taps_fit(
const std::array<Tap, kMaxTaps>& taps, std::size_t count,
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 ||
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
255 static std::array<DerivedTap, kMaxTaps> derive_taps(
const std::array<Tap, kMaxTaps>& taps,
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);
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);
274 if (left_position == -1.0)
276 else if (left_position == 1.0)
278 if (right_position == -1.0)
280 else if (right_position == 1.0)
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;
290 static void apply_headroom(std::array<DerivedTap, kMaxTaps>& taps, std::size_t count,
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);
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;
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_{};
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