Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::signal::FuzzPairT< SampleType > Class Template Reference

The two-transistor direct-coupled feedback pair. More...

#include <fuzz_pair.hpp>

Public Types

using Device = FuzzDevice

Public Member Functions

 FuzzPairT ()
void prepare (double sample_rate)
 Builds the oversampler's filter state. May allocate.
void reset ()
void set_device (Device device)
Device device () const
void set_fuzz (double amount01)
 Feedback-network engagement, 0..1.
void set_bias_starve (double starve01)
 0 = healthy supply, 1 = fully starved.
void set_source_impedance_kohm (double kohm)
 The Thévenin source impedance at the input jack: pickup, cable and — the part that matters — whatever the guitar's volume pot contributes as it is rolled back.
void set_output_level_db (double db)
void set_mix (double mix01)
void set_oversampling_enabled (bool on)
 Test-only escape hatch: runs the nonlinearity at the base rate so the aliasing the oversampler removes can be measured.
void set_seed (std::uint32_t seed)
void set_drift_enabled (bool on)
 Enables the seeded thermal-drift channel.
double loading_factor () const
 The divider ratio the source impedance produces.
double bias_voltage () const
 The STARVED stage's quiescent junction voltage: the conduction knee at whatever current is still available to it.
double base_bias_voltage () const
 The starved stage's quiescent BASE voltage: its junction knee plus the drop its quiescent current develops across the emitter degeneration.
double quiescent_collector () const
 Where the starved stage's collector rests.
double stage_gain () const
 One stage's small-signal gain R_c·gm_eff at the nominal operating point.
double input_scale_volts () const
 Volts of base drive a full-scale input produces, derived from the nominal stage's own rail-to-rail swing.
double available_current () const
 Available collector current after starvation.
double loop_gain () const
 The linearised small-signal loop gain at the CURRENT operating point.
double worst_residual () const
 The largest solver residual seen since reset() — so the fixed iteration count's adequacy is verified rather than assumed.
int latency_samples () const noexcept
 Reported exactly, never estimated (series law 5).
SampleType process (SampleType input)
void process_block (const SampleType *in, SampleType *out, int n)

Static Public Attributes

static constexpr double kGermaniumIdeality = 1.15
 [design parameter] germanium n 1.15 (range 1.05 .. 1.30).
static constexpr double kGermaniumSaturation = 5e-6
 [design parameter] germanium Is 5e-6 calibration units (range 1e-7 .. 5e-5).
static constexpr double kSiliconIdeality = 1.03
 [design parameter] silicon n 1.03 (range 1.00 .. 1.08).
static constexpr double kSiliconSaturation = 5e-12
 [design parameter] silicon Is 5e-12 calibration units (range 1e-13 .. 5e-11).
static constexpr double kThermalVoltage = junction::kThermalVoltage
 The shared thermal voltage.
static constexpr double kNominalCollectorCurrent = 1.0
 Quiescent collector current, the unit everything else is relative to.
static constexpr double kQuiescentCollector = 0.5
 Where the quiescent collector sits on the normalised rail.
static constexpr double kEmitterDegeneration = 0.18
 Emitter degeneration R_e, in the same normalised volts-per-calibration- unit as the collector load.
static constexpr double kStarveExponent = 1.6
 Starvation exponent: how sharply available current falls with the starve control.
static constexpr double kMinAvailableCurrent = 1e-3
 Floor on available current, so a fully starved stage is near cutoff rather than exactly at it (which would make the bias voltage −inf).
static constexpr double kInputImpedanceKohm = 68.0
 The fuzz stage's own input impedance at Q1's base, in kΩ.
static constexpr double kFeedbackConductanceMin = 0.05
 Feedback conductance travel.
static constexpr double kFeedbackConductanceMax = 1.0
static constexpr double kCouplingGain = 0.85
 Interstage coupling loss, Q1's collector into Q2's base.
