Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
tape_machine.hpp
Go to the documentation of this file.
1#pragma once
2
157
158// `character_delay/tape_physical.hpp` MUST come before `tape_loss.hpp` here,
159// which is why this block is not alphabetized. `hysteresis.hpp` (pulled in by
160// tape_physical) names `detail::LinearPhaseOversamplingStage2x` unqualified from
161// inside `pulp::signal::chardelay`, and that only resolves to
162// `pulp::signal::detail` while `pulp::signal::chardelay::detail` does not exist
163// yet. `tape_loss.hpp` opens `chardelay::detail`, so including it first makes the
164// nearer namespace win and the build fails on a name that is plainly there.
167
174#include <pulp/signal/units.hpp>
175
176#include <algorithm>
177#include <array>
178#include <cmath>
179#include <complex>
180#include <cstddef>
181#include <cstdint>
182#include <vector>
183
184namespace pulp::signal {
185
187template <typename SampleType = float>
189public:
192
193 // ── Design parameters (the complete roster) ───────────────────────────
194
196 static constexpr double kBiasMin = -1.0;
197 static constexpr double kBiasMax = 1.0;
198 static constexpr double kBiasDefault = 0.0;
199
206 static constexpr double kOverBiasHfCutDb = 6.0;
208 static constexpr double kUnderBiasHfBoostDb = 4.0;
210 static constexpr double kBiasShelfHz = 8000.0;
211
215 static constexpr double kBiasDistK = 0.6;
216
220 static constexpr double kBiasErasureDb = 3.0;
221
226 static constexpr double kDriveDbMax = 24.0;
227 static constexpr double kDriveDefault = 0.3;
228
230 static constexpr double kCrosstalkDbMin = -45.0;
231 static constexpr double kCrosstalkDbMax = -22.0;
232
238 static constexpr double kCrosstalkTiltHz = 5000.0;
243 static constexpr double kCrosstalkTiltMaxDb = 6.0;
244
247 static constexpr double kPrintThroughDbMin = -80.0;
248 static constexpr double kPrintThroughDbMax = -38.0;
249
255 static constexpr double kPrintThroughOffsetMsDefault = 700.0;
256 static constexpr double kPrintThroughOffsetMsMin = 200.0;
257 static constexpr double kPrintThroughOffsetMsMax = 2000.0;
258
261 static constexpr double kPreEchoAsymmetryDb = 4.0;
262
268 static constexpr double kAlignmentCeilingDb = 24.0;
269
275 static constexpr double kInstabilityNominalMs = 2.0;
276
282
283 // ── Lifecycle ─────────────────────────────────────────────────────────
284
285 TapeMachineT() { set_archetype(Archetype::ampex_350_440); }
286
290 void prepare(double sample_rate) {
291 if (std::isfinite(sample_rate) && sample_rate > 0.0)
292 sample_rate_ = sample_rate;
293
294 for (int index = 0; index < 2; ++index)
295 channels_[static_cast<std::size_t>(index)].prepare(sample_rate_, index);
296
297 instability_nominal_samples_ = static_cast<int>(std::llround(
299 print_max_samples_ = static_cast<int>(std::llround(
301
302 compander_l_.prepare(sample_rate_);
303 compander_r_.prepare(sample_rate_);
304
305 update_speed(speed_ips_, /*redesign=*/true);
306 update_eq();
307 apply_age();
308 update_crosstalk();
309 update_print_through();
310 reset();
311 }
312
315 void reset() {
316 for (auto& channel : channels_) channel.reset();
317 compander_l_.reset();
318 compander_r_.reset();
319 }
320
321 // ── Parameters ────────────────────────────────────────────────────────
322
326 archetype_ = archetype;
328 curve_ = preset.default_curve;
329 crosstalk_db_ = preset.crosstalk_db;
330 companding_ = preset.companding;
331 age01_ = preset.age01;
333 update_eq();
334 apply_age();
335 update_crosstalk();
336 update_print_through();
337 }
338
339 Archetype archetype() const noexcept { return archetype_; }
340
343 void set_speed_ips(double ips) {
344 if (!std::isfinite(ips)) return;
345 update_speed(tape::snap_speed_ips(archetype_, ips), /*redesign=*/false);
346 update_eq();
347 }
348
349 double speed_ips() const noexcept { return speed_ips_; }
350
353 void set_eq_curve(Curve curve) {
354 curve_ = curve;
355 update_eq();
356 }
357
358 Curve eq_curve() const noexcept { return curve_; }
359
360 void set_bias(float bias_c) {
361 if (!std::isfinite(static_cast<double>(bias_c))) return;
362 bias_request_ = std::clamp(static_cast<double>(bias_c), kBiasMin, kBiasMax);
363 update_bias();
364 update_print_through();
365 }
366
370 double effective_bias() const noexcept { return bias_; }
371
372 void set_drive(float drive01) {
373 if (!std::isfinite(static_cast<double>(drive01))) return;
374 drive01_ = std::clamp(static_cast<double>(drive01), 0.0, 1.0);
375 update_bias();
376 }
377
378 double drive() const noexcept { return drive01_; }
379
380 void set_age(float age01) {
381 if (!std::isfinite(static_cast<double>(age01))) return;
382 age01_ = std::clamp(static_cast<double>(age01), 0.0, 1.0);
383 apply_age();
384 update_print_through();
385 }
386
387 double age() const noexcept { return age01_; }
388
389 void set_crosstalk_db(float db) {
390 if (!std::isfinite(static_cast<double>(db))) return;
391 crosstalk_db_ = std::clamp(static_cast<double>(db), kCrosstalkDbMin, kCrosstalkDbMax);
392 update_crosstalk();
393 }
394
395 double crosstalk_db() const noexcept { return crosstalk_db_; }
396
397 void set_companding_enabled(bool enabled) noexcept { companding_ = enabled; }
398 bool companding_enabled() const noexcept { return companding_; }
399
403 void set_print_through(float level_db, float offset_ms, bool pre_echo_enabled) {
404 if (!std::isfinite(static_cast<double>(level_db)) ||
405 !std::isfinite(static_cast<double>(offset_ms)))
406 return;
407 print_through_db_ =
408 std::clamp(static_cast<double>(level_db), kPrintThroughDbMin, kPrintThroughDbMax);
409 print_offset_ms_ = std::clamp(static_cast<double>(offset_ms),
411 pre_echo_ = pre_echo_enabled;
412 update_print_through();
413 }
414
415 double print_through_db() const noexcept { return print_through_db_; }
416 double print_offset_ms() const noexcept { return print_offset_ms_; }
417 bool pre_echo_enabled() const noexcept { return pre_echo_; }
418
419 void set_mix(float wet01) noexcept {
420 if (!std::isfinite(static_cast<double>(wet01))) return;
421 mix_ = std::clamp(static_cast<double>(wet01), 0.0, 1.0);
422 }
423
424 // ── Audio ─────────────────────────────────────────────────────────────
425
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]);
431
432 // Inputs immediately enter several recursive stages. Treat a
433 // non-finite frame as a recovery boundary before it can latch
434 // those states; reset is allocation-free by contract.
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};
439 continue;
440 }
441
442 // Record chain. Companding first, so the compressor sees the
443 // programme rather than the tape's own colouration.
444 double left = companding_ ? compander_l_.encode(dry_l) : dry_l;
445 double right = companding_ ? compander_r_.encode(dry_r) : dry_r;
446
447 left = channels_[0].record(left, drive_gain_, inverse_drive_gain_);
448 right = channels_[1].record(right, drive_gain_, inverse_drive_gain_);
449
450 // Playback chain up to the point where the two channels meet.
451 left = channels_[0].playback(left);
452 right = channels_[1].playback(right);
453
454 // Crosstalk: each channel leaks a tilted copy of the other. Read
455 // both before writing either, or the second leak would be computed
456 // from a channel that already contains the first.
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);
459 left += leak_l;
460 right += leak_r;
461
462 left = channels_[0].print_through(left, print_taps_);
463 right = channels_[1].print_through(right, print_taps_);
464
465 // Reproduce EQ, then the bias-dependent high shelf cascaded with it,
466 // then the expander.
467 left = channels_[0].bias_shelf.process(channels_[0].playback_eq.process(left));
468 right = channels_[1].bias_shelf.process(channels_[1].playback_eq.process(right));
469
470 if (companding_) {
471 left = compander_l_.decode(left);
472 right = compander_r_.decode(right);
473 }
474
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};
481 continue;
482 }
483 out_l[n] = static_cast<SampleType>(output_l);
484 out_r[n] = static_cast<SampleType>(output_r);
485 }
486 }
487
493 int latency_samples() const noexcept {
494 return oversampler_latency_samples() + instability_nominal_samples_ +
495 (pre_echo_ ? print_taps_.offset_samples : 0);
496 }
497
520 double worst_case_insertion_gain() const noexcept {
521 double worst = 1.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}) {
525 const tape::EqTimeConstants tc = tape::eq_time_constants(curve, speed_ips_);
526 TapeEqSection record, playback;
527 record.set(tc, sample_rate_, /*inverse=*/false);
528 playback.set(tc, sample_rate_, /*inverse=*/true);
529 for (double hz = 20.0; hz < nyquist; hz *= 1.005) {
530 worst = std::max(worst,
531 units::db_to_linear(record.response_db(hz, sample_rate_)));
532 worst = std::max(worst,
533 units::db_to_linear(playback.response_db(hz, sample_rate_)));
534 }
535 }
537 }
538
539 // ── Test surface ──────────────────────────────────────────────────────
540 //
541 // The acceptance criteria for the EQ networks, the gap filter and the
542 // compander are stated as tolerances on THOSE STAGES (±0.5 dB against an
543 // analytic prototype, a ≥40 dB extinction null, ±0.1 dB of round-trip
544 // transparency). None of them is measurable end-to-end through a chain that
545 // also contains Wallace loss, a head bump, hiss and pitch modulation, so the
546 // stages are reachable directly. See the suite's notes.
547
548 const TapeEqSection& record_eq(int channel = 0) const noexcept {
549 return channels_[static_cast<std::size_t>(channel)].record_eq;
550 }
551
552 const TapeEqSection& playback_eq(int channel = 0) const noexcept {
553 return channels_[static_cast<std::size_t>(channel)].playback_eq;
554 }
555
558 const std::vector<double>& gap_fir() const noexcept {
559 return channels_[0].gap_taps;
560 }
561
563 double reproduce_gap_m() const noexcept {
564 return tape::head_gap_geometry(speed_ips_).reproduce_m;
565 }
566
590 double reproduce_alignment_db() const noexcept {
591 return units::linear_to_db(channels_[0].alignment_gain);
592 }
593
594 double sample_rate() const noexcept { return sample_rate_; }
595
596private:
597 void recover_audio_fault() noexcept {
598 for (auto& channel : channels_) channel.recover_audio_fault();
599 compander_l_.reset();
600 compander_r_.reset();
601 }
602
605 struct PrintTaps {
606 int offset_samples = 0;
607 double post_gain = 0.0;
608 double pre_gain = 0.0;
609 bool pre_echo = false;
610 };
611
614 struct Channel {
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{};
623
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;
628
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;
634
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);
644 }
645 return std::hypot(re, im);
646 }
647
649 void align(double ceiling_db) noexcept {
650 const double tilt_db = chardelay::tape_loss_iir_magnitude_db(
651 physical.loss_parameters(), tape::kEqReferenceHz);
652 const double reused_gap =
653 fir_magnitude(physical.gap_coefficients(), tape::kEqReferenceHz, sample_rate);
654 const double own_gap = fir_magnitude(gap_taps, tape::kEqReferenceHz, sample_rate);
655 const double loss = units::db_to_linear(tilt_db) *
656 std::max(reused_gap, 1e-6) * std::max(own_gap, 1e-6);
657 alignment_gain = std::min(1.0 / std::max(loss, 1e-6),
658 units::db_to_linear(ceiling_db));
659 }
660
661 void prepare(double fs, int index) {
662 sample_rate = fs;
663 physical.prepare(fs);
664 instability.prepare(fs);
665 // Distinct seeds per channel, DERIVED from the shared constant
666 // rather than drawn, so the two channels decorrelate while both
667 // renders stay bit-identical across a reset (series law 2). The
668 // right channel also starts its flutter stack at the reused
669 // fixed phase offset.
670 const auto seed = static_cast<std::uint32_t>(
671 chardelay::kPrngSeed ^ (0x9E3779B9u * static_cast<std::uint32_t>(index)));
672 instability.configure(seed, index == 0 ? 0.0 : chardelay::kFlutterPhaseRight);
673 physical.set_seeds(seed);
674
675 nominal_samples = units::ms_to_samples(kInstabilityNominalMs, fs);
676 const auto capacity = static_cast<std::size_t>(
677 std::llround(nominal_samples + units::ms_to_samples(4.0, fs))) + 8u;
678 instability_line.prepare(capacity);
679
680 const auto print_capacity = static_cast<std::size_t>(std::llround(
682 print_line.assign(print_capacity, 0.0f);
683 print_write = 0;
684 print_valid = 0;
685 }
686
687 void reset() {
688 physical.reset();
689 instability.reset();
690 instability_line.reset();
691 gap_fir.reset();
692 bias_shelf.reset();
693 crosstalk_tilt.reset();
694 record_eq.reset();
695 playback_eq.reset();
696 std::fill(print_line.begin(), print_line.end(), 0.0f);
697 print_write = 0;
698 print_valid = 0;
699 }
700
701 void recover_audio_fault() noexcept {
702 physical.recover_audio_fault();
703 instability.reset();
704 instability_line.discard_history();
705 gap_fir.discard_history();
706 bias_shelf.reset();
707 crosstalk_tilt.reset();
708 record_eq.reset();
709 playback_eq.reset();
710 print_write = 0;
711 print_valid = 0;
712 }
713
714 void set_gap_taps(std::vector<double> taps) {
715 gap_taps = std::move(taps);
716 gap_fir.prepare(gap_taps.size());
717 }
718
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;
729 }
730
733 double playback(double x) noexcept {
734 gap_fir.push(x);
735 const double gapped = gap_taps.empty()
736 ? x
737 : gap_fir.convolve(gap_taps.data(), gap_taps.size());
738 const double lossy = physical.post_process(gapped) * alignment_gain;
739
740 instability.tick();
741 instability_line.push(lossy);
742 const double offset_ms = instability.offset_ms(wow_depth_ms, flutter_depth_ms);
743 const double read =
744 nominal_samples + units::ms_to_samples(offset_ms, sample_rate);
745 return instability_line.read(read);
746 }
747
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);
756
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]);
761 };
762
763 double y = 0.0;
764 if (taps.pre_echo) {
765 // Programme delayed by one offset; the print arrives at the head
766 // of the stream (early) and again two offsets back (late).
767 y = at(offset) + taps.pre_gain * at(0) + taps.post_gain * at(2u * offset);
768 } else {
769 y = at(0) + taps.post_gain * at(offset);
770 }
771 print_write = (print_write + 1u) % size;
772 if (print_valid < size) ++print_valid;
773 return y;
774 }
775 };
776
777 void update_speed(double ips, bool redesign) {
778 const bool changed = std::abs(ips - speed_ips_) > 1e-9;
779 speed_ips_ = ips;
780 if (!changed && !redesign) return;
781
782 const tape::HeadGapGeometry geometry = tape::head_gap_geometry(speed_ips_);
783 chardelay::TapeLossGeometry gap_geometry;
784 gap_geometry.speed_ips = speed_ips_;
785 gap_geometry.gap_m = geometry.reproduce_m;
786 // The reused minimum-phase design helper, given THIS module's speed-class
787 // reproduce gap rather than the delay tier's fixed 3 µm head. Same
788 // construction (cepstral fold), a different filter.
789 std::vector<double> taps = chardelay::design_tape_gap_fir(
790 sample_rate_, chardelay::tape_gap_fir_taps(sample_rate_), gap_geometry);
791
792 for (auto& channel : channels_) {
793 channel.set_gap_taps(taps);
794 channel.physical.set_speed_ips(speed_ips_);
795 channel.align(kAlignmentCeilingDb);
796 }
797 }
798
799 void update_eq() {
800 const tape::EqTimeConstants tc = tape::eq_time_constants(curve_, speed_ips_);
801 for (auto& channel : channels_) {
802 channel.record_eq.set(tc, sample_rate_, /*inverse=*/false);
803 channel.playback_eq.set(tc, sample_rate_, /*inverse=*/true);
804 }
805 }
806
816 void apply_age() {
817 for (auto& channel : channels_) {
818 channel.physical.update(age01_);
819 // Age moves the reused tier's spacing, so realignment belongs here
820 // and at every speed change — the same two events that move it on a
821 // real machine.
822 channel.align(kAlignmentCeilingDb);
823 }
824 print_through_db_ = tape::age_print_through_db(age01_);
825 update_bias();
826 }
827
828 void update_bias() {
829 // The age table's drift is ADDED to what the user asked for, then
830 // clamped — a tired machine biases hotter than its front panel says.
831 bias_ = std::clamp(bias_request_ + tape::age_bias_drift(age01_), kBiasMin, kBiasMax);
832
833 const double hf_shelf_db = -kOverBiasHfCutDb * std::max(0.0, bias_) +
834 kUnderBiasHfBoostDb * std::max(0.0, -bias_);
835 const double shelf_gain = units::db_to_linear(hf_shelf_db);
836 for (auto& channel : channels_)
837 channel.bias_shelf.set(kBiasShelfHz, shelf_gain, sample_rate_);
838
839 const double distortion_mult = 1.0 + kBiasDistK * bias_ * bias_;
840 drive_gain_ = units::db_to_linear(drive01_ * kDriveDbMax * distortion_mult);
841 inverse_drive_gain_ = 1.0 / drive_gain_;
842
843 // Wow and flutter depths come from the reused table, driven by this
844 // module's own age axis.
847 const double flutter = chardelay::interpolate_knots(
849 for (auto& channel : channels_) {
850 channel.wow_depth_ms = wow;
851 channel.flutter_depth_ms = flutter;
852 }
853 }
854
855 void update_crosstalk() {
856 const double tilt = units::db_to_linear(kCrosstalkTiltMaxDb);
857 for (auto& channel : channels_)
858 channel.crosstalk_tilt.set(kCrosstalkTiltHz, tilt, sample_rate_);
859 // The tilt shelf is normalized to unity at the same 1 kHz reference the
860 // EQ networks use, so a crosstalk measurement at 1 kHz reads the
861 // configured figure EXACTLY rather than the figure plus half a decibel
862 // of shelf skirt. Normalizing against the realized digital shelf, not
863 // its analog prototype, so the bilinear's frequency warping does not
864 // leak into that measurement. The stated ceiling becomes
865 // `crosstalk_db + kCrosstalkTiltMaxDb − (skirt at 1 kHz)`, which is
866 // strictly below the ceiling the spec asks for.
867 crosstalk_gain_ = units::db_to_linear(
868 crosstalk_db_ - tape::first_order_shelf_db(kCrosstalkTiltHz, tilt,
869 tape::kEqReferenceHz, sample_rate_));
870 }
871
872 void update_print_through() {
873 print_taps_.offset_samples = std::min(
874 static_cast<int>(std::llround(units::ms_to_samples(print_offset_ms_, sample_rate_))),
875 print_max_samples_);
876 const double reduction = kBiasErasureDb * std::max(0.0, bias_);
877 const double post_db = print_through_db_ - reduction;
878 print_taps_.post_gain = units::db_to_linear(post_db);
879 print_taps_.pre_gain = units::db_to_linear(post_db - kPreEchoAsymmetryDb);
880 print_taps_.pre_echo = pre_echo_;
881 }
882
883 std::array<Channel, 2> channels_{};
884 TapeCompanderT<SampleType> compander_l_{};
885 TapeCompanderT<SampleType> compander_r_{};
886
887 double sample_rate_ = 48000.0;
888 Archetype archetype_ = Archetype::ampex_350_440;
889 Curve curve_ = Curve::nab;
890 double speed_ips_ = 15.0;
891 double bias_request_ = kBiasDefault;
892 double bias_ = kBiasDefault;
893 double drive01_ = kDriveDefault;
894 double age01_ = 0.0;
895 double crosstalk_db_ = -38.0;
896 bool companding_ = false;
897 double print_through_db_ = kPrintThroughDbMin;
898 double print_offset_ms_ = kPrintThroughOffsetMsDefault;
899 bool pre_echo_ = false;
900 double mix_ = 1.0;
901
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;
908};
909
913
914} // namespace pulp::signal
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 &parameters, 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.