Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
fuzz_pair.hpp
Go to the documentation of this file.
1#pragma once
2
115
120#include <pulp/signal/rng.hpp>
121#include <pulp/signal/units.hpp>
122
123#include <algorithm>
124#include <cmath>
125#include <cstdint>
126
127namespace pulp::signal {
128
130enum class FuzzDevice : std::uint8_t {
133};
134
136template <typename SampleType = float>
138public:
140
141 // ── Device rows ───────────────────────────────────────────────────────
142 // Ideality factor sets knee SOFTNESS; saturation current sets where the
143 // knee falls. Both are honest [design parameter]s standing for a device
144 // family's published order of magnitude, never a measured part.
145
147 static constexpr double kGermaniumIdeality = 1.15;
149 static constexpr double kGermaniumSaturation = 5e-6;
151 static constexpr double kSiliconIdeality = 1.03;
153 static constexpr double kSiliconSaturation = 5e-12;
154
159
160 // ── Circuit constants ─────────────────────────────────────────────────
161
164 static constexpr double kNominalCollectorCurrent = 1.0;
165
169 static constexpr double kQuiescentCollector = 0.5;
170
197 static constexpr double kEmitterDegeneration = 0.18;
198
201 static constexpr double kStarveExponent = 1.6;
202
206 static constexpr double kMinAvailableCurrent = 1e-3;
207
211 static constexpr double kInputImpedanceKohm = 68.0;
212
216 static constexpr double kFeedbackConductanceMin = 0.05;
217 static constexpr double kFeedbackConductanceMax = 1.0;
218
221 static constexpr double kCouplingGain = 0.85;
222
228 static constexpr double kLoopGainCeiling = 0.94;
229
234 static constexpr int kSolverIterations = 24;
235
241 static constexpr double kNewtonStepThermal = 2.0;
242
246 static constexpr double kResidualTolerance = 1e-6;
247
277 static constexpr double kInputDriveRails = 0.4;
278
285 static constexpr double kGermaniumDriftOctaves = 0.6;
286 static constexpr double kSiliconDriftOctaves = 0.05;
287
292 static constexpr double kDcBlockerCornerHz = 12.0;
293
300 static constexpr int kOversampleFactor = 4;
301
302 // ── Lifecycle ─────────────────────────────────────────────────────────
303
304 FuzzPairT() { update(); }
305
307 void prepare(double sample_rate) {
308 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
309 using Os = OversamplerT<SampleType>;
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();
315
316 // The blocker runs at the OVERSAMPLED rate, so its pole is set for
317 // that rate: a corner low enough to pass the bass but high enough to
318 // clear the DC step a starved operating point introduces.
319 blocker_.set_pole(static_cast<SampleType>(
320 1.0 - kDcBlockerCornerHz * 6.283185307179586 /
321 (sample_rate_ * kOversampleFactor)));
322 drift_.prepare(sample_rate_ * kOversampleFactor);
323 update();
324 reset();
325 }
326
327 void reset() {
328 collector1_ = quiescent1_;
329 collector2_ = quiescent2_;
330 blocker_.reset();
331 drift_.reset();
332 oversampler_.reset();
333 worst_residual_ = 0.0;
334 }
335
336 // ── Controls ──────────────────────────────────────────────────────────
337
339 device_ = device;
340 update();
341 }
342
343 Device device() const { return device_; }
344
347 void set_fuzz(double amount01) {
348 if (!std::isfinite(amount01)) return;
349 fuzz_ = std::clamp(amount01, 0.0, 1.0);
350 update();
351 }
352
355 void set_bias_starve(double starve01) {
356 if (!std::isfinite(starve01)) return;
357 starve_ = std::clamp(starve01, 0.0, 1.0);
358 update();
359 }
360
367 void set_source_impedance_kohm(double kohm) {
368 if (!std::isfinite(kohm)) return;
369 source_kohm_ = std::clamp(kohm, 0.1, 1000.0);
370 update();
371 }
372
373 void set_output_level_db(double db) {
374 if (!std::isfinite(db)) return;
375 output_gain_ = units::db_to_linear(std::clamp(db, -24.0, 24.0));
376 }
377
378 void set_mix(double mix01) {
379 if (!std::isfinite(mix01)) return;
380 mix_ = std::clamp(mix01, 0.0, 1.0);
381 }
382
385 void set_oversampling_enabled(bool on) { oversample_ = on; }
386
387 void set_seed(std::uint32_t seed) { drift_.set_seed(seed); }
388
391 void set_drift_enabled(bool on) { drift_enabled_ = on; }
392
393 // ── Observables (for metering and for tests) ──────────────────────────
394
397 double loading_factor() const { return loading_; }
398
407 double bias_voltage() const { return knee2_; }
408
411 double base_bias_voltage() const { return base_bias2_; }
412
416 double quiescent_collector() const { return quiescent2_; }
417
421 double stage_gain() const { return stage_gain_; }
422
425 double input_scale_volts() const { return input_scale_; }
426
428 double available_current() const { return available_current_; }
429
455 double loop_gain() const {
456 const double stage1 = collector_load_ * (kNominalCollectorCurrent /
457 (ideality_ * kThermalVoltage)) /
458 degeneration1_;
459 const double stage2 =
460 collector_load_ * (available_current_ / (ideality_ * kThermalVoltage)) / degeneration2_;
461 return feedback_conductance_ * feedback_norm_ * kCouplingGain * kCouplingGain * stage1 *
462 stage2 * loading_;
463 }
464
467 double worst_residual() const { return worst_residual_; }
468
471 int latency_samples() const noexcept { return oversample_ ? latency_ : 0; }
472
473 // ── Processing ────────────────────────────────────────────────────────
474
475 SampleType process(SampleType input) {
476 // Every stage below is recursive. Treat a non-finite sample as a
477 // recovery boundary before it reaches either the oversampler or the
478 // coupled operating-point state.
479 if (!std::isfinite(static_cast<double>(input))) {
480 reset();
481 return SampleType{0};
482 }
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)));
486 };
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_;
490 // Also recover if an already-poisoned composed primitive or state ever
491 // reaches this boundary. `reset()` is RT-safe and clears every recursive
492 // state owned by the pair.
493 if (!std::isfinite(output)) {
494 reset();
495 return SampleType{0};
496 }
497 return static_cast<SampleType>(snap_to_zero(output));
498 }
499
500 void process_block(const SampleType* in, SampleType* out, int n) {
501 for (int i = 0; i < n; ++i) out[i] = process(in[i]);
502 }
503
504private:
510 double junction_current(double v_be) const { return network_.current(v_be); }
511
513 double transconductance(double v_be) const { return network_.conductance(v_be); }
514
518 double collector(double current) const {
519 return std::clamp(1.0 - collector_load_ * current, 0.0, 1.0);
520 }
521
522 struct PairState {
523 double first;
524 double second;
525 };
526
527 struct Residual {
528 double first;
529 double second;
530
531 double infinity_norm() const {
532 return std::max(std::abs(first), std::abs(second));
533 }
534 double euclidean_norm() const { return std::hypot(first, second); }
535 };
536
537 struct PairEvaluation {
538 PairState current;
539 PairState collector_voltage;
540 Residual residual;
541 };
542
543 PairEvaluation evaluate_pair(PairState voltage, double scale, double drive,
544 double feedback) const {
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{
549 voltage.first + kEmitterDegeneration * current.first -
550 (base_bias1_ + drive +
551 feedback * (collector_voltage.second - quiescent2_) * kCouplingGain),
552 voltage.second + kEmitterDegeneration * current.second -
553 (base_bias2_ + (collector_voltage.first - quiescent1_) * kCouplingGain)};
554 return {current, collector_voltage, residual};
555 }
556
557 void accept_if_better(PairState candidate, double scale, double drive,
558 double feedback, PairState& best, double& best_merit) const {
559 const double merit =
560 evaluate_pair(candidate, scale, drive, feedback).residual.euclidean_norm();
561 if (merit < best_merit) {
562 best = candidate;
563 best_merit = merit;
564 }
565 }
566
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);
574 if (point.residual.infinity_norm() < kResidualTolerance) return true;
575
576 const double gm1 = scale * transconductance(voltage.first);
577 const double gm2 = scale * transconductance(voltage.second);
578 // A clamped collector has zero coupling derivative; emitter
579 // degeneration remains active on the diagonal.
580 const double slope1 = point.collector_voltage.first > 0.0 &&
581 point.collector_voltage.first < 1.0
582 ? collector_load_ * gm1
583 : 0.0;
584 const double slope2 = point.collector_voltage.second > 0.0 &&
585 point.collector_voltage.second < 1.0
586 ? collector_load_ * gm2
587 : 0.0;
588 const double j11 = 1.0 + kEmitterDegeneration * gm1;
589 const double j22 = 1.0 + kEmitterDegeneration * gm2;
590 const double j12 = feedback * kCouplingGain * slope2;
591 const double j21 = kCouplingGain * slope1;
592 const double determinant = j11 * j22 - j12 * j21;
593 if (std::abs(determinant) <= 1e-12) return false;
594
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), 1e-30}));
602
603 const PairState gradient{
604 j11 * point.residual.first + j21 * point.residual.second,
605 j12 * point.residual.first + j22 * point.residual.second};
606 const double gradient_scale =
607 step_limit /
608 std::max({std::abs(gradient.first), std::abs(gradient.second), 1e-30});
609
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) {
617 // Preserve Newton's quadratic path in its local basin.
618 voltage = full_newton;
619 return false;
620 }
621
622 // At a rail kink Newton's active-set Jacobian can point across the
623 // kink. A fixed ladder along both Newton and residual-gradient
624 // directions keeps the cost bounded while retaining a descent option.
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},
629 scale, drive, feedback, best, best_merit);
630 accept_if_better({voltage.first - damping * gradient_scale * gradient.first,
631 voltage.second - damping * gradient_scale * gradient.second},
632 scale, drive, feedback, best, best_merit);
633 damping *= 0.5;
634 }
635 voltage = best;
636 return false;
637 }
638
680 double solve_stage(double input) {
681 // Thermal drift rides on the saturation current, which is the
682 // temperature-sensitive quantity — germanium's far more so than
683 // silicon's.
684 double saturation = saturation_;
685 if (drift_enabled_) {
686 const double octaves = device_ == Device::germanium ? kGermaniumDriftOctaves
688 saturation *= std::pow(2.0, octaves * (static_cast<double>(drift_.next()) - 1.0));
689 }
690 const double scale = saturation / saturation_;
691
692 const double drive = input * input_scale_ * loading_;
693
694 // The feedback path carries the source-impedance loading for the same
695 // reason the input does: it is the same node. One ratio, two
696 // consequences — and because it multiplies the loop here, the ceiling
697 // `loop_gain()` reports is the one the solver actually runs at.
698 const double beta = feedback_conductance_ * feedback_norm_ * loading_;
699
700 // Warm start. Both stages' base voltages are explicit given the PREVIOUS
701 // sample's collectors, so the per-stage equation `v + R_e·i(v) = base`
702 // can be pre-solved approximately before the coupled iteration begins.
703 // Without it the solve starts wherever the last sample left it, which on
704 // a transient is far enough away that the fixed iteration count does not
705 // recover: a junction Newton descends by only about one thermal voltage
706 // per step while it is deep in conduction, so an initial guess 400 mV
707 // high needs a dozen iterations to walk back.
708 //
709 // TWO estimates are taken and the smaller kept, because each is a bound
710 // that is tight in the regime the other is loose in, and both sit ABOVE
711 // the root (the base-voltage function is convex, so Newton from above
712 // descends monotonically — starting below it can overshoot into the
713 // steep region instead):
714 //
715 // * the linearised inverse — divide the base excursion by `1 + R_e·gm`
716 // at the bias point — which is exact for small excursions and
717 // increasingly conservative for large ones;
718 // * the conduction estimate `θ·ln(base/(R_e·Is))`, which assumes the
719 // whole base voltage is dropped across the degeneration. That is the
720 // right asymptote once the stage is driven hard, and it is what
721 // rescues the deeply starved settings: there the bias sits near
722 // cutoff, `1 + R_e·gm` is barely above 1, and the linear estimate
723 // alone lands hundreds of millivolts too high.
724 //
725 // The estimate is asked for at `R_e·scale` rather than `R_e` so the
726 // drift-perturbed saturation current is the one it inverts — the ratio
727 // it forms is `base/(R_e·Is·scale)`, which is the quantity that matters.
728 const double base1 =
729 base_bias1_ + drive + beta * (collector2_ - quiescent2_) * kCouplingGain;
730 const double base2 = base_bias2_ + (collector1_ - quiescent1_) * kCouplingGain;
731 const double degeneration_scale = kEmitterDegeneration * scale;
732 PairState voltage{
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))};
737
738 // Per-iteration step limit, in thermal voltages. The standard remedy
739 // for Newton on a junction: unlimited, a step taken where the
740 // exponential is steep lands somewhere it can take many iterations to
741 // climb back from.
742 const double step_limit = kNewtonStepThermal * ideality_ * kThermalVoltage;
743 bool converged = false;
744 for (int iter = 0; iter < kSolverIterations; ++iter) {
745 // The loop bound remains compile-time fixed for RT predictability;
746 // a converged sample simply skips further transcendental work.
747 if (!converged)
748 converged = solve_iteration(voltage, scale, drive, beta, step_limit);
749 }
750
751 const PairEvaluation solved = evaluate_pair(voltage, scale, drive, beta);
752
753 // The only state carried to the next sample. The junction voltages are
754 // NOT kept: the warm start re-derives them from these collectors, so
755 // holding them too would be a second copy of the same operating point
756 // that could disagree with this one after a parameter change.
757 collector1_ = solved.collector_voltage.first;
758 collector2_ = solved.collector_voltage.second;
759 worst_residual_ = std::max(worst_residual_, solved.residual.infinity_norm());
760
761 // The output is Q2's collector deviation — bounded to the rail by
762 // construction, which is also where the hard fuzz clipping comes from.
763 return static_cast<double>(
764 blocker_.process(static_cast<SampleType>(collector2_ - quiescent2_)));
765 }
766
767 void update() {
768 if (device_ == Device::germanium) {
769 ideality_ = kGermaniumIdeality;
770 saturation_ = kGermaniumSaturation;
771 } else {
772 ideality_ = kSiliconIdeality;
773 saturation_ = kSiliconSaturation;
774 }
775 network_.ideality = ideality_;
776 network_.saturation_current = saturation_;
777 network_.leg_a = 1.0;
778 // A transistor's base-emitter junction conducts one way only.
779 network_.leg_b = 0.0;
780
781 available_current_ =
782 std::max(kNominalCollectorCurrent * std::pow(1.0 - starve_, kStarveExponent),
784
785 // The collector load puts the UNSTARVED quiescent collector mid-rail.
786 collector_load_ = (1.0 - kQuiescentCollector) / kNominalCollectorCurrent;
787
788 // One ratio, two consequences: attenuation and loop-gain loading.
789 loading_ = kInputImpedanceKohm / (source_kohm_ + kInputImpedanceKohm);
790
791 // ── The two operating points ──────────────────────────────────────
792 //
793 // Starvation is applied to STAGE 2 ONLY, because that is what it is: the
794 // supply feeding Q2's collector node runs down. Stage 1 keeps its
795 // healthy bias throughout.
796 //
797 // Starving both stages together is the tempting simplification and it
798 // quietly removes two of the three behaviours the starve control exists
799 // for. With both stages down, the loop gain falls as the SQUARE of the
800 // current, so the feedback path is gone long before the operating point
801 // is asymmetric enough to fold — yet the feedback path amplifying the
802 // asymmetry is precisely the stated mechanism for octave-up. And a
803 // uniformly quiet pair is not a gate: what gates is a healthy first
804 // stage still swinging its full rail into a second stage that can no
805 // longer follow it.
806 //
807 // The bias of each stage is its own conduction knee at its own current,
808 // INVERTED FROM THE SAME LAW the forward path uses rather than written
809 // out again — so the documented 363 mV germanium / 693 mV silicon
810 // figures are consequences of the shipped `(n, Is)` rows and cannot
811 // drift from them. The base sits one emitter drop above that knee.
812 knee1_ = network_.knee_voltage(kNominalCollectorCurrent);
813 knee2_ = network_.knee_voltage(available_current_);
814 base_bias1_ = knee1_ + kEmitterDegeneration * kNominalCollectorCurrent;
815 base_bias2_ = knee2_ + kEmitterDegeneration * available_current_;
816
817 const double gm1 = kNominalCollectorCurrent / (ideality_ * kThermalVoltage);
818 const double gm2 = available_current_ / (ideality_ * kThermalVoltage);
819 degeneration1_ = 1.0 + gm1 * kEmitterDegeneration;
820 degeneration2_ = 1.0 + gm2 * kEmitterDegeneration;
821
822 // Where each collector actually rests. Starving raises Stage 2's toward
823 // the supply rail, because less current means less drop across the load
824 // — that asymmetric headroom is what folds the waveform and produces the
825 // octave, so it must not be assumed away.
826 //
827 // Every coupling in the solve is taken as a deviation from THESE, not
828 // from `kQuiescentCollector`. Referring them to the unstarved mid-rail
829 // instead injects a DC step of `R_c·(I_nominal − I_available)` — nearly
830 // half a rail at full starvation — into the next stage's base, which
831 // drives the starved stage hard ON. That is backwards from what
832 // starvation means, and it is also what makes the solve intractable
833 // there: the junction is pushed hundreds of millivolts past its knee
834 // every sample, where a fixed iteration count cannot follow.
835 quiescent1_ = kQuiescentCollector;
836 quiescent2_ = std::clamp(1.0 - collector_load_ * available_current_, 0.0, 1.0);
837
838 // The stage gain, and with it the input scale, come from STAGE 1 at its
839 // nominal operating point — the stage the input actually drives, and one
840 // whose bias the starve control does not touch. Referring the drive to
841 // the starved stage instead would raise it by exactly the factor
842 // starvation lowered that stage's gain, and the starve control would do
843 // almost nothing.
844 stage_gain_ = collector_load_ * gm1 / degeneration1_;
845 const double rail_to_rail = 1.0 / std::max(stage_gain_, 1e-12);
846 input_scale_ = kInputDriveRails * rail_to_rail;
847
848 feedback_conductance_ =
850
851 // The ceiling is enforced STRUCTURALLY rather than hoped for: the
852 // feedback path is normalised so that the worst case over the whole
853 // parameter space — maximum feedback, healthy bias, zero source
854 // impedance — lands exactly on `kLoopGainCeiling`. The specification
855 // describes this as "clamping feedback_conductance at the top of its
856 // travel such that the measured maximum never reaches 1.0"; deriving
857 // the normaliser is that clamp, with the arithmetic done once instead
858 // of a magic number chosen by trial.
859 //
860 // The worst case uses the SMALLER ideality factor, because a smaller
861 // one means a steeper exponential and therefore more transconductance
862 // at the same current — silicon is the binding device, not germanium.
863 // Degeneration narrows that margin (it divides out most of a `gm`
864 // difference) without reversing it, so the binding device is unchanged.
865 constexpr double worst_ideality = std::min(kGermaniumIdeality, kSiliconIdeality);
866 const double worst_gm = kNominalCollectorCurrent / (worst_ideality * kThermalVoltage);
867 const double worst_gm_eff = worst_gm / (1.0 + worst_gm * kEmitterDegeneration);
868 const double worst_stage = collector_load_ * worst_gm_eff;
869 const double worst_open_loop =
870 kCouplingGain * kCouplingGain * worst_stage * worst_stage;
871 feedback_norm_ =
872 kLoopGainCeiling / std::max(worst_open_loop * kFeedbackConductanceMax, 1e-12);
873 }
874
875 Device device_ = Device::germanium;
876 double sample_rate_ = 44100.0;
877 double fuzz_ = 0.65;
878 double starve_ = 0.0;
879 double source_kohm_ = 10.0;
880 double mix_ = 1.0;
881 double output_gain_ = 1.0;
882 bool oversample_ = true;
883 bool drift_enabled_ = false;
884
885 junction::JunctionPair network_{};
886 double ideality_ = kGermaniumIdeality;
887 double saturation_ = kGermaniumSaturation;
888 double available_current_ = kNominalCollectorCurrent;
889 double knee1_ = 0.0;
890 double knee2_ = 0.0;
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;
901 int latency_ = 0;
902
903 double quiescent1_ = kQuiescentCollector;
904 double quiescent2_ = kQuiescentCollector;
905 double collector1_ = kQuiescentCollector;
906 double collector2_ = kQuiescentCollector;
907 double worst_residual_ = 0.0;
908
909 DcBlocker<SampleType> blocker_{};
910 DriftT<double> drift_{};
911 OversamplerT<SampleType> oversampler_{};
912};
913
916
917} // namespace pulp::signal
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.