static constexpr double kLoopGainCeiling = 0.94
 The enforced loop-gain ceiling.
static constexpr int kSolverIterations = 24
 Fixed solver iterations.
static constexpr double kNewtonStepThermal = 2.0
 Per-iteration Newton step limit, in thermal voltages.
static constexpr double kResidualTolerance = 1e-6
 Residual tolerance the fixed iteration count must clear across the whole parameter sweep.
static constexpr double kInputDriveRails = 0.4
 How far a full-scale input drives the first stage, measured in RAIL-TO-RAIL SWINGS of that stage rather than in volts.
static constexpr double kGermaniumDriftOctaves = 0.6
 Thermal-drift depth, in octaves of saturation current.
static constexpr double kSiliconDriftOctaves = 0.05
static constexpr double kDcBlockerCornerHz = 12.0
 DC-blocker corner, in Hz.
static constexpr int kOversampleFactor = 4
 Oversampling factor.

Detailed Description

template<typename SampleType = float>
class pulp::signal::FuzzPairT< SampleType >

The two-transistor direct-coupled feedback pair.

Member Typedef Documentation

◆ Device

template<typename SampleType = float>
using pulp::signal::FuzzPairT< SampleType >::Device = FuzzDevice

Constructor & Destructor Documentation

◆ FuzzPairT()

template<typename SampleType = float>
pulp::signal::FuzzPairT< SampleType >::FuzzPairT ( )
inline

Member Function Documentation

◆ available_current()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::available_current ( ) const
inline

Available collector current after starvation.

◆ base_bias_voltage()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::base_bias_voltage ( ) const
inline

The starved stage's quiescent BASE voltage: its junction knee plus the drop its quiescent current develops across the emitter degeneration.

◆ bias_voltage()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::bias_voltage ( ) const
inline

The STARVED stage's quiescent junction voltage: the conduction knee at whatever current is still available to it.

Unstarved this is the §3 worked example's value — ~363 mV germanium, ~693 mV silicon — because unstarved is exactly the nominal current. The base sits one emitter drop above it, see base_bias_voltage().

This reports Stage 2 because Stage 2 is the one the starve control moves; Stage 1 stays at the nominal knee by construction.

◆ device()

template<typename SampleType = float>
Device pulp::signal::FuzzPairT< SampleType >::device ( ) const
inline

◆ input_scale_volts()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::input_scale_volts ( ) const
inline

Volts of base drive a full-scale input produces, derived from the nominal stage's own rail-to-rail swing.

◆ latency_samples()

template<typename SampleType = float>
int pulp::signal::FuzzPairT< SampleType >::latency_samples ( ) const
inlinenoexcept

Reported exactly, never estimated (series law 5).

A linear-phase FIR pair cannot be zero-latency, so this is not claimed to be.

◆ loading_factor()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::loading_factor ( ) const
inline

The divider ratio the source impedance produces.

Both the attenuation and the loop-gain loading factor — one number, deliberately.

◆ loop_gain()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::loop_gain ( ) const
inline

The linearised small-signal loop gain at the CURRENT operating point.

This is both the stability margin and the solver's contraction factor. The linearised small-signal loop gain at the DC operating point — the quantity kLoopGainCeiling bounds, and the one the parameter sweep asserts.

It is β · g_couple² · (R_c·gm_eff1) · (R_c·gm_eff2) · loading, where gm_eff = gm/(1 + gm·R_e) is the DEGENERATED transconductance at each stage's own operating current. The two differ under starvation, which is why the loop gain falls only in proportion to the surviving stage rather than as its square — the feedback path stays alive far enough into starvation to amplify the operating point's asymmetry, which is the stated mechanism for the octave-up fold.

Two collector loads appear because the loop passes through both stages' collector nodes; the specification's own formula omits them, which is what leaves it unbounded (it evaluates to ~560 at the defaults). Loading multiplies it, so a higher source impedance LOWERS the loop gain — the behaviour the spec's acceptance test 2 demands, though its §5.2 prose says the opposite. See adjudication A-14.

