136template <
typename SampleType =
float>
308 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
310 oversampler_.
set_kind(Os::Kind::linear_phase_fir);
311 oversampler_.set_quality(Os::Quality::standard);
312 oversampler_.set_factor(Os::Factor::x4);
313 oversampler_.set_sample_rate(
static_cast<SampleType
>(sample_rate_));
314 latency_ = oversampler_.latency_samples();
319 blocker_.set_pole(
static_cast<SampleType
>(
328 collector1_ = quiescent1_;
329 collector2_ = quiescent2_;
332 oversampler_.reset();
333 worst_residual_ = 0.0;
348 if (!std::isfinite(amount01))
return;
349 fuzz_ = std::clamp(amount01, 0.0, 1.0);
356 if (!std::isfinite(starve01))
return;
357 starve_ = std::clamp(starve01, 0.0, 1.0);
368 if (!std::isfinite(kohm))
return;
369 source_kohm_ = std::clamp(kohm, 0.1, 1000.0);
374 if (!std::isfinite(db))
return;
379 if (!std::isfinite(mix01))
return;
380 mix_ = std::clamp(mix01, 0.0, 1.0);
387 void set_seed(std::uint32_t seed) { drift_.set_seed(seed); }
459 const double stage2 =
460 collector_load_ * (available_current_ / (ideality_ *
kThermalVoltage)) / degeneration2_;
479 if (!std::isfinite(
static_cast<double>(input))) {
481 return SampleType{0};
483 const double dry =
static_cast<double>(input);
484 const auto stage = [
this](SampleType x) {
485 return static_cast<SampleType
>(solve_stage(
static_cast<double>(x)));
487 const double wet = oversample_ ?
static_cast<double>(oversampler_.process(input, stage))
488 :
static_cast<double>(stage(input));
489 const double output = (dry + mix_ * (wet - dry)) * output_gain_;
493 if (!std::isfinite(output)) {
495 return SampleType{0};
510 double junction_current(
double v_be)
const {
return network_.
current(v_be); }
513 double transconductance(
double v_be)
const {
return network_.
conductance(v_be); }
518 double collector(
double current)
const {
519 return std::clamp(1.0 - collector_load_ * current, 0.0, 1.0);
531 double infinity_norm()
const {
532 return std::max(std::abs(first), std::abs(second));
534 double euclidean_norm()
const {
return std::hypot(first, second); }
537 struct PairEvaluation {
539 PairState collector_voltage;
543 PairEvaluation evaluate_pair(PairState voltage,
double scale,
double drive,
545 const PairState current{
scale * junction_current(voltage.first),
546 scale * junction_current(voltage.second)};
547 const PairState collector_voltage{collector(current.first), collector(current.second)};
548 const Residual residual{
550 (base_bias1_ +
drive +
553 (base_bias2_ + (collector_voltage.first - quiescent1_) *
kCouplingGain)};
554 return {current, collector_voltage, residual};
557 void accept_if_better(PairState candidate,
double scale,
double drive,
558 double feedback, PairState& best,
double& best_merit)
const {
560 evaluate_pair(candidate, scale, drive,
feedback).residual.euclidean_norm();
561 if (merit < best_merit) {
571 bool solve_iteration(PairState& voltage,
double scale,
double drive,
572 double feedback,
double step_limit)
const {
573 const PairEvaluation point = evaluate_pair(voltage, scale, drive,
feedback);
576 const double gm1 =
scale * transconductance(voltage.first);
577 const double gm2 =
scale * transconductance(voltage.second);
580 const double slope1 = point.collector_voltage.first > 0.0 &&
581 point.collector_voltage.first < 1.0
582 ? collector_load_ * gm1
584 const double slope2 = point.collector_voltage.second > 0.0 &&
585 point.collector_voltage.second < 1.0
586 ? collector_load_ * gm2
595 const PairState newton_step{
596 (j22 * point.residual.first - j12 * point.residual.second) /
determinant,
597 (j11 * point.residual.second - j21 * point.residual.first) /
determinant};
598 const double newton_scale =
599 std::min(1.0, step_limit /
600 std::max({std::abs(newton_step.first),
601 std::abs(newton_step.second), 1
e-30}));
604 j11 * point.residual.first + j21 * point.residual.second,
605 j12 * point.residual.first + j22 * point.residual.second};
606 const double gradient_scale =
610 PairState best = voltage;
611 double best_merit = point.residual.euclidean_norm();
612 const PairState full_newton{voltage.first - newton_scale * newton_step.first,
613 voltage.second - newton_scale * newton_step.second};
614 const double full_newton_merit =
615 evaluate_pair(full_newton, scale, drive,
feedback).residual.euclidean_norm();
616 if (full_newton_merit < best_merit) {
618 voltage = full_newton;
625 double damping = 1.0;
626 for (
int trial = 0; trial < 6; ++trial) {
627 accept_if_better({voltage.first - damping * newton_scale * newton_step.first,
628 voltage.second - damping * newton_scale * newton_step.second},
630 accept_if_better({voltage.first - damping * gradient_scale *
gradient.first,
631 voltage.second - damping * gradient_scale *
gradient.second},
680 double solve_stage(
double input) {
684 double saturation = saturation_;
685 if (drift_enabled_) {
688 saturation *= std::pow(2.0, octaves * (
static_cast<double>(drift_.next()) - 1.0));
690 const double scale = saturation / saturation_;
692 const double drive = input * input_scale_ * loading_;
698 const double beta = feedback_conductance_ * feedback_norm_ * loading_;
730 const double base2 = base_bias2_ + (collector1_ - quiescent1_) *
kCouplingGain;
733 std::min(knee1_ + (base1 - base_bias1_) / degeneration1_,
734 network_.conduction_estimate(base1, degeneration_scale)),
735 std::min(knee2_ + (base2 - base_bias2_) / degeneration2_,
736 network_.conduction_estimate(base2, degeneration_scale))};
743 bool converged =
false;
748 converged = solve_iteration(voltage, scale, drive, beta, step_limit);
751 const PairEvaluation solved = evaluate_pair(voltage, scale, drive, beta);
757 collector1_ = solved.collector_voltage.first;
758 collector2_ = solved.collector_voltage.second;
759 worst_residual_ = std::max(worst_residual_, solved.residual.infinity_norm());
763 return static_cast<double>(
764 blocker_.process(
static_cast<SampleType
>(collector2_ - quiescent2_)));
768 if (device_ == Device::germanium) {
775 network_.ideality = ideality_;
776 network_.saturation_current = saturation_;
777 network_.leg_a = 1.0;
779 network_.leg_b = 0.0;
813 knee2_ = network_.knee_voltage(available_current_);
836 quiescent2_ = std::clamp(1.0 - collector_load_ * available_current_, 0.0, 1.0);
844 stage_gain_ = collector_load_ * gm1 / degeneration1_;
845 const double rail_to_rail = 1.0 / std::max(stage_gain_, 1
e-12);
848 feedback_conductance_ =
868 const double worst_stage = collector_load_ * worst_gm_eff;
869 const double worst_open_loop =
875 Device device_ = Device::germanium;
876 double sample_rate_ = 44100.0;
878 double starve_ = 0.0;
879 double source_kohm_ = 10.0;
881 double output_gain_ = 1.0;
882 bool oversample_ =
true;
883 bool drift_enabled_ =
false;
885 junction::JunctionPair network_{};
891 double base_bias1_ = 0.0;
892 double base_bias2_ = 0.0;
893 double degeneration1_ = 1.0;
894 double degeneration2_ = 1.0;
895 double stage_gain_ = 1.0;
896 double collector_load_ = 0.5;
897 double loading_ = 1.0;
898 double input_scale_ = 1.0;
899 double feedback_conductance_ = 0.0;
900 double feedback_norm_ = 1.0;
907 double worst_residual_ = 0.0;
909 DcBlocker<SampleType> blocker_{};
910 DriftT<double> drift_{};
911 OversamplerT<SampleType> oversampler_{};
The two-transistor direct-coupled feedback pair.
Definition fuzz_pair.hpp:137
void set_seed(std::uint32_t seed)
Definition fuzz_pair.hpp:387
static constexpr double kNominalCollectorCurrent
Definition fuzz_pair.hpp:164
static constexpr double kQuiescentCollector
Definition fuzz_pair.hpp:169
static constexpr double kFeedbackConductanceMin
Definition fuzz_pair.hpp:216
static constexpr double kThermalVoltage
Definition fuzz_pair.hpp:158
void set_mix(double mix01)
Definition fuzz_pair.hpp:378
void set_oversampling_enabled(bool on)
Test-only escape hatch: runs the nonlinearity at the base rate so the aliasing the oversampler remove...
Definition fuzz_pair.hpp:385
static constexpr double kInputDriveRails
Definition fuzz_pair.hpp:277
void set_drift_enabled(bool on)
Enables the seeded thermal-drift channel.
Definition fuzz_pair.hpp:391
void process_block(const SampleType *in, SampleType *out, int n)
Definition fuzz_pair.hpp:500
static constexpr double kSiliconSaturation
Definition fuzz_pair.hpp:153
static constexpr double kLoopGainCeiling
Definition fuzz_pair.hpp:228
void set_bias_starve(double starve01)
0 = healthy supply, 1 = fully starved.
Definition fuzz_pair.hpp:355
double base_bias_voltage() const
The starved stage's quiescent BASE voltage: its junction knee plus the drop its quiescent current dev...
Definition fuzz_pair.hpp:411
void reset()
Definition fuzz_pair.hpp:327
double available_current() const
Available collector current after starvation.
Definition fuzz_pair.hpp:428
static constexpr double kNewtonStepThermal
Definition fuzz_pair.hpp:241
int latency_samples() const noexcept
Reported exactly, never estimated (series law 5).
Definition fuzz_pair.hpp:471
void set_fuzz(double amount01)
Feedback-network engagement, 0..1.
Definition fuzz_pair.hpp:347
static constexpr double kStarveExponent
Definition fuzz_pair.hpp:201
double loading_factor() const
The divider ratio the source impedance produces.
Definition fuzz_pair.hpp:397
FuzzPairT()
Definition fuzz_pair.hpp:304
static constexpr int kSolverIterations
Definition fuzz_pair.hpp:234
static constexpr double kGermaniumSaturation
Definition fuzz_pair.hpp:149
FuzzDevice Device
Definition fuzz_pair.hpp:139
double quiescent_collector() const
Where the starved stage's collector rests.
Definition fuzz_pair.hpp:416
void set_device(Device device)
Definition fuzz_pair.hpp:338
double worst_residual() const
The largest solver residual seen since reset() — so the fixed iteration count's adequacy is verified ...
Definition fuzz_pair.hpp:467
static constexpr double kSiliconIdeality
Definition fuzz_pair.hpp:151
static constexpr double kResidualTolerance
Definition fuzz_pair.hpp:246
Device device() const
Definition fuzz_pair.hpp:343
static constexpr double kEmitterDegeneration
Definition fuzz_pair.hpp:197
static constexpr double kCouplingGain
Definition fuzz_pair.hpp:221
double loop_gain() const
The linearised small-signal loop gain at the CURRENT operating point.
Definition fuzz_pair.hpp:455
static constexpr double kGermaniumIdeality
Definition fuzz_pair.hpp:147
SampleType process(SampleType input)
Definition fuzz_pair.hpp:475
static constexpr double kMinAvailableCurrent
Definition fuzz_pair.hpp:206
static constexpr double kSiliconDriftOctaves
Definition fuzz_pair.hpp:286
static constexpr double kGermaniumDriftOctaves
Definition fuzz_pair.hpp:285
static constexpr double kFeedbackConductanceMax
Definition fuzz_pair.hpp:217
double stage_gain() const
One stage's small-signal gain R_c·gm_eff at the nominal operating point.
Definition fuzz_pair.hpp:421
void set_output_level_db(double db)
Definition fuzz_pair.hpp:373
double input_scale_volts() const
Volts of base drive a full-scale input produces, derived from the nominal stage's own rail-to-rail sw...
Definition fuzz_pair.hpp:425
void set_source_impedance_kohm(double kohm)
The Thévenin source impedance at the input jack: pickup, cable and — the part that matters — whatever...
Definition fuzz_pair.hpp:367
static constexpr int kOversampleFactor
Definition fuzz_pair.hpp:300
static constexpr double kInputImpedanceKohm
Definition fuzz_pair.hpp:211
void prepare(double sample_rate)
Builds the oversampler's filter state. May allocate.
Definition fuzz_pair.hpp:307
double bias_voltage() const
The STARVED stage's quiescent junction voltage: the conduction knee at whatever current is still avai...
Definition fuzz_pair.hpp:407
static constexpr double kDcBlockerCornerHz
Definition fuzz_pair.hpp:292
Definition oversampling.hpp:78
void set_kind(Kind kind)
Definition oversampling.hpp:121
The semiconductor junction law, once.
@ gradient
GradientAtlas — evaluated gradient ramps (row-packed).
Definition atlas_inventory.hpp:42
@ drive
Definition config.hpp:223
constexpr double kThermalVoltage
Thermal voltage k·T/q at 300 K, in volts.
Definition junction.hpp:69
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
@ feedback
Definition comb_filter.hpp:18
FuzzDevice
The two device families. They differ only in (n, Is).
Definition fuzz_pair.hpp:130
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
SampleType determinant(const Matrix< 2, SampleType > &m)
Determinant of a 2x2 matrix.
Definition matrix.hpp:84
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ silicon
Small-signal switching class. Clips ~0.6 V, hard.
Definition distortion.hpp:113
@ germanium
Much larger Is: turns on earlier and softer.
Definition distortion.hpp:114
FuzzPairT< float > FuzzPair
Definition fuzz_pair.hpp:914
FuzzPairT< double > FuzzPair64
Definition fuzz_pair.hpp:915
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
@ candidate
Definition waveform_editor_primitives.hpp:193
Deterministic randomness for modulation: a counter-free PRNG, a Gaussian draw, a stateless purpose-ke...
double conductance(double v) const
di/dv — the incremental conductance a Newton solve needs.
Definition junction.hpp:123
double current(double v) const
i(v).
Definition junction.hpp:116
The real-unit vocabulary shared by every DSP block in the catalog.