Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
fuzz_pair.hpp File Reference

The two-transistor direct-coupled feedback pair — the fuzz circuit whose character comes from its FEEDBACK, not from its clipping. More...

#include <pulp/signal/dc_blocker.hpp>
#include <pulp/signal/junction.hpp>
#include <pulp/signal/denormal.hpp>
#include <pulp/signal/oversampling.hpp>
#include <pulp/signal/rng.hpp>
#include <pulp/signal/units.hpp>
#include <algorithm>
#include <cmath>
#include <cstdint>

Go to the source code of this file.

Classes

class  pulp::signal::FuzzPairT< SampleType >
 The two-transistor direct-coupled feedback pair. More...

Namespaces

namespace  pulp
namespace  pulp::signal

Typedefs

using pulp::signal::FuzzPair = FuzzPairT<float>
using pulp::signal::FuzzPair64 = FuzzPairT<double>

Enumerations

enum class  pulp::signal::FuzzDevice : std::uint8_t { pulp::signal::germanium , pulp::signal::silicon }
 The two device families. They differ only in (n, Is). More...

Detailed Description

The two-transistor direct-coupled feedback pair — the fuzz circuit whose character comes from its FEEDBACK, not from its clipping.

A diode clipper distorts because a diode conducts. This circuit distorts because two exponential junctions are wrapped in a feedback loop that runs deliberately close to instability, and the loop's proximity to that edge is the sag and bloom the family is known for. Everything characteristic about it follows from that one structural fact:

  • It cleans up from the guitar's volume knob far more than a gain-staged distortion does. Raising the source impedance attenuates the signal AND reduces how much of the feedback the input node returns to Q1's base, which lowers the loop gain. Two effects, one ratio, both pushing the same direction — at the tested settings the divider accounts for 11 dB of the drop and the loop gain for another 7. A model that implements only the attenuation reproduces half the behaviour — see set_source_impedance_kohm and the adjudication in loop_gain().
  • Starving the bias makes it gate and sputter per note, not per level. A loud attack passes, the decaying tail of that same note falls below the available current and cuts out, and the next attack re-opens it. That is the starved stage's own behaviour, not a noise gate bolted on the end — a gate would trigger on absolute level and get the dynamics wrong.
  • Misbiasing it produces octave-up content with no octave parameter anywhere: an asymmetric operating point generates even harmonics.
  • Germanium and silicon differ only in (n, Is). The ~0.36 V and ~0.69 V conduction knees are EMERGENT from a six-decade difference in saturation current, not hard-coded thresholds. Hard-coding them would lose the continuous, current- and temperature-dependent softness that is the actual difference between the two devices.

The circuit model

Each stage is a common-emitter amplifier with emitter degeneration, in normalised units, biased at 1.0 calibration unit of collector current:

i(v_be) = Is·(exp(v_be/(n·V_T)) − 1) // Ebers-Moll, forward-active term
v_b = v_be + R_e·i // base node — the emitter rises with i
v_c = clamp(1 − R_c·i, 0, 1) // collector node, normalised rail

The collector load is what bounds the circuit. Without it the junction current grows without limit — at the germanium defaults a 0.9 V drive gives i ≈ 7.2e7 — and a feedback path fed by an unbounded current has no solution at all. R_c is set so the quiescent collector sits mid-rail, which is also where the clipping headroom is symmetric.

The emitter degeneration R_e is what makes the two stages composable, and it is the element a reduced model is most tempted to drop. Drop it and R_c — pinned by the mid-rail bias — forces a stage gain of ~19, so a stage's whole usable input window is 50 mV while the stage before it hands over 85 % of a full rail. Every setting then clips identically, the cleanup has nothing to show, and the solver never leaves the steep part of the exponential. With it, gm_eff = gm/(1 + gm·R_e), the gain approaches the resistor ratio R_c/R_e, and the two stages' scales match. See kEmitterDegeneration.

The stages couple through collector-voltage DEVIATION from each stage's own quiescent point, which is what the feedback resistor's DC servo achieves in the real circuit while keeping the interstage coupling direct.

Why the solver has a fixed cost

The pair is solved by a fixed-count safeguarded 2×2 Newton descent. The small-signal loop-gain bound keeps the operating point stable, but does not by itself bound nonlinear conditioning at a collector-rail kink or after a full-scale discontinuity. The solve therefore uses a capped ladder of trust-region Newton and residual-gradient candidates and accepts the one with the smallest coupled residual. The full device/control/source-impedance grid, including full-scale square transitions, independently asserts the residual tolerance.

The loop is explicitly bounded rather than unity-compensated (series law 1). Compensating it to unity would remove the module's reason for existing: this circuit is supposed to run near the edge.

Bias starvation

Starvation lowers the current available to the SECOND stage — the supply feeding its collector node is what runs down — and therefore lowers that stage's quiescent bias, rather than clamping the current mid-solve. Two distinctions, both of which decide whether the module gates or merely goes quiet:

  • Lowering the bias, not clamping the current. With the bias lowered the stage sits near cutoff, so small signals do not turn it on and loud transients do — gating that tracks per-note dynamics. Clamping the current instead pins the collector at a constant, which compresses loud passages and leaves quiet ones alone: the opposite of a gate.
  • Starving one stage, not both. A healthy first stage still swinging its full rail into a second stage that can no longer follow it is what gates. Starving both leaves the loop gain falling as the SQUARE of the current, so the feedback path dies long before the operating point is asymmetric enough to fold — and the feedback amplifying that asymmetry is the whole mechanism behind the octave-up.

RT contract

prepare() may allocate — it builds the oversampler's filter state. set_*, process(), process_block() and reset() never allocate, never lock, and never perform I/O. The implicit solve runs a COMPILE-TIME-FIXED iteration count, so the worst-case time per sample is bounded and not data-dependent. All randomness is the seeded house OuWalkT, rewound by reset(). The seed is fixed and never automated (series law 2); hosts may toggle the separate drift-enable setup control without changing the deterministic sequence.

References: Ebers & Moll, "Large-Signal Behavior of Junction Transistors", Proc. IRE 42(12):1761–1772, 1954, for the forward-active junction law. ElectroSmash's public Fuzz Face analysis for the topology and its documented behaviour only — no component values are reproduced and every default here is independently chosen and independently ranged.