84template <
typename SampleType>
106 if (!std::isfinite(sample_rate))
return;
107 sample_rate_ = std::max(sample_rate, 1000.0);
108 const auto line_capacity =
109 static_cast<std::size_t
>(
112 for (
auto& channel : channels_) {
113 channel.line.prepare(line_capacity);
114 channel.reverse.prepare(line_capacity);
115 channel.diffusion.prepare(sample_rate_);
116 channel.bbd.prepare(sample_rate_);
117 channel.vintage.prepare(sample_rate_);
118 channel.tape.prepare(sample_rate_);
119 channel.tape_physical.prepare(sample_rate_);
120 channel.instability.prepare(sample_rate_);
131 for (
auto* smoother : {&feedback_, &crossfeed_, &duck_, &freeze_, &character_,
137 configure_time_slew();
144 character_type_ = character;
145 configure_time_slew();
151 tape_speed_ips_ = std::clamp(finite_or(
static_cast<double>(ips), tape_speed_ips_),
160 time_ms_ = std::clamp(finite_or(
static_cast<double>(left_ms), time_ms_), 1.0,
168 const double candidate =
static_cast<double>(multiplier);
169 if (!std::isfinite(candidate))
return;
172 right_time_absolute_ =
false;
183 const double candidate =
static_cast<double>(right_ms);
184 if (!std::isfinite(candidate))
return;
187 right_time_absolute_ =
true;
195 return right_time_absolute_ ? right_time_ms_ : time_ms_ * time_offset_;
199 feedback_target_ = std::clamp(
200 finite_or(
static_cast<double>(
feedback), feedback_target_), 0.0,
206 std::clamp(finite_or(
static_cast<double>(crossfeed), crossfeed_target_), 0.0, 1.0);
211 std::clamp(finite_or(
static_cast<double>(amount), character_target_), 0.0, 1.0);
219 diffusion_amount_target_ =
220 std::clamp(finite_or(
static_cast<double>(amount), diffusion_amount_target_), 0.0, 1.0);
223 void set_mod(SampleType rate01, SampleType depth01) {
224 if (
const double candidate =
static_cast<double>(rate01); std::isfinite(candidate)) {
225 const double rate = std::clamp(candidate, 0.0, 1.0);
231 if (
const double candidate =
static_cast<double>(depth01); std::isfinite(candidate))
237 std::clamp(finite_or(
static_cast<double>(amount01), duck_target_), 0.0, 1.0);
257 const double next = std::clamp(finite_or(
static_cast<double>(hz), loop_low_cut_hz_),
259 if (next == loop_low_cut_hz_)
return;
260 loop_low_cut_hz_ = next;
265 const double next = std::clamp(finite_or(
static_cast<double>(q), loop_low_cut_q_),
267 if (next == loop_low_cut_q_)
return;
268 loop_low_cut_q_ = next;
273 const double next = std::clamp(finite_or(
static_cast<double>(hz), loop_high_cut_hz_),
275 if (next == loop_high_cut_hz_)
return;
276 loop_high_cut_hz_ = next;
281 const double next = std::clamp(finite_or(
static_cast<double>(q), loop_high_cut_q_),
283 if (next == loop_high_cut_q_)
return;
284 loop_high_cut_q_ = next;
304 for (
auto& channel : channels_) {
305 channel.line.reset();
306 channel.reverse.reset();
307 channel.diffusion.reset();
309 channel.vintage.reset();
310 channel.tape.reset();
311 channel.tape_physical.reset();
312 channel.instability.reset();
313 channel.loop_highpass.reset();
314 channel.loop_lowpass.reset();
315 channel.tone_high_pass.reset();
316 channel.tone_low_pass.reset();
319 feedback_.snap(feedback_target_);
320 crossfeed_.snap(crossfeed_target_);
321 duck_.snap(duck_target_);
322 character_.snap(character_target_);
323 diffusion_amount_.snap(diffusion_amount_target_);
324 freeze_.snap(freeze_on_ ? 1.0 : 0.0);
326 duck_follower_.reset(0.0);
328 duck_gain_step_ = 0.0;
331 bbd_flutter_phase_ = 0.0;
332 control_counter_ = 0;
334 time_slew_[0].snap(time_ms_);
336 update_control_rate();
341 if (
left ==
nullptr ||
right ==
nullptr)
return;
342 for (
int i = 0;
i < num_samples; ++
i) {
343 if (control_counter_-- <= 0) {
345 update_control_rate();
348 const double character_amount = character_.process(character_target_);
349 diffusion_amount_value_ = diffusion_amount_.process(diffusion_amount_target_);
350 const double freeze = freeze_.process(freeze_on_ ? 1.0 : 0.0);
351 duck_.process(duck_target_);
359 const double crossfeed = crossfeed_.process(reverse_on_ ? 0.0 : crossfeed_target_);
361 double feedback = std::min(feedback_.process(feedback_target_), feedback_ceiling());
369 const double in_left =
static_cast<double>(
left[
i]);
370 const double in_right =
static_cast<double>(
right[
i]);
371 duck_peak_ = std::max(duck_peak_, std::max(std::abs(in_left), std::abs(in_right)));
373 const double tap_left =
374 (1.0 - crossfeed) * channels_[0].wet + crossfeed * channels_[1].wet;
375 const double tap_right =
376 (1.0 - crossfeed) * channels_[1].wet + crossfeed * channels_[0].wet;
377 const double input_gain = 1.0 - freeze;
378 const double line_in_left = in_left * input_gain + loop_saturate(tap_left *
feedback);
379 const double line_in_right =
380 in_right * input_gain + loop_saturate(tap_right *
feedback);
382 advance_modulation(character_amount);
383 const double time_left = modulated_time_ms(0, character_amount);
384 const double time_right = modulated_time_ms(1, character_amount);
387 process_character(channels_[0], line_in_left, time_left, character_amount, 1.0);
388 channels_[1].wet = process_character(channels_[1], line_in_right, time_right,
398 channels_[0].wet = apply_loop_tone(channels_[0], channels_[0].wet, freeze);
399 channels_[1].wet = apply_loop_tone(channels_[1], channels_[1].wet, freeze);
404 duck_gain_ = std::clamp(duck_gain_ + duck_gain_step_, 0.0, 1.0);
405 const double gain = duck_gain_;
411 double out_l = channels_[0].wet * gain;
412 double out_r = channels_[1].wet * gain;
418 out_l = channels_[0].diffusion.process_cloud(out_l, 1.0);
420 }
else if (diffusion_amount_value_ > 0.0) {
421 out_l += diffusion_amount_value_ *
422 (channels_[0].diffusion.process(out_l, 1.0) - out_l);
423 channels_[0].diffusion.tick_modulation();
424 out_r += diffusion_amount_value_ *
426 channels_[1].diffusion.tick_modulation();
428 left[
i] =
static_cast<SampleType
>(out_l);
429 right[
i] =
static_cast<SampleType
>(out_r);
437 for (
auto& channel : channels_) channel.bbd.set_compander_enabled(enabled);
441 std::size_t
bbd_stages() const noexcept {
return channels_[0].bbd.stages(); }
444 return channels_[0].vintage.internal_rate_hz();
447 return channels_[
static_cast<std::size_t
>(channel)].tape_physical.chew_state_index();
450 return channels_[
static_cast<std::size_t
>(channel)].tape_physical.gap_coefficients();
453 static double finite_or(
double value,
double fallback)
noexcept {
454 return std::isfinite(value) ? value : fallback;
457 struct ChannelState {
458 chardelay::FractionalDelayLine line;
459 chardelay::ReverseSegmenter reverse;
460 chardelay::DiffusionChain diffusion;
461 chardelay::BbdChannel bbd;
462 chardelay::VintageChannel vintage;
463 chardelay::TapeStandardChannel tape;
464 chardelay::TapePhysicalChannel tape_physical;
465 chardelay::TapeInstability instability;
466 chardelay::OnePole loop_highpass;
467 chardelay::OnePole loop_lowpass;
470 Svf64 tone_high_pass;
477 void update_loop_tone() noexcept {
480 const bool low_cut_boundary_crossed = low_cut_active != loop_low_cut_active_;
481 const bool high_cut_boundary_crossed = high_cut_active != loop_high_cut_active_;
482 loop_tone_active_ = low_cut_active || high_cut_active;
484 const double effective_low_cut = std::clamp(loop_low_cut_hz_, 0.1, guarded_nyquist);
485 const double effective_high_cut = std::clamp(loop_high_cut_hz_, 0.1, guarded_nyquist);
486 for (
auto& channel : channels_) {
487 channel.tone_high_pass.configure(Svf64::Mode::highpass, sample_rate_, effective_low_cut,
489 channel.tone_low_pass.configure(Svf64::Mode::lowpass, sample_rate_, effective_high_cut,
495 if (low_cut_boundary_crossed) channel.tone_high_pass.reset();
496 if (high_cut_boundary_crossed) channel.tone_low_pass.reset();
498 loop_low_cut_active_ = low_cut_active;
499 loop_high_cut_active_ = high_cut_active;
501 const auto peak_bound = [](
double q)
noexcept {
502 constexpr double kButterworthQ = 0.7071067811865476;
503 if (q <= kButterworthQ)
return 1.0;
506 return q / std::sqrt(1.0 - 1.0 / (4.0 * q * q));
508 double tone_gain_bound = 1.0;
510 tone_gain_bound *= peak_bound(loop_low_cut_q_);
512 tone_gain_bound *= peak_bound(loop_high_cut_q_);
520 double apply_loop_tone(ChannelState& channel,
double x,
double freeze)
noexcept {
521 if (!loop_tone_active_)
return x;
524 y = channel.tone_high_pass.process(y);
526 y = channel.tone_low_pass.process(y);
527 return freeze >= 1.0 ?
x :
y + freeze * (
x -
y);
530 bool physical_tape() const noexcept {
538 double feedback_ceiling() const noexcept {
539 switch (character_type_) {
551 double loop_saturate(
double v)
const noexcept {
552 switch (character_type_) {
564 double time_slew_seconds() const noexcept {
565 switch (character_type_) {
575 void configure_time_slew() {
576 for (
auto& slew : time_slew_) slew.configure(time_slew_seconds(), sample_rate_);
579 void apply_tape_speed() {
580 for (
auto& channel : channels_) channel.tape_physical.set_speed_ips(tape_speed_ips_);
587 void update_control_rate() noexcept {
588 const double amount = character_.current();
600 const double envelope = duck_follower_.process(detector);
603 const double knee = normalized * normalized * (3.0 - 2.0 * normalized);
604 const double target = 1.0 - duck_.current() * knee;
607 switch (character_type_) {
609 const double lowpass_hz =
611 const double highpass_hz =
614 for (
auto& channel : channels_) {
615 channel.loop_highpass.set_cutoff(highpass_hz, sample_rate_);
616 channel.loop_lowpass.set_cutoff(lowpass_hz, sample_rate_);
621 clean_lowpass_bypassed_ =
false;
622 for (
auto& channel : channels_) {
628 for (
auto& channel : channels_) {
629 if (physical_tape()) {
630 channel.tape_physical.update(amount);
632 channel.tape.update(amount);
640 for (std::size_t
c = 0;
c < channels_.size(); ++
c)
641 channels_[
c].
bbd.update(amount, reverse_on_
643 : time_slew_[
c].current() * 0.001);
646 for (
auto& channel : channels_) channel.vintage.update(amount);
659 for (
auto& channel : channels_)
662 : diffusion_amount_.current());
665 void advance_modulation(
double character_amount)
noexcept {
666 lfo_phase_ += mod_rate_hz_ / sample_rate_;
667 if (lfo_phase_ >= 1.0) lfo_phase_ -= 1.0;
671 bbd_flutter_phase_ += rate / sample_rate_;
672 if (bbd_flutter_phase_ >= 1.0) bbd_flutter_phase_ -= 1.0;
680 double modulated_time_ms(std::size_t channel,
double character_amount)
noexcept {
687 target *= 1.0 + depth * decorrelation * std::sin(2.0 *
chardelay::kPi * bbd_flutter_phase_);
690 const double slewed = time_slew_[channel].process(target);
691 const double modulation =
692 mod_depth_ * decorrelation * std::sin(2.0 *
chardelay::kPi * lfo_phase_);
693 return slewed * (1.0 + modulation);
696 double process_character(ChannelState& channel,
double x,
double time_ms,
697 double character_amount,
double decorrelation)
noexcept {
698 const double base_samples = time_ms * 0.001 * sample_rate_;
700 switch (character_type_) {
702 y = read_line(channel, x, base_samples, base_samples, 0.0);
703 y = channel.loop_highpass.highpass(y);
704 if (!clean_lowpass_bypassed_)
y = channel.loop_lowpass.lowpass(y);
708 y = read_line(channel, x, base_samples, base_samples, 0.0);
714 y = channel.diffusion.process_diffuser(y, decorrelation);
715 channel.diffusion.tick_modulation();
716 y = channel.loop_highpass.highpass(y);
717 y = channel.loop_lowpass.lowpass(y);
721 channel.instability.tick();
726 const double offset_samples =
727 channel.instability.offset_ms(wow, flutter) * 0.001 * sample_rate_;
729 if (physical_tape()) {
736 const double folded =
737 std::max(base_samples - channel.tape_physical.in_loop_delay_samples(), 1.0);
738 const double pre = channel.tape_physical.pre_process(x);
739 const double delayed =
740 read_line(channel, pre, folded + offset_samples, folded, offset_samples);
741 y = channel.tape_physical.post_process(delayed);
743 const double pre = channel.tape.pre_process(x);
744 const double delayed = read_line(channel, pre, base_samples + offset_samples,
745 base_samples, offset_samples);
746 y = channel.tape.post_process(delayed);
757 const double v = reverse_on_
758 ? channel.reverse.process(x, base_samples, 0.0)
764 const double clock_mod =
765 mod_depth_ * decorrelation * std::sin(2.0 *
chardelay::kPi * lfo_phase_);
766 y = channel.bbd.process(v, clock_mod);
770 const double v = reverse_on_
771 ? channel.reverse.process(x, base_samples, 0.0)
773 const double line_ms =
775 y = channel.vintage.process(v, line_ms * 0.001);
793 double read_line(ChannelState& channel,
double x,
double delay_samples,
794 double segment_samples,
double offset_samples)
noexcept {
800 channel.line.push(x);
801 if (reverse_on_)
return channel.reverse.process(x, segment_samples, offset_samples);
802 return channel.line.read(delay_samples);
805 std::array<ChannelState, 2> channels_{};
806 std::array<chardelay::Smoother, 2> time_slew_{};
807 chardelay::Smoother feedback_;
808 chardelay::Smoother crossfeed_;
809 chardelay::Smoother duck_;
810 chardelay::Smoother freeze_;
811 chardelay::Smoother character_;
812 chardelay::Smoother diffusion_amount_;
813 chardelay::EnvelopeFollower duck_follower_;
818 double sample_rate_ = 48000.0;
819 double time_ms_ = 350.0;
820 double time_offset_ = 1.0;
821 double right_time_ms_ = 350.0;
822 bool right_time_absolute_ =
false;
823 double feedback_target_ = 0.35;
824 double crossfeed_target_ = 0.0;
825 double character_target_ = 0.5;
826 double diffusion_amount_target_ = 0.0;
827 double diffusion_amount_value_ = 0.0;
828 double duck_target_ = 0.0;
829 double mod_rate_hz_ = 0.5;
830 double mod_depth_ = 0.0;
831 double tape_speed_ips_ = 7.5;
832 bool freeze_on_ =
false;
833 bool reverse_on_ =
false;
837 double loop_low_cut_q_ = 0.707;
839 double loop_high_cut_q_ = 0.707;
840 bool loop_low_cut_active_ =
false;
841 bool loop_high_cut_active_ =
false;
842 bool loop_tone_active_ =
false;
844 bool clean_lowpass_bypassed_ =
true;
846 double lfo_phase_ = 0.0;
847 double bbd_flutter_phase_ = 0.0;
848 double duck_gain_ = 1.0;
849 double duck_gain_step_ = 0.0;
850 double duck_peak_ = 0.0;
851 int control_counter_ = 0;
Definition character_delay.hpp:85
void set_reverse(bool on)
Definition character_delay.hpp:241
bool loop_tone_active() const noexcept
True when the in-loop tone stage is doing anything at all.
Definition character_delay.hpp:289
double bbd_bandwidth_hz() const noexcept
Definition character_delay.hpp:440
void set_bbd_compander_enabled(bool enabled) noexcept
Bypass the BBD compander.
Definition character_delay.hpp:436
void set_freeze(bool on)
Definition character_delay.hpp:240
void reset() noexcept
Allocation-free.
Definition character_delay.hpp:303
bool right_time_is_absolute() const noexcept
True while the right channel is following set_right_time_ms().
Definition character_delay.hpp:191
void set_right_time_ms(SampleType right_ms)
Right-channel time in MILLISECONDS, independent of the left.
Definition character_delay.hpp:182
void set_time_ms(SampleType left_ms)
Definition character_delay.hpp:159
void set_loop_high_cut_hz(SampleType hz)
Definition character_delay.hpp:272
void process(SampleType *left, SampleType *right, int num_samples) noexcept
Stereo, in place, WET ONLY.
Definition character_delay.hpp:340
void set_tape_speed_ips(SampleType ips)
Definition character_delay.hpp:150
void set_tape_tier(TapeTier tier)
Definition character_delay.hpp:148
void set_character(Character character)
Definition character_delay.hpp:143
void set_sample_rate(double sample_rate)
Allocates every buffer for the 2 s left-channel maximum times the largest right-channel offset,...
Definition character_delay.hpp:105
void set_feedback(SampleType feedback)
Definition character_delay.hpp:198
Character
Definition character_delay.hpp:87
@ clean
Definition character_delay.hpp:87
@ tape
Definition character_delay.hpp:87
@ bbd
Definition character_delay.hpp:87
@ diffusion
Definition character_delay.hpp:87
@ vintage_digital
Definition character_delay.hpp:87
void set_mod(SampleType rate01, SampleType depth01)
Definition character_delay.hpp:223
void set_crossfeed(SampleType crossfeed)
Definition character_delay.hpp:204
void set_duck(SampleType amount01)
Definition character_delay.hpp:235
void set_loop_low_cut_resonance(SampleType q)
Definition character_delay.hpp:264
const std::vector< double > & tape_gap_coefficients(int channel) const noexcept
Definition character_delay.hpp:449
std::size_t chew_state_index(int channel) const noexcept
Definition character_delay.hpp:446
void set_diffusion_amount(SampleType amount)
Definition character_delay.hpp:218
void set_loop_high_cut_resonance(SampleType q)
Definition character_delay.hpp:280
void set_loop_low_cut_hz(SampleType hz)
Definition character_delay.hpp:256
int latency_samples() const noexcept
Always 0.
Definition character_delay.hpp:296
double vintage_internal_rate_hz() const noexcept
Definition character_delay.hpp:443
double vintage_band_edge_hz() const noexcept
Definition character_delay.hpp:442
double right_time_ms() const noexcept
The right channel's target time in ms, whichever mode is selected.
Definition character_delay.hpp:194
void set_character_amount(SampleType amount)
Definition character_delay.hpp:209
TapeTier
Definition character_delay.hpp:88
@ physical
Definition character_delay.hpp:88
@ standard
Definition character_delay.hpp:88
void set_time_offset(SampleType multiplier)
Right-channel time as a MULTIPLE of the left.
Definition character_delay.hpp:167
std::size_t bbd_stages() const noexcept
Definition character_delay.hpp:441
constexpr double kTimeSlewCleanMs
Definition tables.hpp:99
constexpr double kTimeOffsetMax
Definition tables.hpp:34
constexpr double kLoopToneNyquistFraction
Keep the TPT prewarp strictly below Nyquist.
Definition tables.hpp:76
constexpr double kModRateMaxHz
Definition tables.hpp:59
constexpr std::array< double, 5 > kTapeSpeedsIps
The IEC/NAB standard tape-speed set, in inches per second (published).
Definition tables.hpp:509
constexpr double kDuckReleaseS
Definition tables.hpp:355
constexpr std::array< double, 3 > kBbdFlutterDepth
Definition tables.hpp:199
constexpr double kTimeOffsetMin
Definition tables.hpp:33
constexpr double kLoopToneHpMaxHz
Definition tables.hpp:71
constexpr double kPi
Definition primitives.hpp:38
constexpr int kControlInterval
Coefficient recomputation and stochastic-walk stepping cadence, in samples.
Definition tables.hpp:40
constexpr double kFlutterPhaseRight
Right-channel flutter phase offset, in cycles.
Definition tables.hpp:187
constexpr double kLoopToneHpMinHz
User-facing in-loop tone stage.
Definition tables.hpp:70
double fast_tanh(double v) noexcept
Definition primitives.hpp:57
constexpr std::array< double, 3 > kBbdAxis
Definition tables.hpp:190
constexpr double kLoopToneResMin
Resonance range for both loop-tone filters.
Definition tables.hpp:82
constexpr double kReverseLineMs
Delay the CLOCK-DOMAIN characters' own line runs at while reverse is engaged, in milliseconds.
Definition tables.hpp:350
constexpr double kLoopToneLpMinHz
Definition tables.hpp:72
constexpr double kTimeSlewBbdMs
Definition tables.hpp:102
constexpr double kStereoDecorr
Right-channel depth scaler for every shared-LFO modulation.
Definition tables.hpp:88
constexpr double kModRateMinHz
Generic modulation LFO: log rate map and maximum depth as a fraction of delay time.
Definition tables.hpp:58
constexpr double kDiffusionLoopHpHz
Diffusion loop filters. [LP is a design parameter].
Definition tables.hpp:264
constexpr double kTimeSlewVintageMs
Definition tables.hpp:100
constexpr double kDuckThreshold
Definition tables.hpp:356
constexpr double kMaxDelayMs
Maximum left-channel base delay and stereo-offset bounds.
Definition tables.hpp:32
constexpr double kTimeSlewTapeMs
Definition tables.hpp:101
constexpr std::array< double, 3 > kCleanAxis
Definition tables.hpp:107
constexpr std::uint32_t kPrngSeed
PRNG seed.
Definition tables.hpp:92
constexpr double kDiffusionLoopLpHz
Definition tables.hpp:265
constexpr double kLoopLpBypassHz
A loop lowpass at or above this cutoff is BYPASSED rather than run.
Definition tables.hpp:116
double interpolate_knots(const std::array< double, N > &axis, const std::array< double, N > &values, double c) noexcept
Definition primitives.hpp:125
constexpr double kBbdFlutterRateBaseHz
BBD flutter rate map, Hz.
Definition tables.hpp:238
constexpr double kDuckAttackS
Definition tables.hpp:354
constexpr std::array< double, 3 > kCleanLoopHpHz
Definition tables.hpp:109
constexpr double kLoopToneResMax
Definition tables.hpp:83
constexpr double kSaturatedFeedbackMax
Feedback ceiling for the saturating characters.
Definition tables.hpp:54
constexpr std::array< double, 4 > kTapeWowDepthMs
Definition tables.hpp:142
constexpr double kLoopToneLpMaxHz
Definition tables.hpp:73
constexpr std::array< double, 4 > kTapeAxis
Definition tables.hpp:139
constexpr double kParamGlideS
De-zipper glide for scalar params (feedback, crossfeed, duck, freeze, and the character table's per-s...
Definition tables.hpp:44
double physical_tape_effective_feedback(double requested_feedback, double age) noexcept
Apply the physical tier's measured whole-loop stability calibration.
Definition tape_physical.hpp:47
constexpr std::array< double, 4 > kTapeFlutterDepthMs
Definition tables.hpp:143
constexpr double kUnsaturatedFeedbackMax
Feedback ceiling for the characters with NO in-loop saturator.
Definition tables.hpp:49
constexpr double kModMaxDepth
Definition tables.hpp:60
constexpr std::array< double, 3 > kCleanLoopLpHz
Definition tables.hpp:108
constexpr double kMaxAddressableDelayMs
Definition tables.hpp:35
constexpr double kBbdFlutterRateSpanHz
Definition tables.hpp:239
constexpr double kTimeSlewDiffusionMs
Definition tables.hpp:103
Published tables and closed forms, free of any instance so a test can assert the SHIPPED constant rat...
Definition tape_machine_components.hpp:41
Definition additive_bank.hpp:218
@ feedback
Definition comb_filter.hpp:18
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ c
Definition spectrum_trace.hpp:35
@ i
Definition formant_filter_bank.hpp:20
CharacterDelayT< float > CharacterDelay
Definition character_delay.hpp:854
SvfT< double > Svf64
Definition svf.hpp:149
@ y
Definition input_events.hpp:42
@ q
Definition input_events.hpp:41
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41