This is not a separate formula bolted alongside the solve: it is exactly the off-diagonal product over the diagonal product of the Jacobian in solve_stage, so the number reported here IS the solver's contraction factor rather than an estimate of it.

◆ prepare()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::prepare ( double sample_rate)
inline

Builds the oversampler's filter state. May allocate.

◆ process()

template<typename SampleType = float>
SampleType pulp::signal::FuzzPairT< SampleType >::process ( SampleType input)
inline

◆ process_block()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::process_block ( const SampleType * in,
SampleType * out,
int n )
inline

◆ quiescent_collector()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::quiescent_collector ( ) const
inline

Where the starved stage's collector rests.

Unstarved it is mid-rail; starving walks it toward the supply rail, and the headroom asymmetry that creates is what folds the waveform into even harmonics.

◆ reset()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::reset ( )
inline

◆ set_bias_starve()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_bias_starve ( double starve01)
inline

0 = healthy supply, 1 = fully starved.

Lowers the quiescent bias toward cutoff, which is what makes the stage gate rather than go quiet.

◆ set_device()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_device ( Device device)
inline

◆ set_drift_enabled()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_drift_enabled ( bool on)
inline

Enables the seeded thermal-drift channel.

Off by default so a render is exactly repeatable without relying on the seed discipline.

◆ set_fuzz()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_fuzz ( double amount01)
inline

Feedback-network engagement, 0..1.

At 1 the loop gain approaches the tested ceiling.

◆ set_mix()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_mix ( double mix01)
inline

◆ set_output_level_db()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_output_level_db ( double db)
inline

◆ set_oversampling_enabled()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_oversampling_enabled ( bool on)
inline

Test-only escape hatch: runs the nonlinearity at the base rate so the aliasing the oversampler removes can be measured.

◆ set_seed()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_seed ( std::uint32_t seed)
inline

◆ set_source_impedance_kohm()

template<typename SampleType = float>
void pulp::signal::FuzzPairT< SampleType >::set_source_impedance_kohm ( double kohm)
inline

The Thévenin source impedance at the input jack: pickup, cable and — the part that matters — whatever the guitar's volume pot contributes as it is rolled back.

ONE ratio with TWO consequences: it attenuates the signal AND lightens Q1's input loading, which lowers the loop gain. Both push toward cleaner, which is why this circuit family cleans up so much more dramatically than a plain gain stage.

◆ stage_gain()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::stage_gain ( ) const
inline

One stage's small-signal gain R_c·gm_eff at the nominal operating point.

A design consequence of the collector load and the emitter degeneration together, not of the bias point alone.

◆ worst_residual()

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::worst_residual ( ) const
inline

The largest solver residual seen since reset() — so the fixed iteration count's adequacy is verified rather than assumed.

Member Data Documentation

◆ kCouplingGain

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kCouplingGain = 0.85
staticconstexpr

Interstage coupling loss, Q1's collector into Q2's base.

[design parameter] default 0.85, range 0.6 .. 1.0.

◆ kDcBlockerCornerHz

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kDcBlockerCornerHz = 12.0
staticconstexpr

DC-blocker corner, in Hz.

The starved operating point introduces a real DC step, so this has to actually remove it while staying below the lowest musical fundamental. [design parameter] default 12 Hz, range 2 .. 30 Hz.

◆ kEmitterDegeneration

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kEmitterDegeneration = 0.18
staticconstexpr

Emitter degeneration R_e, in the same normalised volts-per-calibration- unit as the collector load.

