187template <
typename SampleType =
float>
294 for (
int index = 0; index < 2; ++index)
295 channels_[
static_cast<std::size_t
>(index)].prepare(sample_rate_, index);
297 instability_nominal_samples_ =
static_cast<int>(std::llround(
299 print_max_samples_ =
static_cast<int>(std::llround(
302 compander_l_.prepare(sample_rate_);
303 compander_r_.prepare(sample_rate_);
305 update_speed(speed_ips_,
true);
309 update_print_through();
316 for (
auto& channel : channels_) channel.reset();
317 compander_l_.reset();
318 compander_r_.reset();
331 age01_ = preset.
age01;
336 update_print_through();
344 if (!std::isfinite(ips))
return;
349 double speed_ips() const noexcept {
return speed_ips_; }
361 if (!std::isfinite(
static_cast<double>(bias_c)))
return;
362 bias_request_ = std::clamp(
static_cast<double>(bias_c),
kBiasMin,
kBiasMax);
364 update_print_through();
373 if (!std::isfinite(
static_cast<double>(drive01)))
return;
374 drive01_ = std::clamp(
static_cast<double>(drive01), 0.0, 1.0);
378 double drive() const noexcept {
return drive01_; }
381 if (!std::isfinite(
static_cast<double>(age01)))
return;
382 age01_ = std::clamp(
static_cast<double>(age01), 0.0, 1.0);
384 update_print_through();
387 double age() const noexcept {
return age01_; }
390 if (!std::isfinite(
static_cast<double>(db)))
return;
404 if (!std::isfinite(
static_cast<double>(level_db)) ||
405 !std::isfinite(
static_cast<double>(offset_ms)))
409 print_offset_ms_ = std::clamp(
static_cast<double>(offset_ms),
412 update_print_through();
420 if (!std::isfinite(
static_cast<double>(wet01)))
return;
421 mix_ = std::clamp(
static_cast<double>(wet01), 0.0, 1.0);
426 void process(
const SampleType* in_l,
const SampleType* in_r, SampleType* out_l,
427 SampleType* out_r,
int n_frames) {
428 for (
int n = 0; n < n_frames; ++n) {
429 const double dry_l =
static_cast<double>(in_l[n]);
430 const double dry_r =
static_cast<double>(in_r[n]);
435 if (!std::isfinite(dry_l) || !std::isfinite(dry_r)) {
436 recover_audio_fault();
437 out_l[n] = SampleType{0};
438 out_r[n] = SampleType{0};
444 double left = companding_ ? compander_l_.encode(dry_l) : dry_l;
445 double right = companding_ ? compander_r_.encode(dry_r) : dry_r;
447 left = channels_[0].record(
left, drive_gain_, inverse_drive_gain_);
448 right = channels_[1].record(
right, drive_gain_, inverse_drive_gain_);
457 const double leak_l = crosstalk_gain_ * channels_[0].crosstalk_tilt.process(
right);
458 const double leak_r = crosstalk_gain_ * channels_[1].crosstalk_tilt.process(
left);
462 left = channels_[0].print_through(
left, print_taps_);
463 right = channels_[1].print_through(
right, print_taps_);
475 const double output_l = dry_l + mix_ * (
left - dry_l);
476 const double output_r = dry_r + mix_ * (
right - dry_r);
477 if (!std::isfinite(output_l) || !std::isfinite(output_r)) {
478 recover_audio_fault();
479 out_l[n] = SampleType{0};
480 out_r[n] = SampleType{0};
483 out_l[n] =
static_cast<SampleType
>(output_l);
484 out_r[n] =
static_cast<SampleType
>(output_r);
495 (pre_echo_ ? print_taps_.offset_samples : 0);
522 const double nyquist = 0.5 * sample_rate_;
523 for (
const Curve curve : {Curve::nab, Curve::iec_ccir, Curve::cassette_type1,
524 Curve::cassette_type2}) {
527 record.
set(tc, sample_rate_,
false);
528 playback.set(tc, sample_rate_,
true);
529 for (
double hz = 20.0; hz < nyquist; hz *= 1.005) {
530 worst = std::max(worst,
532 worst = std::max(worst,
549 return channels_[
static_cast<std::size_t
>(channel)].
record_eq;
553 return channels_[
static_cast<std::size_t
>(channel)].
playback_eq;
558 const std::vector<double>&
gap_fir() const noexcept {
559 return channels_[0].gap_taps;
597 void recover_audio_fault() noexcept {
598 for (
auto& channel : channels_) channel.recover_audio_fault();
599 compander_l_.reset();
600 compander_r_.reset();
606 int offset_samples = 0;
607 double post_gain = 0.0;
608 double pre_gain = 0.0;
609 bool pre_echo =
false;
615 chardelay::TapePhysicalChannel physical{};
616 chardelay::TapeInstability instability{};
617 chardelay::FractionalDelayLine instability_line{};
618 chardelay::FixedFir gap_fir{};
619 chardelay::FirstOrderShelf bias_shelf{};
620 chardelay::FirstOrderShelf crosstalk_tilt{};
621 TapeEqSection record_eq{};
622 TapeEqSection playback_eq{};
624 std::vector<double> gap_taps{};
625 std::vector<float> print_line{};
626 std::size_t print_write = 0;
627 std::size_t print_valid = 0;
629 double wow_depth_ms = 0.0;
630 double flutter_depth_ms = 0.0;
631 double nominal_samples = 0.0;
632 double sample_rate = 48000.0;
633 double alignment_gain = 1.0;
636 static double fir_magnitude(
const std::vector<double>& taps,
double hz,
637 double fs)
noexcept {
638 if (taps.empty())
return 1.0;
639 double re = 0.0, im = 0.0;
640 for (std::size_t n = 0;
n < taps.size(); ++
n) {
641 const double theta = 2.0 *
chardelay::kPi * hz *
static_cast<double>(
n) / fs;
642 re += taps[
n] * std::cos(theta);
643 im -= taps[
n] * std::sin(theta);
645 return std::hypot(re, im);
649 void align(
double ceiling_db)
noexcept {
652 const double reused_gap =
656 std::max(reused_gap, 1
e-6) * std::max(own_gap, 1
e-6);
657 alignment_gain = std::min(1.0 / std::max(loss, 1
e-6),
661 void prepare(
double fs,
int index) {
663 physical.prepare(fs);
664 instability.prepare(fs);
670 const auto seed =
static_cast<std::uint32_t
>(
673 physical.set_seeds(seed);
676 const auto capacity =
static_cast<std::size_t
>(
678 instability_line.prepare(capacity);
680 const auto print_capacity =
static_cast<std::size_t
>(std::llround(
682 print_line.assign(print_capacity, 0.0f);
690 instability_line.reset();
693 crosstalk_tilt.reset();
696 std::fill(print_line.begin(), print_line.end(), 0.0f);
701 void recover_audio_fault() noexcept {
702 physical.recover_audio_fault();
704 instability_line.discard_history();
705 gap_fir.discard_history();
707 crosstalk_tilt.reset();
714 void set_gap_taps(std::vector<double> taps) {
715 gap_taps = std::move(taps);
716 gap_fir.prepare(gap_taps.size());
726 double record(
double x,
double drive_gain,
double inverse_drive_gain)
noexcept {
727 const double emphasized = record_eq.process(x);
728 return physical.pre_process(emphasized * drive_gain) * inverse_drive_gain;
733 double playback(
double x)
noexcept {
735 const double gapped = gap_taps.empty()
737 : gap_fir.convolve(gap_taps.data(), gap_taps.size());
738 const double lossy = physical.post_process(gapped) * alignment_gain;
741 instability_line.push(lossy);
742 const double offset_ms = instability.offset_ms(wow_depth_ms, flutter_depth_ms);
745 return instability_line.read(read);
752 double print_through(
double x,
const PrintTaps& taps)
noexcept {
753 const std::size_t
size = print_line.size();
754 if (size == 0 || taps.offset_samples <= 0)
return x;
755 const auto offset =
static_cast<std::size_t
>(taps.offset_samples);
757 print_line[print_write] =
static_cast<float>(
x);
758 const auto at = [&](std::size_t
back) {
759 if (back >= print_valid + 1u)
return 0.0;
760 return static_cast<double>(print_line[(print_write +
size -
back) %
size]);
767 y =
at(offset) + taps.pre_gain *
at(0) + taps.post_gain *
at(2u * offset);
769 y =
at(0) + taps.post_gain *
at(offset);
771 print_write = (print_write + 1u) % size;
772 if (print_valid < size) ++print_valid;
777 void update_speed(
double ips,
bool redesign) {
778 const bool changed = std::abs(ips - speed_ips_) > 1
e-9;
780 if (!changed && !redesign)
return;
783 chardelay::TapeLossGeometry gap_geometry;
784 gap_geometry.speed_ips = speed_ips_;
785 gap_geometry.gap_m = geometry.reproduce_m;
792 for (
auto& channel : channels_) {
793 channel.set_gap_taps(taps);
794 channel.physical.set_speed_ips(speed_ips_);
801 for (
auto& channel : channels_) {
802 channel.record_eq.set(tc, sample_rate_,
false);
803 channel.playback_eq.set(tc, sample_rate_,
true);
817 for (
auto& channel : channels_) {
818 channel.physical.update(age01_);
836 for (
auto& channel : channels_)
837 channel.bias_shelf.set(
kBiasShelfHz, shelf_gain, sample_rate_);
839 const double distortion_mult = 1.0 +
kBiasDistK * bias_ * bias_;
841 inverse_drive_gain_ = 1.0 / drive_gain_;
849 for (
auto& channel : channels_) {
850 channel.wow_depth_ms = wow;
851 channel.flutter_depth_ms = flutter;
855 void update_crosstalk() {
857 for (
auto& channel : channels_)
872 void update_print_through() {
873 print_taps_.offset_samples = std::min(
877 const double post_db = print_through_db_ - reduction;
880 print_taps_.pre_echo = pre_echo_;
883 std::array<Channel, 2> channels_{};
884 TapeCompanderT<SampleType> compander_l_{};
885 TapeCompanderT<SampleType> compander_r_{};
887 double sample_rate_ = 48000.0;
888 Archetype archetype_ = Archetype::ampex_350_440;
889 Curve curve_ = Curve::nab;
890 double speed_ips_ = 15.0;
895 double crosstalk_db_ = -38.0;
896 bool companding_ =
false;
899 bool pre_echo_ =
false;
902 double drive_gain_ = 1.0;
903 double inverse_drive_gain_ = 1.0;
904 double crosstalk_gain_ = 0.0;
905 PrintTaps print_taps_{};
906 int instability_nominal_samples_ = 0;
907 int print_max_samples_ = 0;
Peak/RMS envelope follower with independent attack and release curves.
The 2:1 / 1:2 linear-dB compander pair.
Definition tape_machine_components.hpp:469
One first-order EQ section — a (curve, speed) record network, or its exact inverse.
Definition tape_machine_components.hpp:341
void set(const tape::EqTimeConstants &tc, double sample_rate, bool inverse) noexcept
Definition tape_machine_components.hpp:370
double response_db(double hz, double sample_rate) const noexcept
Magnitude response in dB.
Definition tape_machine_components.hpp:425
The tape machine insert.
Definition tape_machine.hpp:188
void set_speed_ips(double ips)
Snapped to the nearest legal speed for the active archetype.
Definition tape_machine.hpp:343
static constexpr double kPrintThroughOffsetMsDefault
Definition tape_machine.hpp:255
static constexpr double kPreEchoAsymmetryDb
Definition tape_machine.hpp:261
const TapeEqSection & playback_eq(int channel=0) const noexcept
Definition tape_machine.hpp:552
double worst_case_insertion_gain() const noexcept
The largest gain any single stage in this insert can present, as a bound a reviewer can size headroom...
Definition tape_machine.hpp:520
void reset()
Never allocates.
Definition tape_machine.hpp:315
double print_offset_ms() const noexcept
Definition tape_machine.hpp:416
static constexpr double kOverBiasHfCutDb
Definition tape_machine.hpp:206
TapeMachineT()
Definition tape_machine.hpp:285
static constexpr double kDriveDefault
Definition tape_machine.hpp:227
bool companding_enabled() const noexcept
Definition tape_machine.hpp:398
static constexpr double kUnderBiasHfBoostDb
Definition tape_machine.hpp:208
static constexpr double kCrosstalkDbMax
Definition tape_machine.hpp:231
void set_age(float age01)
Definition tape_machine.hpp:380
double reproduce_alignment_db() const noexcept
How much reproduce gain the machine is currently aligned with, in dB.
Definition tape_machine.hpp:590
bool pre_echo_enabled() const noexcept
Definition tape_machine.hpp:417
double sample_rate() const noexcept
Definition tape_machine.hpp:594
static constexpr double kBiasErasureDb
Definition tape_machine.hpp:220
double speed_ips() const noexcept
Definition tape_machine.hpp:349
double crosstalk_db() const noexcept
Definition tape_machine.hpp:395
double drive() const noexcept
Definition tape_machine.hpp:378
void prepare(double sample_rate)
Sizes both delay lines for their worst configured case and designs one gap-loss FIR per legal speed,...
Definition tape_machine.hpp:290
static constexpr double kPrintThroughDbMax
Definition tape_machine.hpp:248
void set_eq_curve(Curve curve)
Independent of archetype on purpose: a Studer really could be told to run NAB, and the resulting low-...
Definition tape_machine.hpp:353
static constexpr double kAlignmentCeilingDb
Definition tape_machine.hpp:268
const std::vector< double > & gap_fir() const noexcept
Coefficients of this instance's reproduce gap-loss FIR at the active speed, so the suite can measure ...
Definition tape_machine.hpp:558
static int oversampler_latency_samples() noexcept
Group delay of the reused hysteresis stage's 4× half-band oversampling wrap, in host samples.
Definition tape_machine.hpp:279
static constexpr double kDriveDbMax
Definition tape_machine.hpp:226
void set_crosstalk_db(float db)
Definition tape_machine.hpp:389
static constexpr double kBiasMin
Definition tape_machine.hpp:196
double print_through_db() const noexcept
Definition tape_machine.hpp:415
const TapeEqSection & record_eq(int channel=0) const noexcept
Definition tape_machine.hpp:548
static constexpr double kBiasMax
Definition tape_machine.hpp:197
void set_bias(float bias_c)
Definition tape_machine.hpp:360
void set_companding_enabled(bool enabled) noexcept
Definition tape_machine.hpp:397
static constexpr double kPrintThroughOffsetMsMax
Definition tape_machine.hpp:257
int latency_samples() const noexcept
Reported exactly, never estimated (series law 5).
Definition tape_machine.hpp:493
void set_archetype(Archetype archetype)
Applies the full baked preset.
Definition tape_machine.hpp:325
void set_print_through(float level_db, float offset_ms, bool pre_echo_enabled)
level_db is the post-echo level before bias erasure; offset_ms is the wrap offset; pre_echo_enabled a...
Definition tape_machine.hpp:403
double reproduce_gap_m() const noexcept
The reproduce gap width in force, in metres.
Definition tape_machine.hpp:563
static constexpr double kCrosstalkTiltHz
Definition tape_machine.hpp:238
void process(const SampleType *in_l, const SampleType *in_r, SampleType *out_l, SampleType *out_r, int n_frames)
Definition tape_machine.hpp:426
TapeCurve Curve
Definition tape_machine.hpp:190
Archetype archetype() const noexcept
Definition tape_machine.hpp:339
static constexpr double kBiasDefault
Definition tape_machine.hpp:198
void set_mix(float wet01) noexcept
Definition tape_machine.hpp:419
static constexpr double kBiasShelfHz
Definition tape_machine.hpp:210
static constexpr double kBiasDistK
Definition tape_machine.hpp:215
void set_drive(float drive01)
Definition tape_machine.hpp:372
static constexpr double kCrosstalkDbMin
Definition tape_machine.hpp:230
static constexpr double kPrintThroughDbMin
Definition tape_machine.hpp:247
double effective_bias() const noexcept
The bias actually in force: what was requested plus the age table's drift, clamped.
Definition tape_machine.hpp:370
static constexpr double kPrintThroughOffsetMsMin
Definition tape_machine.hpp:256
Curve eq_curve() const noexcept
Definition tape_machine.hpp:358
TapeArchetype Archetype
Definition tape_machine.hpp:191
static constexpr double kInstabilityNominalMs
Definition tape_machine.hpp:275
double age() const noexcept
Definition tape_machine.hpp:387
static constexpr double kCrosstalkTiltMaxDb
Definition tape_machine.hpp:243
static int oversampler_latency_samples() noexcept
Group delay the oversampling wrap adds inside the loop, in host samples.
Definition tape_physical.hpp:291
MidiEvent at(MidiEvent event, std::int32_t sample_offset) noexcept
Definition utility_contract.hpp:140
Definition audio_renderer.hpp:20
@ index
Definition gpu_pass.hpp:140
@ back
Definition gpu_pass.hpp:283
std::vector< double > design_tape_gap_fir(double fs, std::size_t taps, const TapeLossGeometry &geometry)
Design a minimum-phase FIR realizing the GAP term only.
Definition tape_loss.hpp:495
constexpr double kPi
Definition primitives.hpp:38
constexpr double kFlutterPhaseRight
Right-channel flutter phase offset, in cycles.
Definition tables.hpp:187
std::size_t tape_gap_fir_taps(double fs)
FIR length at a given sample rate: the specified order at 48 kHz, scaled with the rate so the filter ...
Definition tape_loss.hpp:482
constexpr std::uint32_t kPrngSeed
PRNG seed.
Definition tables.hpp:92
double interpolate_knots(const std::array< double, N > &axis, const std::array< double, N > &values, double c) noexcept
Definition primitives.hpp:125
constexpr std::array< double, 4 > kTapeWowDepthMs
Definition tables.hpp:142
constexpr std::array< double, 4 > kTapeAxis
Definition tables.hpp:139
constexpr std::array< double, 4 > kTapeFlutterDepthMs
Definition tables.hpp:143
double tape_loss_iir_magnitude_db(const TapeLossIirParams ¶meters, double frequency_hz) noexcept
Magnitude of the cascade, in dB. Used by the fitter and by tests.
Definition tape_loss.hpp:150
@ size
Definition config.hpp:216
std::uint64_t read(std::span< const std::uint8_t > b, std::size_t p, unsigned n) noexcept
Definition spectral_frame_codec.hpp:111
EqTimeConstants eq_time_constants(TapeCurve curve, double speed_ips) noexcept
The published time-constant table, selected by (curve, speed).
Definition tape_machine_components.hpp:74
HeadGapGeometry head_gap_geometry(double speed_ips) noexcept
Gap widths by speed class.
Definition tape_machine_components.hpp:140
constexpr double kEqReferenceHz
The frequency every EQ curve in this module is normalized to be unity at.
Definition tape_machine_components.hpp:55
double age_bias_drift(double age01) noexcept
Definition tape_machine_components.hpp:195
double first_order_shelf_db(double corner_hz, double gain_linear, double hz, double sample_rate) noexcept
Magnitude, in dB, of the REALIZED chardelay::FirstOrderShelf — the TPT one-pole shelf this module use...
Definition tape_machine_components.hpp:212
double snap_speed_ips(TapeArchetype archetype, double requested_ips) noexcept
Nearest legal speed for an archetype.
Definition tape_machine_components.hpp:295
ArchetypePreset archetype_preset(TapeArchetype archetype) noexcept
Baked defaults per §5 of the source spec.
Definition tape_machine_components.hpp:256
double age_print_through_db(double age01) noexcept
Definition tape_machine_components.hpp:190
T linear_to_db(T linear)
Linear gain → amplitude dB, floored at kDbFloorAmplitude so silence maps to a finite −200 dB rather t...
Definition units.hpp:79
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
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
TapeCompanderT< float > TapeCompander
Definition tape_machine.hpp:912
TapeMachineT< double > TapeMachine64
Definition tape_machine.hpp:911
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
TapeCurve
Which recording standard's record/playback network pair is in force.
Definition tape_machine_components.hpp:25
@ e
Definition formant_filter_bank.hpp:20
TapeMachineT< float > TapeMachine
Definition tape_machine.hpp:910
TapeArchetype
Which machine's full preset a fresh instance starts from.
Definition tape_machine_components.hpp:33
@ y
Definition input_events.hpp:42
@ n
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ changed
Definition gesture.hpp:17
The complete baked preset for one machine.
Definition tape_machine_components.hpp:228
TapeCurve default_curve
Definition tape_machine_components.hpp:232
double crosstalk_db
Definition tape_machine_components.hpp:233
double age01
Definition tape_machine_components.hpp:235
double default_speed_ips
Definition tape_machine_components.hpp:231
bool companding
Definition tape_machine_components.hpp:234
One standard's pair of time constants, in SECONDS.
Definition tape_machine_components.hpp:60
double reproduce_m
Definition tape_machine_components.hpp:125
Public tape standards, physical models, EQ, and compander building blocks.
The real-unit vocabulary shared by every DSP block in the catalog.