48 double age)
noexcept {
49 const double feedback = std::clamp(requested_feedback, 0.0,
55 const double over_unity =
57 loop_gain += (1.0 - loop_gain) * over_unity;
83 void set_seed(std::uint32_t seed)
noexcept { rng_.reseed(seed); }
87 std::size_t
state_index() const noexcept {
return state_index_; }
89 double process(
double x,
double depth)
noexcept {
90 if (--remaining_ <= 0) {
91 degraded_ = !degraded_;
96 const double target = degraded_ ? 1.0 : 0.0;
97 const double increment = 1.0 /
static_cast<double>(transition_samples_);
98 envelope_ = std::clamp(envelope_ + (target > envelope_ ? increment : -increment), 0.0, 1.0);
99 if (depth <= 0.0 || envelope_ <= 0.0) {
104 const double window = 0.5 * (1.0 - std::cos(
kPi * envelope_));
105 const double magnitude = std::pow(std::abs(x),
kChewPower);
106 const double shaped = lowpass_.lowpass((x < 0.0 ? -magnitude : magnitude));
107 return x + depth * window * (shaped - x);
111 int draw_duration(
double scale_seconds)
noexcept {
112 const double r = std::max(rng_.
uniform(), 1
e-6);
113 const double seconds = scale_seconds * std::pow(r,
kChewVariance);
114 return std::max(1,
static_cast<int>(seconds * sample_rate_));
119 double sample_rate_ = 48000.0;
120 double envelope_ = 0.0;
121 int transition_samples_ = 1;
123 bool degraded_ =
false;
124 std::size_t state_index_ = 0;
139 hysteresis_.prepare(fs);
142 design_.prepare(fs, speed_ips_);
143 previous_design_ = design_;
144 const std::size_t taps = design_.gap_taps().size();
145 active_.prepare(taps);
146 previous_.prepare(taps);
149 degrade_filter_.set_cutoff(20000.0, fs);
151 build_makeup_table();
158 playback_eq_.reset();
160 hiss_filter_.reset();
161 degrade_filter_.reset();
168 degrade_rng_.reset();
169 crossfade_remaining_ = 0;
175 playback_eq_.reset();
177 hiss_filter_.reset();
178 degrade_filter_.reset();
182 active_.discard_history();
183 previous_.discard_history();
185 degrade_rng_.reset();
186 crossfade_remaining_ = 0;
190 hiss_rng_.reseed(base ^ 0x9E3779B9u);
191 degrade_rng_.reseed(base ^ 0x85EBCA6Bu);
192 chew_.set_seed(base ^ 0xC2B2AE35u);
206 if (std::abs(clamped - speed_ips_) < 1
e-9)
return;
207 speed_ips_ = clamped;
212 if (design_.empty())
return;
215 previous_design_ = design_;
216 design_.set_speed(speed_ips_);
217 active_.copy_history_from(previous_);
218 active_.reset_recursive();
219 active_.set_parameters(design_.parameters_at(age_), sample_rate_);
220 crossfade_remaining_ = crossfade_samples_;
223 double speed_ips() const noexcept {
return speed_ips_; }
224 const std::vector<double>&
gap_coefficients() const noexcept {
return design_.gap_taps(); }
229 age_ = std::clamp(age, 0.0, 1.0);
233 hysteresis_.set_character(drive, bias);
234 makeup_ = makeup_for(age_);
242 bump_db, sample_rate_);
248 active_.set_parameters(design_.parameters_at(age_), sample_rate_);
252 const double nyquist_guard = 0.45 * sample_rate_;
253 const double top = std::min(20000.0, nyquist_guard);
254 const double degrade_hz = top * std::pow(
kDegradeMinLpHz / top, degrade_);
255 degrade_filter_.set_cutoff(degrade_hz, sample_rate_);
261 const double emphasized = record_eq_.process(x);
262 const double magnetized = hysteresis_.process(emphasized);
263 return playback_eq_.process(magnetized * makeup_);
269 double lossy = active_.process(design_);
270 if (crossfade_remaining_ > 0) {
272 const double outgoing = previous_.process(previous_design_);
273 const double t = 1.0 -
static_cast<double>(crossfade_remaining_) /
274 static_cast<double>(crossfade_samples_);
275 const double weight = 0.5 * (1.0 - std::cos(
kPi * t));
276 lossy = outgoing + weight * (lossy - outgoing);
277 --crossfade_remaining_;
280 double y = head_bump_.process(lossy);
281 y += hiss_filter_.lowpass(hiss_rng_.bipolar()) * hiss_gain_;
282 y = chew_.process(y, chew_depth_);
283 if (degrade_ > 0.0) {
284 y = degrade_filter_.lowpass(y) * degrade_gain_;
285 y += degrade_rng_.bipolar() * 1
e-4 * degrade_;
287 return dc_blocker_.highpass(y);
301 design_.gap_group_delay_samples() +
302 active_.dc_group_delay_seconds() * sample_rate_;
308 static constexpr std::size_t kMakeupKnots = 11;
316 void build_makeup_table() {
317 for (std::size_t knot = 0; knot < kMakeupKnots; ++knot) {
319 static_cast<double>(knot) /
static_cast<double>(kMakeupKnots - 1u);
327 constexpr double kProbeAmplitude = 0.02;
328 constexpr double kProbeHz = 1000.0;
329 const auto period_samples =
330 static_cast<int>(std::llround(sample_rate_ / kProbeHz));
331 double input_energy = 0.0;
332 double output_energy = 0.0;
333 for (
int n = 0; n < period_samples * 8; ++n) {
334 const double phase = 2.0 *
kPi *
static_cast<double>(n) /
static_cast<double>(period_samples);
335 const double in = kProbeAmplitude * std::sin(phase);
336 const double out = probe.
process(in);
337 if (n >= period_samples * 4) {
338 input_energy += in * in;
339 output_energy += out * out;
342 const double ratio = (output_energy > 1
e-30)
343 ? std::sqrt(input_energy / output_energy)
345 makeup_table_[knot] = std::clamp(
ratio, 0.05, 50.0);
349 double makeup_for(
double age)
const noexcept {
350 const double position =
351 std::clamp(age, 0.0, 1.0) *
static_cast<double>(kMakeupKnots - 1u);
352 auto lower =
static_cast<std::size_t
>(position);
353 if (lower >= kMakeupKnots - 1u) lower = kMakeupKnots - 2u;
354 const double t = position -
static_cast<double>(lower);
355 return makeup_table_[lower] +
t * (makeup_table_[lower + 1u] - makeup_table_[lower]);
358 FirstOrderShelf record_eq_;
359 FirstOrderShelf playback_eq_;
366 TapeLossDesign design_;
367 TapeLossDesign previous_design_;
368 TapeLossStage active_;
369 TapeLossStage previous_;
373 std::array<double, kMakeupKnots> makeup_table_{};
375 double sample_rate_ = 48000.0;
376 double speed_ips_ = 7.5;
378 double makeup_ = 1.0;
379 double hiss_gain_ = 0.0;
380 double chew_depth_ = 0.0;
381 double degrade_ = 0.0;
382 double degrade_gain_ = 1.0;
383 int crossfade_samples_ = 1;
384 int crossfade_remaining_ = 0;
Xorshift32 (Marsaglia, "Xorshift RNGs", J.
Definition rng.hpp:33
double process(double input)
Definition hysteresis.hpp:410
void prepare(double host_rate_hz)
Definition hysteresis.hpp:396
void set_character(double drive, double bias) noexcept
Definition hysteresis.hpp:406
static int latency_samples() noexcept
Definition hysteresis.hpp:392
Intermittent dropout.
Definition tape_physical.hpp:65
void prepare(double fs)
Definition tape_physical.hpp:67
std::size_t state_index() const noexcept
State-transition count since reset — the acceptance suite's determinism check compares this sequence ...
Definition tape_physical.hpp:87
void set_seed(std::uint32_t seed) noexcept
Definition tape_physical.hpp:83
double process(double x, double depth) noexcept
Definition tape_physical.hpp:89
void reset() noexcept
Definition tape_physical.hpp:74
The physical tier for one channel.
Definition tape_physical.hpp:128
double speed_ips() const noexcept
Definition tape_physical.hpp:223
void set_speed_ips(double ips)
Control-thread only: redesigns both filters and starts the crossfade.
Definition tape_physical.hpp:204
void recover_audio_fault() noexcept
Fresh-state recovery without clearing dynamically sized FIR storage.
Definition tape_physical.hpp:173
void prepare(double fs)
Definition tape_physical.hpp:130
static int oversampler_latency_samples() noexcept
Group delay the oversampling wrap adds inside the loop, in host samples.
Definition tape_physical.hpp:291
double pre_process(double x) noexcept
Record EQ → oversampled hysteresis → makeup → playback EQ.
Definition tape_physical.hpp:260
void reset() noexcept
Definition tape_physical.hpp:156
void update(double age) noexcept
Control-rate update. In this tier character_amount IS the age axis.
Definition tape_physical.hpp:228
void set_seeds(std::uint32_t base) noexcept
Definition tape_physical.hpp:189
const std::vector< double > & gap_coefficients() const noexcept
Definition tape_physical.hpp:224
double in_loop_delay_samples() const noexcept
Total in-loop delay this tier contributes ahead of the delay line's read: the oversampler's group del...
Definition tape_physical.hpp:299
std::size_t chew_state_index() const noexcept
Definition tape_physical.hpp:305
TapeLossIirParams loss_parameters() const noexcept
Definition tape_physical.hpp:225
double post_process(double x) noexcept
Wallace loss → head bump → hiss → chew → degrade → DC block.
Definition tape_physical.hpp:267
double uniform() noexcept
Uniform in [0, 1].
Definition primitives.hpp:87
@ OnePole
Definition sample_heritage_schema.hpp:50
constexpr std::array< double, 4 > kTapePhysicalFeedbackCompensation
Physical-loop compensation at each age knot.
Definition tables.hpp:152
OversampledHysteresis< HysteresisOversampler8x > OversampledHysteresis8x
Definition hysteresis.hpp:421
constexpr double kTapeEmphasisDb
Record/playback EQ bracketing the saturator: boost HF into saturation, cut it after,...
Definition tables.hpp:158
constexpr std::array< double, 5 > kHeadBumpIps
Head-bump center against speed.
Definition tables.hpp:513
constexpr std::array< double, 4 > kTapeBias
Definition tables.hpp:141
constexpr double kHissLpHz
Hiss: white noise through a 1-pole lowpass, injected AFTER the delay line so it accumulates per repea...
Definition tables.hpp:526
constexpr std::array< double, 4 > kTapeBumpDb
Definition tables.hpp:145
constexpr std::array< double, 5 > kTapeSpeedsIps
The IEC/NAB standard tape-speed set, in inches per second (published).
Definition tables.hpp:509
constexpr double kChewVariance
Definition tables.hpp:532
constexpr double kPi
Definition primitives.hpp:38
constexpr std::array< double, 3 > kAgeDegrade
Definition tables.hpp:522
constexpr std::array< double, 3 > kAgeAxis
Age macro.
Definition tables.hpp:518
constexpr double kChewCleanScaleS
Chew — an intermittent dropout modeled as a two-state machine.
Definition tables.hpp:530
constexpr std::array< double, 3 > kAgeChewDepth
Definition tables.hpp:521
constexpr double kTapeDcBlockHz
In-loop DC blocker.
Definition tables.hpp:168
constexpr double kTapeBumpQ
Definition tables.hpp:164
constexpr std::uint32_t kPrngSeed
PRNG seed.
Definition tables.hpp:92
constexpr double kTapeEmphasisHz
Definition tables.hpp:159
constexpr double kLossBankCrossfadeS
NOTE: there are no age knots and no interpolation along the age axis.
Definition tables.hpp:500
constexpr std::array< double, 4 > kTapeDrive
Definition tables.hpp:140
double interpolate_knots(const std::array< double, N > &axis, const std::array< double, N > &values, double c) noexcept
Definition primitives.hpp:125
constexpr double kChewLpHz
Definition tables.hpp:535
constexpr double kSaturatedFeedbackMax
Feedback ceiling for the saturating characters.
Definition tables.hpp:54
constexpr std::array< double, 3 > kAgeHissDbfs
Definition tables.hpp:520
constexpr double kDegradeGainDipDb
Definition tables.hpp:540
constexpr double kDegradeMinLpHz
Degrade — continuous wear.
Definition tables.hpp:539
constexpr std::array< double, 4 > kTapeAxis
Definition tables.hpp:139
constexpr double kChewPower
Definition tables.hpp:534
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 double kChewDegradedScaleS
Definition tables.hpp:531
constexpr std::array< double, 5 > kHeadBumpHz
Definition tables.hpp:514
constexpr double kChewTransitionS
Definition tables.hpp:533
@ feedback
Definition comb_filter.hpp:18
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ t
Definition input_events.hpp:41
The realized cascade's parameters in hertz, poles and shelves from both shapes concatenated.
Definition tape_loss.hpp:143