This is the element that makes the stage gain a DESIGN CHOICE instead of an accident of the bias point, and leaving it out is not a simplification — it is a different circuit. Without it, putting the quiescent collector mid-rail fixes R_c = 0.5/I_q, and the stage gain R_c·gm collapses to 0.5/(n·V_T) ≈ 19 no matter what else is chosen. A gain of 19 means the base-voltage window that swings the collector across its whole rail is ~50 mV, while the interstage coupling hands the next stage 85 % of a full rail — a 14x overdrive before any input arrives, so the second stage is a square wave at every setting and every source impedance. The circuit's signature cleanup then does not exist to be measured, and the solver never leaves the steepest part of the exponential.

With degeneration the emitter voltage rises with the current the base asks for, so gm_eff = gm/(1 + gm·R_e) and the stage gain approaches the resistor ratio R_c/R_e — independent of the operating current. That is how the real circuit does it too: the Fuzz-Face-class second stage sits on an emitter resistor precisely so its input window is volts-scale and matches the previous collector's swing.

The default gives a stage gain near 2.4, so a FULL-rail swing out of the first stage overdrives the second by about 2x — enough that a hot input slams the pair, little enough that a rolled-back guitar does not. [design parameter] default 0.18, range 0.05 .. 0.5.

◆ kFeedbackConductanceMax

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kFeedbackConductanceMax = 1.0
staticconstexpr

◆ kFeedbackConductanceMin

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kFeedbackConductanceMin = 0.05
staticconstexpr

Feedback conductance travel.

The top of the range is what kLoopGainCeiling constrains. [design parameter] min 0.05 (range 0 .. 0.2), max 1.0 (range 0.5 .. 1.5).

◆ kGermaniumDriftOctaves

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kGermaniumDriftOctaves = 0.6
staticconstexpr

Thermal-drift depth, in octaves of saturation current.

Germanium's reverse saturation current is far more temperature-sensitive than silicon's, which is why germanium units are famous for changing character with the weather. [design parameter] germanium 0.6 (range 0.1 .. 1.5), silicon 0.05 (range 0 .. 0.2).

◆ kGermaniumIdeality

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kGermaniumIdeality = 1.15
staticconstexpr

[design parameter] germanium n 1.15 (range 1.05 .. 1.30).

◆ kGermaniumSaturation

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kGermaniumSaturation = 5e-6
staticconstexpr

[design parameter] germanium Is 5e-6 calibration units (range 1e-7 .. 5e-5).

◆ kInputDriveRails

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kInputDriveRails = 0.4
staticconstexpr

How far a full-scale input drives the first stage, measured in RAIL-TO-RAIL SWINGS of that stage rather than in volts.

This has to be relative, not absolute, and getting it wrong is silent: an absolute scale of a few hundred millivolts slams a stage whose whole input window is tens of millivolts, so every input level clips identically and the source-impedance cleanup has nothing to show. Expressed as a multiple of the stage's own swing it tracks the device row and the collector load automatically — series law 7.

The swing it is measured against is the NOMINAL one, taken at the unstarved operating point, because the input network is a fixed property of the circuit. Referring it to the starved operating point instead would turn the drive up by exactly as much as starvation turned the stage's gain down, and bias starvation would be close to inaudible.

The linear window is half a swing either side of the quiescent point, so the number to compare against is 0.5. At the default, a full-scale input at a low source impedance lands past that before the feedback loop amplifies it further — it clips hard — while the same input through the ~27 % divider of a rolled-back guitar volume stays inside it. That difference IS the cleanup the circuit is famous for, and Acceptance Test 2 measures precisely the gap.

At the default, a full-scale input through a 10 kΩ source arrives as 0.35 swings, which the feedback loop's ~2.8x closed-loop gain turns into about two full windows — hard clipping — while the same input through 220 kΩ arrives as 0.09 and stays under a quarter of a window. [design parameter] default 0.4, range 0.2 .. 3.0 rail-to-rail swings.

◆ kInputImpedanceKohm

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kInputImpedanceKohm = 68.0
staticconstexpr

The fuzz stage's own input impedance at Q1's base, in kΩ.

Forms the divider with the source impedance. [design parameter] default 68 kΩ, range 20 .. 200 kΩ.

