The circuit-modelled clipper family: diode-to-ground and diode-in-the-loop, solved as ODEs with memory. More...
#include <pulp/signal/denormal.hpp>#include <pulp/signal/junction.hpp>#include <pulp/signal/tpt_filter.hpp>#include <pulp/signal/units.hpp>#include <algorithm>#include <cmath>#include <cstdint>#include <utility>Go to the source code of this file.
Classes | |
| struct | pulp::signal::ClipperSolverConfig |
| class | pulp::signal::DiodeClipperT< SampleType > |
| The diode-to-ground clipper: series resistor, shunt diode network, optional shunt capacitor. More... | |
| class | pulp::signal::FeedbackClipperT< SampleType > |
| The op-amp gain stage with the diode network either at its output or across its feedback resistor. More... | |
| class | pulp::signal::ToneStackT< SampleType > |
| Pre/post tone shaping around the clipper: a two-pole tilt pair. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
| namespace | pulp::signal::detail |
| Round-2 effect-wave vocabulary. | |
Typedefs | |
| using | pulp::signal::DiodeClipper = DiodeClipperT<float> |
| using | pulp::signal::DiodeClipper64 = DiodeClipperT<double> |
| using | pulp::signal::FeedbackClipper = FeedbackClipperT<float> |
| using | pulp::signal::FeedbackClipper64 = FeedbackClipperT<double> |
| using | pulp::signal::ToneStack = ToneStackT<float> |
| using | pulp::signal::ToneStack64 = ToneStackT<double> |
Enumerations | |
| enum class | pulp::signal::DiodeModel : std::uint8_t { pulp::signal::silicon , pulp::signal::germanium , pulp::signal::led } |
| Diode device families. The members differ only in (Is, n). More... | |
| enum class | pulp::signal::ClipperTopology : std::uint8_t { pulp::signal::to_ground , pulp::signal::in_loop } |
| Where the diode pair sits relative to the gain stage. More... | |
Functions | |
| void | pulp::signal::detail::apply_diode_model (junction::JunctionPair &network, DiodeModel model) |
| The (Is, n) pair for a device family. | |
| void | pulp::signal::detail::apply_symmetry (junction::JunctionPair &network, double symmetry) |
| symmetry ∈ [−1, +1] → the two legs' reciprocal series counts. | |
| template<typename Residual, typename Derivative> | |
| double | pulp::signal::detail::solve_safeguarded_newton (Residual &&f, Derivative &&df, double lo, double hi, double initial, double alternate, double tolerance, int max_iterations, int &iterations_out) |
| One monotone implicit stage, solved by a SAFEGUARDED Newton iteration. | |
| double | pulp::signal::detail::solve_implicit_flow_stage (const junction::JunctionPair &network, double history_voltage, double history_flow, double source_current, double flow_weight, double c_over_step, double resistance, double initial, int &iterations_out) |
| Solves one implicit stage of C·dv/dt = source_current − v/R − i_D(v). | |
| double | pulp::signal::detail::solve_tr_bdf2 (const junction::JunctionPair &network, double previous_voltage, double previous_source_current, double source_current, double capacitance, double timestep, double resistance, int &iterations_out) |
| One L-stable TR-BDF2 step of C·dv/dt = source_current(t) − v/R − i_D(v). | |
The circuit-modelled clipper family: diode-to-ground and diode-in-the-loop, solved as ODEs with memory.
This is the distortion family WITH MEMORY, and that boundary is the reason it is a separate header from saturator.hpp rather than a mode on it. A capacitor sits inside the clipping network, so the effective clip point at this instant depends on the filtered recent history of the signal, not just on the current sample. That is what makes Newton-Raphson on an ODE the right tool instead of a static y = f(x) table — and it is why a "clean tanh" character belongs in SaturatorT, not as a memoryless mode bolted on here. The C = 0 degenerate configuration exists to test the solver's limit case, not as a shipped preset.
Three classes, one header, because they share the ODE solver. Splitting them per class would mean either duplicating that solver or inventing a fourth header to hold it; the solver is the coupling, so the coupling stays in one file.
Everything here is the Shockley diode law, i_D(v) = Is·(exp(v/(n·Vt)) − 1) (Shockley 1949, as used throughout the circuit-modelling literature — Yeh, Abel & Smith 2010, §II). Silicon, germanium and LED differ ONLY in (Is, n). The audible consequences — germanium turns on ~150–200 mV earlier and softer, LEDs latest and hardest — fall straight out of plugging each row into the one equation. That is series law 7 applied to diode families: find the dimensionless shape rather than interpolating fitted curves.
None of the (Is, n) pairs are measured parts. Each is an honest [design parameter] representing the typical published order of magnitude for a device CLASS.
The antiparallel network is parameterised by how many diodes are in series on each leg, because that is the physical thing that changes:
where a and b are the RECIPROCAL series counts of the two legs. a = b = 1 is the matched pair (odd, symmetric, odd harmonics only). a = 1/2 is two diodes in series on one leg, turning on at twice the threshold — mild asymmetry. b = 0 removes the second leg entirely: a single diode, half-wave clipping, strong even harmonics.
Reciprocal counts rather than counts is what makes the parameter continuous: "leg absent" is b = 0, a finite value, where a count would have to be infinite. symmetry ∈ [−1, +1] maps onto them directly, and the crossfade interpolates the dimensionless shape rather than blending two independently fitted waveforms.
The clipper ODE is stiff near the diode knee — Vt is 26 mV against a signal swinging volts — so explicit methods need prohibitive sample rates to stay stable. That is precisely Yeh, Abel & Smith's finding for this class of ODE. TR-BDF2 is implicit, second-order, A-stable and L-stable: its trapezoid substep preserves accuracy in the audio band, while its BDF2 completion damps the stiff alternating mode that plain trapezoidal rule preserves. Each substep has a HARD Newton iteration cap: an unbounded loop in process() is not RT-safe, and hitting either cap clamps to the last iterate.
RT contract: prepare() may allocate (it configures the oversampler when one is in use). set_*, process() and reset() never allocate, never lock, never perform I/O. A stateful process() costs two implicit substeps, each capped at kMaxNewtonIterations evaluations of one exp pair — bounded, never data-dependent past those caps. There is no randomness anywhere in this file, so determinism (series law 2) is satisfied by construction; that is worth stating because it is easy to assume a "vintage" effect needs drift, and this one does not.
References: Shockley (1949) for the diode law. Yeh, Abel & Smith, "Simplified, Physically-Informed Models of Distortion and Overdrive Guitar Effects Pedals" (DAFx-07 / IEEE TASLP 2010) for the ODE formulation and the numerical-methods survey that selects TR. ElectroSmash's public Tube Screamer circuit analysis for the in-loop topology and its documented behaviour ONLY — no component values are reproduced, and every default here is independently chosen and independently ranged.