18template <
typename SampleType =
float>
28 max_block_ = std::max(1, max_block);
29 channels_ = std::clamp(channels, 1, 2);
34 predelay_.assign(2, std::vector<SampleType>(
static_cast<std::size_t
>(predelay_len_),
37 wet_scratch_.assign(2, std::vector<SampleType>(
38 static_cast<std::size_t
>(std::max(max_block_,
kHeadLenMax)),
43 2, std::vector<SampleType>(
44 static_cast<std::size_t
>(head_len_ - 1 + std::max(max_block_, head_len_)),
46 head_out_.assign(
static_cast<std::size_t
>(std::max(max_block_, head_len_)), SampleType{0});
47 hp_state_.assign(2, OnePole{});
48 lp_state_.assign(2, OnePole{});
66 double ir_sample_rate) {
67 if (!is_prepared_)
return false;
68 if (ir_channels ==
nullptr || ir_length <= 0)
return false;
69 if (ir_channel_count != 1 && ir_channel_count != 2 && ir_channel_count != 4)
74 for (
int c = 0;
c < ir_channel_count; ++
c) {
75 if (ir_channels[
c] ==
nullptr)
return false;
76 for (
int i = 0;
i < ir_length; ++
i)
77 if (!std::isfinite(
static_cast<double>(ir_channels[
c][
i])))
return false;
82 std::vector<std::vector<double>> work(
static_cast<std::size_t
>(ir_channel_count));
83 for (
int c = 0;
c < ir_channel_count; ++
c) {
84 auto& dst = work[
static_cast<std::size_t
>(
c)];
85 dst.resize(
static_cast<std::size_t
>(ir_length));
86 for (
int i = 0;
i < ir_length; ++
i)
87 dst[
static_cast<std::size_t
>(
i)] =
static_cast<double>(ir_channels[
c][
i]);
91 if (ir_sample_rate > 0.0 && std::abs(ir_sample_rate - sample_rate_) > 1
e-9)
92 for (
auto& channel : work)
93 channel = resample_energy_preserving(channel, ir_sample_rate, sample_rate_);
97 for (
auto& channel : work) {
98 if (channel.empty())
continue;
100 for (
double v : channel) mean += v;
101 mean /=
static_cast<double>(channel.size());
102 for (
double& v : channel) v -= mean;
109 normalize(work, ir_channel_count);
112 publish(work, ir_channel_count);
119 if (!std::isfinite(db))
return;
124 if (!std::isfinite(ms))
return;
126 const int limit = std::max(0, predelay_len_ - max_block_ - 1);
127 predelay_samples_ = std::clamp(
133 refresh_active_cells();
136 if (std::isfinite(percent)) wet_gain_ = std::clamp(percent, 0.0, 100.0) / 100.0;
139 if (std::isfinite(percent)) dry_gain_ = std::clamp(percent, 0.0, 100.0) / 100.0;
142 if (!std::isfinite(percent))
return;
146 if (!std::isfinite(hz))
return;
151 if (!std::isfinite(hz))
return;
160 if (!std::isfinite(db))
return;
164 if (!std::isfinite(ms))
return;
174 void process(
const SampleType*
const* in, SampleType*
const* out,
int n) {
184 for (
int c = 0;
c < channels_; ++
c)
185 for (
int i = 0;
i < n; ++
i)
186 out[
c][
i] =
static_cast<SampleType
>(dry_gain_) * finite_sample(in[
c][
i]);
197 static_cast<int>(counter_ %
static_cast<std::uint64_t
>(smallest_period_));
198 const int chunk = std::min(n - done, to_grid);
200 render_chunk(in, done, chunk);
201 mix_chunk(in, out, done, chunk);
203 counter_ +=
static_cast<std::uint64_t
>(chunk);
211 for (
auto& ring : history_) std::fill(ring.begin(), ring.end(), SampleType{0});
214 for (
auto& g : groups_) g.reset();
215 for (
auto& ring : predelay_) std::fill(ring.begin(), ring.end(), SampleType{0});
217 for (
auto& s : hp_state_) s = OnePole{};
218 for (
auto& s : lp_state_) s = OnePole{};
241 if (!loaded_)
return 0.0;
243 for (
int dst = 0; dst < 2; ++dst) {
245 for (
int cell = 0; cell < cell_count_; ++cell) {
246 const Cell& k = cells_[
static_cast<std::size_t
>(cell)];
247 if (cell_active(cell) && k.dst == dst) sum += k.l1;
249 worst = std::max(worst, sum);
258 int num_levels()
const {
return static_cast<int>(groups_.size()); }
260 return groups_[
static_cast<std::size_t
>(level)].block_len;
263 return groups_[
static_cast<std::size_t
>(level)].ir_start;
266 return groups_[
static_cast<std::size_t
>(level)].num_partitions;
272 const auto& g = groups_[
static_cast<std::size_t
>(level)];
273 return g.ir_start - g.block_len + 1;
278 return prepared_[
static_cast<std::size_t
>(channel)].data();
292 static SampleType finite_sample(SampleType sample)
noexcept {
293 return std::isfinite(
static_cast<double>(sample)) ? sample : SampleType{0};
296 using Group = detail::ZeroLatencyConvolverGroup<SampleType>;
297 using Cell = detail::ZeroLatencyConvolverCell<SampleType>;
298 using OnePole = detail::ZeroLatencyConvolverOnePole;
302 void render_chunk(
const SampleType*
const* in,
int offset,
int chunk) {
303 const std::size_t mask = history_mask_;
307 for (
int c = 0;
c < channels_; ++
c) {
308 SampleType* ring = history_[
static_cast<std::size_t
>(
c)].data();
309 auto& hp = hp_state_[
static_cast<std::size_t
>(
c)];
310 auto& lp = lp_state_[
static_cast<std::size_t
>(
c)];
311 for (
int i = 0;
i < chunk; ++
i) {
312 double x =
static_cast<double>(finite_sample(in[
c][offset +
i]));
322 ring[(history_write_ +
static_cast<std::size_t
>(
i)) & mask] =
323 static_cast<SampleType
>(x);
326 history_write_ = (history_write_ +
static_cast<std::size_t
>(chunk)) & mask;
328 for (
int c = 0;
c < 2; ++
c)
329 std::fill_n(wet_scratch_[
static_cast<std::size_t
>(
c)].begin(), chunk, SampleType{0});
337 const std::size_t taps =
static_cast<std::size_t
>(head_len_);
338 const std::size_t span = taps - 1 +
static_cast<std::size_t
>(chunk);
339 const std::size_t head_base = (history_write_ -
static_cast<std::size_t
>(chunk)) & mask;
340 const std::size_t window_start = (head_base - (taps - 1)) & mask;
341 const SampleType* window[2] = {
nullptr,
nullptr};
342 for (
int cell = 0; cell < cell_count_; ++cell) {
343 if (!cell_active(cell))
continue;
344 const Cell&
k = cells_[
static_cast<std::size_t
>(cell)];
345 const auto src =
static_cast<std::size_t
>(
k.src);
346 if (window[src] ==
nullptr) {
347 const SampleType*
ring = history_[src].data();
348 const std::size_t ring_len = mask + 1;
349 if (window_start + span <= ring_len) {
350 window[src] =
ring + window_start;
352 SampleType*
linear = head_window_[src].data();
353 const std::size_t
first = ring_len - window_start;
359 SampleType* wet = wet_scratch_[
static_cast<std::size_t
>(
k.dst)].data();
360 pulp::simd::correlate(window[src],
k.head_reversed.data(), head_out_.data(),
361 static_cast<std::size_t
>(chunk), taps);
362 pulp::simd::add(wet, head_out_.data(), wet,
static_cast<std::size_t
>(chunk));
364 block_cost_ +=
static_cast<long long>(active_cells_) * head_len_ * chunk * 2;
369 for (
auto& g : groups_) {
371 static_cast<int>(counter_ %
static_cast<std::uint64_t
>(
g.block_len));
372 for (
int cell = 0; cell < cell_count_; ++cell) {
373 if (!cell_active(cell))
continue;
374 const Cell&
k = cells_[
static_cast<std::size_t
>(cell)];
375 const SampleType* src =
g.emit[
static_cast<std::size_t
>(cell)].data() + phase;
376 SampleType* wet = wet_scratch_[
static_cast<std::size_t
>(
k.dst)].data();
377 for (
int i = 0;
i < chunk; ++
i) wet[
i] += src[
i];
380 block_cost_ +=
static_cast<long long>(groups_.size()) * active_cells_ * chunk;
383 void mix_chunk(
const SampleType*
const* in, SampleType*
const* out,
int offset,
int chunk) {
384 const SampleType gain =
static_cast<SampleType
>(wet_gain_ * ir_gain_lin_);
385 const SampleType dry =
static_cast<SampleType
>(dry_gain_);
386 const SampleType
width =
static_cast<SampleType
>(width_);
388 for (
int i = 0;
i < chunk; ++
i) {
392 SampleType
w[2] = {SampleType{0}, SampleType{0}};
393 const int write = (predelay_write_ +
i) % predelay_len_;
394 const int read = (
write - predelay_samples_ + predelay_len_) % predelay_len_;
395 for (
int c = 0;
c < channels_; ++
c) {
396 auto&
ring = predelay_[
static_cast<std::size_t
>(
c)];
398 wet_scratch_[
static_cast<std::size_t
>(
c)][
static_cast<std::size_t
>(
i)];
401 if (channels_ == 2 && width != SampleType{1}) {
402 const SampleType mid = (
w[0] +
w[1]) * SampleType{0.5};
403 const SampleType side = (
w[0] -
w[1]) * SampleType{0.5} *
width;
407 for (
int c = 0;
c < channels_; ++
c)
408 out[
c][offset +
i] = dry * finite_sample(in[
c][offset +
i]) + gain *
w[
c];
410 predelay_write_ = (predelay_write_ + chunk) % predelay_len_;
415 void advance_groups() {
416 for (
auto& g : groups_) {
417 const int period =
g.block_len;
418 const int phase =
static_cast<int>(counter_ %
static_cast<std::uint64_t
>(period));
421 const int elapsed = phase == 0 ? period : phase;
429 static_cast<long>((
static_cast<long long>(
g.total_cost) * elapsed) / period);
430 const long long previous_cost = block_cost_;
431 run_group_work(g, target -
g.granted);
434 g.granted +=
static_cast<long>(block_cost_ - previous_cost);
437 if (phase != 0)
continue;
447 run_group_work(g, std::max(1L,
g.total_cost -
g.granted));
450 for (
int cell = 0; cell < cell_count_; ++cell)
451 g.emit[
static_cast<std::size_t
>(cell)].swap(
452 g.ready[
static_cast<std::size_t
>(cell)]);
453 g.has_result =
false;
455 for (
auto&
e :
g.emit) std::fill(
e.begin(),
e.end(), SampleType{0});
467 void arm_group(Group& g) {
468 g.slot =
static_cast<int>((counter_ /
static_cast<std::uint64_t
>(
g.block_len)) %
469 static_cast<std::uint64_t
>(
g.num_partitions));
470 g.frame_base = (history_write_ -
static_cast<std::size_t
>(
g.fft_size)) & history_mask_;
485 void run_group_work(Group& g,
long budget) {
486 if (!
g.armed || budget <= 0)
return;
487 const int np =
g.num_partitions;
488 const int K =
g.fft_size;
489 const int L =
g.block_len;
490 const int half = K / 2;
491 const bool packed_stereo = channels_ == 2 && (cell_count_ == 2 || cell_count_ == 4);
492 const int output_jobs = packed_stereo ? (cell_count_ == 2 ? 1 : 2) : cell_count_;
494 const auto finish = [&]()
noexcept { block_cost_ += spent; };
501 if (packed_stereo &&
g.job == 0) {
502 auto& packed =
g.accum[0];
503 if (
g.job_step == 0) {
507 if (
g.job_step == 1) {
510 static_cast<int>(std::min<long>((budget - spent) / unit, K -
g.job_cursor));
515 const auto*
left = history_[0].data();
516 const auto*
right = history_[1].data();
517 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i) {
518 const std::size_t
index =
519 (
g.frame_base +
static_cast<std::size_t
>(
i)) & history_mask_;
522 g.job_cursor += take;
523 spent +=
static_cast<long>(take) * unit;
524 if (
g.job_cursor >= K) {
525 g.fft.begin_forward_in_place();
529 if (spent >= budget ||
g.job_step == 1) {
534 if (
g.job_step == 2) {
535 spent +=
g.fft.advance(
g.plan, packed.data(), budget - spent);
543 if (
g.job_step == 3) {
544 constexpr long unpack_unit = 8;
545 const int take =
static_cast<int>(
546 std::min<long>((budget - spent) / unpack_unit, half + 1 -
g.job_cursor));
551 auto&
left =
g.spectra[
static_cast<std::size_t
>(
g.slot)];
552 auto&
right =
g.spectra[
static_cast<std::size_t
>(np +
g.slot)];
553 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i) {
554 if (
i == 0 ||
i == half) {
555 left[
static_cast<std::size_t
>(
i)] = {
556 packed[
static_cast<std::size_t
>(
i)].real(), 0};
557 right[
static_cast<std::size_t
>(
i)] = {
558 packed[
static_cast<std::size_t
>(
i)].imag(), 0};
560 const auto z = packed[
static_cast<std::size_t
>(
i)];
561 const auto mirror = std::conj(packed[
static_cast<std::size_t
>(K -
i)]);
564 left[
static_cast<std::size_t
>(
i)] = sum * SampleType{0.5};
565 right[
static_cast<std::size_t
>(
i)] = {diff.imag() * SampleType{0.5},
566 -diff.real() * SampleType{0.5}};
569 g.job_cursor += take;
570 spent +=
static_cast<long>(take) * unpack_unit;
571 if (
g.job_cursor >= half + 1) {
576 if (spent >= budget ||
g.job_step == 3) {
583 while (spent < budget) {
584 if (
g.job < channels_) {
586 auto& spec =
g.spectra[
static_cast<std::size_t
>(
g.job * np +
g.slot)];
587 if (
g.fft.done() &&
g.job_step == 0) {
588 g.fft.begin_forward_from_ring(history_[
static_cast<std::size_t
>(
g.job)].data(),
589 g.frame_base, history_mask_);
592 spent +=
g.fft.advance(
g.plan, spec.data(), budget - spent);
593 if (!
g.fft.done())
break;
600 const int output_job =
g.job - channels_;
601 if (output_job >= output_jobs) {
610 packed_stereo ? (cell_count_ == 2 ? 0 : (output_job == 0 ? 0 : 3)) : output_job;
611 if (!cell_active(cell)) {
612 std::fill(
g.ready[
static_cast<std::size_t
>(cell)].begin(),
613 g.ready[
static_cast<std::size_t
>(cell)].end(), SampleType{0});
620 auto& acc =
g.accum[
static_cast<std::size_t
>(cell)];
621 if (
g.job_step == 0) {
630 const int remaining =
half + 1 -
g.job_cursor;
631 const int take =
static_cast<int>(
632 std::min<long>((budget - spent) / unit, remaining));
633 if (take <= 0)
break;
634 if (!packed_stereo) {
635 for (
int p = 0;
p < np; ++
p) {
636 const int slot = (
g.slot -
p + np) % np;
637 const Cell& source = cells_[
static_cast<std::size_t
>(cell)];
638 const std::complex<SampleType>*
x =
639 g.spectra[
static_cast<std::size_t
>(source.src * np + slot)].data();
640 const std::complex<SampleType>*
h =
641 g.ir_spectra[
static_cast<std::size_t
>(cell * np +
p)].data();
642 std::complex<SampleType>*
a = acc.data();
644 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i)
647 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i)
654 const int cross_cell = cell_count_ == 2 ? 1 : (output_job == 0 ? 2 : 1);
655 auto& cross_acc =
g.accum[
static_cast<std::size_t
>(cross_cell)];
656 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i) {
657 acc[
static_cast<std::size_t
>(
i)] = {};
658 cross_acc[
static_cast<std::size_t
>(
i)] = {};
660 for (
int source_cell = 0; source_cell < cell_count_; ++source_cell) {
661 const Cell& source = cells_[
static_cast<std::size_t
>(source_cell)];
662 if (cell_count_ == 4 && source.dst != output_job)
664 for (
int p = 0;
p < np; ++
p) {
665 const int slot = (
g.slot -
p + np) % np;
666 const std::complex<SampleType>*
x =
667 g.spectra[
static_cast<std::size_t
>(source.src * np + slot)].data();
668 const std::complex<SampleType>*
h =
669 g.ir_spectra[
static_cast<std::size_t
>(source_cell * np +
p)].data();
670 auto*
a = acc.data();
671 auto*
b = cross_acc.data();
672 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i) {
673 if (source_cell == cross_cell)
681 g.job_cursor += take;
682 spent +=
static_cast<long>(take) * unit;
683 if (
g.job_cursor > half) {
685 g.job_cursor = packed_stereo ? 0 :
half + 1;
690 if (
g.job_step == 1) {
692 const int mirror_end = packed_stereo ?
half + 1 : K;
693 const long mirror_unit = packed_stereo ? 2L : 1L;
694 const int take =
static_cast<int>(
695 std::min<long>((budget - spent) / mirror_unit, mirror_end -
g.job_cursor));
696 if (take <= 0)
break;
697 std::complex<SampleType>*
a = acc.data();
699 const int cross_cell = cell_count_ == 2 ? 1 : (output_job == 0 ? 2 : 1);
700 const auto*
b =
g.accum[
static_cast<std::size_t
>(cross_cell)].data();
701 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i) {
702 const auto av =
a[
i];
703 const auto bv =
b[
i];
704 if (
i == 0 ||
i == half)
705 a[
i] = {av.real(), bv.real()};
707 a[
i] = {av.real() - bv.imag(), av.imag() + bv.real()};
708 if (
i > 0 &&
i < half)
709 a[K -
i] = {av.real() + bv.imag(), -av.imag() + bv.real()};
712 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i)
713 a[
i] = std::conj(
a[K -
i]);
715 g.job_cursor += take;
716 spent +=
static_cast<long>(take) * mirror_unit;
717 if (
g.job_cursor >= mirror_end) {
720 g.fft.begin_inverse_in_place();
725 if (
g.job_step == 2) {
726 spent +=
g.fft.advance(
g.plan, acc.data(), budget - spent);
727 if (!
g.fft.done())
break;
734 const long extract_unit = packed_stereo ? 2L : 1L;
736 static_cast<int>(std::min<long>((budget - spent) / extract_unit, L -
g.job_cursor));
737 if (take <= 0)
break;
738 SampleType* dst =
g.ready[
static_cast<std::size_t
>(cell)].data();
739 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i)
740 dst[
i] = acc[
static_cast<std::size_t
>(L +
i)].real();
742 const int cross_cell = cell_count_ == 2 ? 1 : (output_job == 0 ? 2 : 1);
743 SampleType* cross =
g.ready[
static_cast<std::size_t
>(cross_cell)].data();
744 for (
int i =
g.job_cursor;
i <
g.job_cursor + take; ++
i)
745 cross[
i] = acc[
static_cast<std::size_t
>(L +
i)].imag();
747 g.job_cursor += take;
748 spent +=
static_cast<long>(take) * extract_unit;
749 if (
g.job_cursor >= L) {
755 block_cost_ += spent;
773 std::vector<double> resample_energy_preserving(
const std::vector<double>& src,
775 double dst_rate)
const {
776 if (src.empty())
return src;
777 constexpr double pi = 3.14159265358979323846;
778 const double ratio = dst_rate / src_rate;
779 const int src_len =
static_cast<int>(src.size());
780 const int dst_len = std::max(1,
static_cast<int>(std::ceil(src_len *
ratio)));
783 const double fc = std::min(1.0,
ratio);
786 const double half = 0.5 * resample_taps_ / fc;
787 const int reach =
static_cast<int>(std::ceil(half)) + 1;
790 std::vector<double> dst(
static_cast<std::size_t
>(dst_len), 0.0);
791 for (
int j = 0;
j < dst_len; ++
j) {
792 const double centre =
static_cast<double>(
j) /
ratio;
793 const int i0 =
static_cast<int>(std::floor(centre));
795 for (
int i = i0 - reach;
i <= i0 + reach; ++
i) {
797 const double x = centre -
static_cast<double>(
i);
798 if (std::abs(x) > half)
continue;
799 const double t =
x /
half;
802 const double s = std::abs(x) < 1
e-12 ? fc : std::sin(pi * fc * x) / (
pi *
x);
803 sum += src[
static_cast<std::size_t
>(
i)] * s * w;
805 dst[
static_cast<std::size_t
>(
j)] = sum;
808 double e_before = 0.0, e_after = 0.0;
809 for (
double v : src) e_before +=
v *
v;
810 for (
double v : dst) e_after +=
v *
v;
812 const double correction = std::sqrt(e_before / e_after);
813 for (
double& v : dst)
v *= correction;
819 void trim_tail(std::vector<std::vector<double>>& work)
const {
820 constexpr double pi = 3.14159265358979323846;
821 if (work.empty() || work[0].empty())
return;
822 const std::size_t len = work[0].size();
827 std::vector<double> schroeder(len + 1, 0.0);
828 for (std::size_t
i = len;
i-- > 0;) {
830 for (
const auto& ch : work)
e += ch[
i] * ch[
i];
831 schroeder[
i] = schroeder[
i + 1] +
e;
835 const double floor_lin = std::pow(10.0, tail_trim_db_ / 10.0) * schroeder[0];
836 std::size_t keep = len;
837 for (std::size_t
i = 0;
i < len; ++
i)
838 if (schroeder[
i] < floor_lin) { keep =
i;
break; }
839 keep = std::max<std::size_t>(keep, 1);
841 const auto fade_target =
static_cast<std::size_t
>(
843 const std::size_t fade = std::min(keep, fade_target);
844 for (
auto& ch : work) {
846 for (std::size_t
i = 0;
i < fade; ++
i) {
849 const double t =
static_cast<double>(
i + 1) /
static_cast<double>(fade);
850 ch[keep - fade +
i] *= 0.5 * (1.0 + std::cos(pi * t));
855 void normalize(std::vector<std::vector<double>>& work,
int ir_channel_count)
const {
860 for (
const auto& ch : work)
861 for (
double v : ch)
peak = std::max(
peak, std::abs(v));
867 if (ir_channel_count == 1)
868 worst = energy(work[0]);
869 else if (ir_channel_count == 2)
870 worst = std::max(energy(work[0]), energy(work[1]));
872 worst = std::max(energy(work[0]) + energy(work[2]),
873 energy(work[1]) + energy(work[3]));
876 for (
auto& ch : work)
877 for (
double& v : ch)
v *=
scale;
880 static double energy(
const std::vector<double>& v) {
882 for (
double x : v)
e +=
x *
x;
886 void publish(
const std::vector<std::vector<double>>& work,
int ir_channel_count) {
887 ir_channels_ = ir_channel_count;
888 prepared_len_ =
static_cast<int>(work[0].size());
890 prepared_.assign(
static_cast<std::size_t
>(ir_channel_count), {});
891 for (
int c = 0;
c < ir_channel_count; ++
c) {
892 auto& dst = prepared_[
static_cast<std::size_t
>(
c)];
893 dst.resize(
static_cast<std::size_t
>(prepared_len_));
894 for (
int i = 0;
i < prepared_len_; ++
i)
895 dst[
static_cast<std::size_t
>(
i)] =
static_cast<SampleType
>(
896 work[
static_cast<std::size_t
>(
c)][
static_cast<std::size_t
>(
i)]);
899 build_cells(work, ir_channel_count);
904 smallest_period_ = head_len_;
905 if (prepared_len_ > head_len_) {
906 smallest_period_ = head_len_ / 2;
907 for (
int L = head_len_ / 2; 2 * L < prepared_len_; L *= 2)
908 groups_.push_back(build_group(L, work));
914 int ring = std::max(2 * head_len_, 4 * max_block_);
915 for (
const auto& g : groups_)
ring = std::max(ring, 4 *
g.block_len);
916 ring = round_up_pow2(ring);
917 history_.assign(2, std::vector<SampleType>(
static_cast<std::size_t
>(ring), SampleType{0}));
918 history_mask_ =
static_cast<std::size_t
>(
ring) - 1;
924 void build_cells(
const std::vector<std::vector<double>>& work,
int ir_channel_count) {
926 auto add = [&](
int src,
int dst,
int ir_channel) {
930 c.ir_channel = ir_channel;
931 const auto&
h = work[
static_cast<std::size_t
>(ir_channel)];
932 for (
double v : h)
c.l1 += std::abs(v);
933 c.head.assign(
static_cast<std::size_t
>(head_len_), SampleType{0});
934 const int n = std::min<int>(head_len_,
static_cast<int>(
h.size()));
935 for (
int i = 0;
i <
n; ++
i)
936 c.head[
static_cast<std::size_t
>(
i)] =
937 static_cast<SampleType
>(
h[
static_cast<std::size_t
>(
i)]);
938 c.head_reversed.assign(
c.head.rbegin(),
c.head.rend());
939 cells_.push_back(std::move(
c));
942 if (ir_channel_count == 1) {
944 if (channels_ == 2)
add(1, 1, 0);
945 }
else if (ir_channel_count == 2) {
947 if (channels_ == 2)
add(1, 1, 1);
952 if (channels_ == 2) {
957 cell_count_ =
static_cast<int>(cells_.size());
958 refresh_active_cells();
965 bool cell_active(
int cell)
const {
966 const Cell&
k = cells_[
static_cast<std::size_t
>(cell)];
967 return k.src ==
k.dst || true_stereo_;
970 void refresh_active_cells() {
972 for (
int c = 0;
c < cell_count_; ++
c)
973 if (cell_active(
c)) ++active_cells_;
976 Group build_group(
int L,
const std::vector<std::vector<double>>& work) {
981 g.num_partitions = (3 * L < prepared_len_) ? 2 : 1;
982 g.plan.prepare(
g.fft_size);
984 const int np =
g.num_partitions;
985 const int K =
g.fft_size;
986 g.spectra.assign(
static_cast<std::size_t
>(2 * np),
987 std::vector<std::complex<SampleType>>(
static_cast<std::size_t
>(K)));
988 g.ir_spectra.assign(
static_cast<std::size_t
>(cell_count_ * np),
989 std::vector<std::complex<SampleType>>(
static_cast<std::size_t
>(K)));
990 g.accum.assign(
static_cast<std::size_t
>(cell_count_),
991 std::vector<std::complex<SampleType>>(
static_cast<std::size_t
>(K)));
992 g.emit.assign(
static_cast<std::size_t
>(cell_count_),
993 std::vector<SampleType>(
static_cast<std::size_t
>(L), SampleType{0}));
994 g.ready.assign(
static_cast<std::size_t
>(cell_count_),
995 std::vector<SampleType>(
static_cast<std::size_t
>(L), SampleType{0}));
997 std::vector<double> segment(
static_cast<std::size_t
>(L), 0.0);
998 for (
int cell = 0; cell < cell_count_; ++cell) {
1000 work[
static_cast<std::size_t
>(cells_[
static_cast<std::size_t
>(cell)].ir_channel)];
1001 for (
int p = 0;
p < np; ++
p) {
1002 const int start =
g.ir_start +
p * L;
1003 std::fill(segment.begin(), segment.end(), 0.0);
1004 for (
int i = 0;
i < L; ++
i) {
1005 const int idx =
start +
i;
1006 if (idx <
static_cast<int>(
h.size()))
1007 segment[
static_cast<std::size_t
>(
i)] = h[
static_cast<std::size_t
>(idx)];
1009 g.plan.transform_real_padded(
1010 segment.data(), L,
g.ir_spectra[
static_cast<std::size_t
>(cell * np + p)].data());
1018 const bool packed_stereo = channels_ == 2 && (cell_count_ == 2 || cell_count_ == 4);
1019 if (packed_stereo) {
1020 constexpr long packed_unpack_cost = 8;
1023 g.plan.forward_cost() +
static_cast<long>(K / 2 + 1) * packed_unpack_cost;
1025 g.total_cost =
static_cast<long>(channels_) *
g.plan.forward_cost();
1028 static_cast<long>(cell_count_) *
1030 const int output_jobs = packed_stereo ? (cell_count_ == 2 ? 1 : 2) : cell_count_;
1031 const long mirror_cost = packed_stereo ? 2L * (K / 2 + 1) : K / 2 - 1;
1032 const long extract_cost = packed_stereo ? 2L * L : L;
1034 static_cast<long>(output_jobs) * (mirror_cost +
g.plan.inverse_cost() + extract_cost);
1040 void update_send_eq() {
1044 constexpr double two_pi = 6.283185307179586476925286766559;
1045 const double hp_w = two_pi * lowcut_hz_ / sample_rate_;
1046 hp_coef_ = 1.0 / (1.0 + hp_w);
1047 const double lp_w = two_pi * highcut_hz_ / sample_rate_;
1048 lp_coef_ = std::clamp(lp_w / (1.0 + lp_w), 0.0, 1.0);
1051 static int round_up_pow2(
int v) {
1052 if (v <= 1)
return 1;
1054 while (p < v)
p <<= 1;
1059 double sample_rate_ = 48000.0;
1060 int max_block_ = 512;
1062 int head_len_ = 128;
1063 bool is_prepared_ =
false;
1064 bool loaded_ =
false;
1066 std::vector<std::vector<SampleType>> history_;
1067 std::size_t history_mask_ = 0;
1068 std::size_t history_write_ = 0;
1069 std::uint64_t counter_ = 0;
1070 int smallest_period_ = 1;
1072 std::vector<Group> groups_;
1073 std::vector<Cell> cells_;
1074 int cell_count_ = 0;
1075 int active_cells_ = 0;
1077 std::vector<std::vector<SampleType>> prepared_;
1078 int prepared_len_ = 0;
1079 int ir_channels_ = 0;
1081 std::vector<std::vector<SampleType>> wet_scratch_;
1084 std::vector<std::vector<SampleType>> head_window_;
1085 std::vector<SampleType> head_out_;
1086 std::vector<std::vector<SampleType>> predelay_;
1087 int predelay_len_ = 1;
1088 int predelay_write_ = 0;
1089 int predelay_samples_ = 0;
1091 std::vector<OnePole> hp_state_;
1092 std::vector<OnePole> lp_state_;
1093 bool hp_active_ =
false;
1094 bool lp_active_ =
false;
1095 double hp_coef_ = 1.0;
1096 double lp_coef_ = 1.0;
1104 double ir_gain_lin_ = 1.0;
1111 bool true_stereo_ =
false;
1113 long long block_cost_ = 0;
Definition scoped_flush_denormals.hpp:106
See the file doc block for the schedule and its lineage.
Definition zero_latency_convolver.hpp:19
void set_tail_fade_ms(double ms)
Definition zero_latency_convolver.hpp:163
int prepared_ir_channels() const
Definition zero_latency_convolver.hpp:276
void set_resample_taps_per_phase(int taps)
Definition zero_latency_convolver.hpp:167
void set_predelay_ms(double ms)
Definition zero_latency_convolver.hpp:123
int prepared_ir_length() const
Definition zero_latency_convolver.hpp:275
void set_lowcut_hz(double hz)
Definition zero_latency_convolver.hpp:145
IrNormalizeMode normalize_mode() const
Definition zero_latency_convolver.hpp:285
int level_partitions(int level) const
Definition zero_latency_convolver.hpp:265
int latency_samples() const
The headline invariant.
Definition zero_latency_convolver.hpp:225
const SampleType * prepared_ir(int channel) const
Definition zero_latency_convolver.hpp:277
void set_true_stereo(bool on)
Definition zero_latency_convolver.hpp:131
double worst_case_gain() const
ir_gain_linear * ||h||_1, the exact l-infinity operator norm of the wet path: |y[n]| <= ||h||_1 * max...
Definition zero_latency_convolver.hpp:237
void set_highcut_hz(double hz)
Definition zero_latency_convolver.hpp:150
double l1_norm() const
||h||_1 under the current channel routing, without the IR trim.
Definition zero_latency_convolver.hpp:240
int num_levels() const
Definition zero_latency_convolver.hpp:258
double tail_trim_db() const
Definition zero_latency_convolver.hpp:286
double predelay_ms() const
Definition zero_latency_convolver.hpp:288
void process(const SampleType *const *in, SampleType *const *out, int n)
Convolve n <= max_block samples. Allocation-free and lock-free.
Definition zero_latency_convolver.hpp:174
int head_length() const
Definition zero_latency_convolver.hpp:257
void set_ir_gain_db(double db)
Definition zero_latency_convolver.hpp:118
void set_normalize_mode(IrNormalizeMode mode)
Load-time policy knobs.
Definition zero_latency_convolver.hpp:158
bool load_impulse_response(const SampleType *const *ir_channels, int ir_channel_count, int ir_length, double ir_sample_rate)
Ingest an impulse response.
Definition zero_latency_convolver.hpp:63
bool is_loaded() const
Definition zero_latency_convolver.hpp:256
void set_width_percent(double percent)
Definition zero_latency_convolver.hpp:141
int predelay_samples() const
Definition zero_latency_convolver.hpp:289
void reset()
Drop all convolution history. Allocation-free.
Definition zero_latency_convolver.hpp:210
double sample_rate() const
Definition zero_latency_convolver.hpp:284
int level_ir_start(int level) const
Definition zero_latency_convolver.hpp:262
void set_wet_percent(double percent)
Definition zero_latency_convolver.hpp:135
int level_block_length(int level) const
Definition zero_latency_convolver.hpp:259
void prepare(double sample_rate, int max_block, int channels)
Size the engine.
Definition zero_latency_convolver.hpp:25
int level_margin(int level) const
Scheduling margin of level i, in samples: the time between its frame closing and the first output it ...
Definition zero_latency_convolver.hpp:271
void set_tail_trim_db(double db)
Definition zero_latency_convolver.hpp:159
double tail_fade_ms() const
Definition zero_latency_convolver.hpp:287
long long last_block_cost() const
Estimated real flops performed by the most recent process() call.
Definition zero_latency_convolver.hpp:282
void set_dry_percent(double percent)
Definition zero_latency_convolver.hpp:138
static constexpr long kMacCost
Definition zero_latency_convolver_support.hpp:192
static constexpr long kGatherCost
Definition zero_latency_convolver_support.hpp:188
@ mirror
Definition canvas.hpp:169
@ b
Definition pitch.hpp:31
@ g
Definition pitch.hpp:27
@ index
Definition gpu_pass.hpp:140
@ ring
Definition string.hpp:18
@ width
Definition config.hpp:225
constexpr double pi
Definition fir_design.hpp:164
std::uint64_t read(std::span< const std::uint8_t > b, std::size_t p, unsigned n) noexcept
Definition spectral_frame_codec.hpp:111
void write(std::uint8_t *b, std::size_t p, std::uint64_t v, unsigned n) noexcept
Definition spectral_frame_codec.hpp:117
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
@ half
Definition units.hpp:290
T db_to_linear(T db)
Amplitude dB → linear gain. 0 dB → 1, −6.0206 dB → 0.5.
Definition units.hpp:71
Definition additive_bank.hpp:218
@ first
Definition dither.hpp:23
ZeroLatencyConvolverT< double > ZeroLatencyConvolver64
Definition zero_latency_convolver.hpp:1117
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ peak
Sample peak. Literal, fastest-responding.
Definition feedforward_compressor.hpp:119
@ c
Definition spectrum_trace.hpp:35
IrNormalizeMode
IR amplitude policy applied at load, after resampling and tail trim.
Definition zero_latency_convolver_support.hpp:157
@ energy
sum h^2 = 1 per output ear (default; convolution reverb).
Definition zero_latency_convolver_support.hpp:160
@ none
File passes through untouched (match / linear-phase EQ kernels).
Definition zero_latency_convolver_support.hpp:158
@ peak
max|h| = 1 (cabinet / mic-response IRs).
Definition zero_latency_convolver_support.hpp:159
float bessel_i0(float x)
Modified Bessel function of the first kind, order 0 Used in Kaiser window design.
Definition special_functions.hpp:25
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
ZeroLatencyConvolverT< float > ZeroLatencyConvolver
Definition zero_latency_convolver.hpp:1116
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
float sum(const float *data, std::size_t count) noexcept
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
void add(const float *a, const float *b, float *dst, std::size_t count) noexcept
@ s
Definition input_events.hpp:41
@ h
Definition input_events.hpp:39
@ j
Definition input_events.hpp:40
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ t
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
@ z
Definition input_events.hpp:42
@ start
Definition geometry.hpp:370
Zero-latency non-uniform partitioned convolution engine.
Definition zero_latency_convolver_support.hpp:422
static constexpr double kWidthPercentMax
Definition zero_latency_convolver_support.hpp:465
static constexpr double kWidthPercentMin
Definition zero_latency_convolver_support.hpp:464
static constexpr double kResampKaiserBeta
House Kaiser window shape, fixed by the series contract's oversampling / resampling law — not a tunab...
Definition zero_latency_convolver_support.hpp:522
static constexpr double kPredelayMsMin
Definition zero_latency_convolver_support.hpp:441
static constexpr double kTailTrimDbMin
Definition zero_latency_convolver_support.hpp:470
static constexpr int kResampTapsPerPhaseMax
Definition zero_latency_convolver_support.hpp:485
static constexpr double kHighcutHzDefault
Definition zero_latency_convolver_support.hpp:458
static constexpr double kTailFadeMsDefault
Raised-cosine fade applied to the last part of the kept IR so the truncation cannot click.
Definition zero_latency_convolver_support.hpp:475
static constexpr double kDryPercentDefault
Definition zero_latency_convolver_support.hpp:446
static constexpr double kTailTrimDbDefault
IR truncation floor on the Schroeder backward-energy curve.
Definition zero_latency_convolver_support.hpp:469
static constexpr double kLowcutHzMin
Definition zero_latency_convolver_support.hpp:456
static constexpr int kHeadLenMax
Definition zero_latency_convolver_support.hpp:431
static constexpr double kIrGainDbDefault
IR trim on the convolved path. [dp, default 0 dB, range -24..+24 dB].
Definition zero_latency_convolver_support.hpp:434
static constexpr double kTailTrimDbMax
Definition zero_latency_convolver_support.hpp:471
static constexpr double kHighcutHzMin
Definition zero_latency_convolver_support.hpp:459
static constexpr int kResampTapsPerPhaseMin
Definition zero_latency_convolver_support.hpp:484
static constexpr int kResampTapsPerPhaseDefault
Resampler kernel width, in taps per polyphase branch.
Definition zero_latency_convolver_support.hpp:483
static constexpr double kSilenceEpsilon
Below this, energies are treated as zero.
Definition zero_latency_convolver_support.hpp:536
static constexpr double kIrGainDbMin
Definition zero_latency_convolver_support.hpp:435
static constexpr double kPredelayMsMax
Definition zero_latency_convolver_support.hpp:442
static constexpr double kHighcutHzMax
Definition zero_latency_convolver_support.hpp:460
static constexpr double kLowcutHzMax
Definition zero_latency_convolver_support.hpp:457
static constexpr double kTailFadeMsMin
Definition zero_latency_convolver_support.hpp:476
static constexpr double kLowcutHzDefault
Send-EQ corners.
Definition zero_latency_convolver_support.hpp:455
static bool valid_resample_geometry(int source_length, double source_rate, double destination_rate, int taps_per_phase) noexcept
Proves every resampler geometry value is finite and representable before the ingest path casts it to ...
Definition zero_latency_convolver_support.hpp:491
static constexpr double kWidthPercentDefault
Mid/side width of the wet output. [dp, default 100 %, range 0..200 %].
Definition zero_latency_convolver_support.hpp:463
static constexpr double kIrGainDbMax
Definition zero_latency_convolver_support.hpp:436
static constexpr int kHeadLenMin
Definition zero_latency_convolver_support.hpp:430
static constexpr double kTailFadeMsMax
Definition zero_latency_convolver_support.hpp:477
static constexpr double kPredelayMsDefault
Pre-IR delay of the wet path.
Definition zero_latency_convolver_support.hpp:440
static constexpr double kWetPercentDefault
Wet / dry mix, in percent. [dp, defaults 100 % / 0 %, range 0..100 %].
Definition zero_latency_convolver_support.hpp:445