◆ kLoopGainCeiling

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kLoopGainCeiling = 0.94
staticconstexpr

The enforced loop-gain ceiling.

Deliberately short of the 1.0 instability boundary: the floor of its range keeps audible sag headroom, the ceiling keeps the measured maximum safely stable. It is also the solver's contraction condition — see the file doc block. [design parameter] default 0.94, range 0.85 .. 0.98.

◆ kMinAvailableCurrent

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kMinAvailableCurrent = 1e-3
staticconstexpr

Floor on available current, so a fully starved stage is near cutoff rather than exactly at it (which would make the bias voltage −inf).

[design parameter] default 1e-3, range 1e-5 .. 1e-2.

◆ kNewtonStepThermal

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kNewtonStepThermal = 2.0
staticconstexpr

Per-iteration Newton step limit, in thermal voltages.

Junction-limiting: the standard remedy for Newton on an exponential, where an unlimited step taken from the steep region lands somewhere it takes many iterations to climb back from. [design parameter] 2 thermal voltages.

◆ kNominalCollectorCurrent

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kNominalCollectorCurrent = 1.0
staticconstexpr

Quiescent collector current, the unit everything else is relative to.

[design parameter] default 1.0, range 0.5 .. 2.0 calibration units.

◆ kOversampleFactor

template<typename SampleType = float>
int pulp::signal::FuzzPairT< SampleType >::kOversampleFactor = 4
staticconstexpr

Oversampling factor.

The junction law is a hard exponential and the loop runs near its gain ceiling, so harmonic content extends well past Nyquist at the base rate. ADAA is not used: its published formulations target MEMORYLESS nonlinearities, and this is a stateful coupled solve with feedback whose loop has no closed-form antiderivative. [design parameter] default 4, range 2 .. 8.

◆ kQuiescentCollector

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kQuiescentCollector = 0.5
staticconstexpr

Where the quiescent collector sits on the normalised rail.

Mid-rail gives symmetric clipping headroom. [design parameter] default 0.5, range 0.3 .. 0.7.

◆ kResidualTolerance

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kResidualTolerance = 1e-6
staticconstexpr

Residual tolerance the fixed iteration count must clear across the whole parameter sweep.

Verified rather than assumed. [design parameter] default 1e-6, range 1e-8 .. 1e-4 calibration units.

◆ kSiliconDriftOctaves

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kSiliconDriftOctaves = 0.05
staticconstexpr

◆ kSiliconIdeality

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kSiliconIdeality = 1.03
staticconstexpr

[design parameter] silicon n 1.03 (range 1.00 .. 1.08).

◆ kSiliconSaturation

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kSiliconSaturation = 5e-12
staticconstexpr

[design parameter] silicon Is 5e-12 calibration units (range 1e-13 .. 5e-11).

◆ kSolverIterations

template<typename SampleType = float>
int pulp::signal::FuzzPairT< SampleType >::kSolverIterations = 24
staticconstexpr

Fixed solver iterations.

Not a loop bound to be tuned at runtime — the RT contract depends on it being compile-time constant. [design parameter] 24 iterations, sufficient for the fixed trust-region ladder to clear the residual tolerance at the nonlinear rail kinks.

◆ kStarveExponent

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kStarveExponent = 1.6
staticconstexpr

Starvation exponent: how sharply available current falls with the starve control.

[design parameter] default 1.6, range 1.0 .. 3.0.

◆ kThermalVoltage

template<typename SampleType = float>
double pulp::signal::FuzzPairT< SampleType >::kThermalVoltage = junction::kThermalVoltage
staticconstexpr

The shared thermal voltage.

Named here so this module's own formulas read in its terms, but there is exactly one definition, in junction.hpp.


The documentation for this class was generated from the following file:
  • /home/runner/work/pulp/pulp/core/signal/include/pulp/signal/fuzz_pair.hpp