The memoryless saturation stage every drive, fuzz, tube and tape-warmth block in the catalog composes instead of hand-rolling a tanh(x) call. More...
#include <pulp/signal/biquad.hpp>#include <pulp/signal/denormal.hpp>#include <pulp/signal/oversampling.hpp>#include <pulp/signal/units.hpp>#include <algorithm>#include <cmath>#include <cstddef>#include <cstdint>#include <array>Go to the source code of this file.
Classes | |
| struct | pulp::signal::detail::SaturatorShapeMath |
| The waveshaper family's three closed forms per member: the curve f, its derivative f' (needed for the bias normaliser), and its antiderivative F1 (needed for ADAA). More... | |
| class | pulp::signal::SaturatorAdaa2 |
| Second-order antiderivative antialiasing over one shape, in the curve's own argument domain (Bilbao, Esqueda, Parker & Välimäki, IEEE SPL 24(7), 2017). More... | |
| class | pulp::signal::SaturatorT< SampleType > |
| The memoryless saturation stage. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
| namespace | pulp::signal::detail |
| Round-2 effect-wave vocabulary. | |
Typedefs | |
| using | pulp::signal::Saturator = SaturatorT<float> |
| using | pulp::signal::Saturator64 = SaturatorT<double> |
Enumerations | |
| enum class | pulp::signal::SaturatorShape : std::uint8_t { pulp::signal::tanh_soft , pulp::signal::atan_soft , pulp::signal::cubic_soft , pulp::signal::sinh_arc } |
| Which member of the waveshaper family a SaturatorT evaluates. More... | |
| enum class | pulp::signal::SaturatorAliasPolicy : std::uint8_t { pulp::signal::adaa , pulp::signal::oversample_2x , pulp::signal::off , pulp::signal::adaa2 } |
| How a SaturatorT keeps the harmonics it generates from folding back. More... | |
Functions | |
| double | pulp::signal::adaa_linear_magnitude (int order, double cycles_per_sample) noexcept |
| The linear-path magnitude response of order-th-order antiderivative antialiasing (1 or 2) at cycles_per_sample (0 … 0.5). | |
The memoryless saturation stage every drive, fuzz, tube and tape-warmth block in the catalog composes instead of hand-rolling a tanh(x) call.
Saturation is the most-reused nonlinearity there is: a drive pedal, a tape stage, a tube preamp, transformer colour and mix-bus glue are one waveshaper wearing different clothes. What differentiates them is bias, tone bracketing, and an antialiasing policy — not the core curve. Getting that core right once means every later module specifies only its own character mapping.
Four shapes, chosen to span the useful design space with textbook functions only (Arfib 1979 / Le Brun 1979 waveshaping lineage): tanh (bounded, symmetric), atan (bounded, softer knee), a cubic soft clip (exact polynomial, cheapest, hardest character) and asinh (unbounded, gentlest — saturation character that never fully clips).
Unity small-signal gain, at every drive setting (series law 1). All four shapes have f'(0) = 1, and the drive law is shaped(x) = f(d·x)/d, so shaped'(0) = f'(0) = 1 identically. The drive knob changes how much of the curve's bend a signal reaches, never the pass-through gain of a quiet one.
Unbiased, that also makes 1.0 the SUPREMUM, since every shape is globally compressive past the origin: gain only ever falls below 1. With bias it does not, and this is worth stating plainly because it is easy to assume otherwise. Normalising by the local slope f'(b) means the curve is expansive on the side of the operating point that runs back toward the origin, where the slope is steeper. The true supremum is
— 1.0 at zero bias, 2.38 for tanh at full bias, and 25× for cubic_soft at its clamp, where f'(b) = 1 − b² is deliberately near zero. That is not a defect to fix; it is what "normalise the gain at the operating point" means, and a caller running a heavy bias is asking for a hot stage. It IS a number the caller must be told rather than left to discover, so worst_case_gain() computes it and the test suite asserts it holds across the whole grid. That asserted bound — not a flat 1.0 — is what Forge's registry cites (series law 8).
Bias without a DC offset. Raw f(u + b) would hand the rest of the chain a DC step to eat, and subtracting f(b) alone would still break the drive law, because the local slope at the operating point is f'(b), not 1. The construction normalises by the local slope:
which is exactly zero at x = 0 for every bias and every drive, collapses to the unbiased form at b = 0 (since f'(0) = 1), and keeps unity gain at the operating point — so bias can be swept live without a click or a level jump. Asymmetry is what produces even harmonics alongside the odd ones, the documented single-ended-triode character.
A stated antialiasing policy (series law 4). The default is first-order antiderivative antialiasing: on the linear part of the curve it is the two-tap average (x[n] + x[n−1]) / 2, so it delays by half a sample and rolls off the top octave (adaa_linear_magnitude()); the integer latency_samples() rounds that half sample down to 0 and group_delay_samples() reports it exactly. adaa2 (second order) suppresses aliases further for one more F2 evaluation, at one whole sample of delay — reported, and matched on the dry path — and a steeper top-octave roll-off. oversample_2x keeps the house half-band path available for chains that must stay phase-coherent with another oversampled stage; it reports its latency exactly rather than estimating. off exists only for tests and the CPU floor.
Tone bracketing that actually cancels. Pre-emphasis boosts above a corner so high content hits the knee hotter and generates proportionally more upper harmonics; de-emphasis applies the inverse shelf. With matching corners the pair is an exact inverse (an RBJ shelf at −G is the algebraic reciprocal of the same shelf at +G), so a linear pass-through measures flat end to end.
Memory/state-dependent nonlinearities (hysteresis, thermal lag) are the tape physical tier's job. Multiband drive is N instances plus the existing crossovers. Hard clipping is excluded: its discontinuous derivative generates a far wider, more alias-prone spectrum for the same antialiasing budget, and it deserves its own justification rather than riding in here.
RT contract: prepare() may allocate (it sizes the oversampler's filter state and the dry-path alignment delay). set_*, process() and reset() never allocate, never lock, never perform I/O, and are safe per sample on the audio thread. All state is POD; zero-init is a valid fresh instance at the neutral setting (tanh_soft, 0 dB drive, no bias, tone off, ADAA, fully wet). process() costs one or two transcendental evaluations depending on the alias policy.
References: Pakarinen & Yeh, "A Review of Digital Techniques for Modeling Vacuum-Tube Guitar Amplifiers", Computer Music Journal 33(2), 2009 (cited for the published concepts — asymmetry produces even harmonics; linear filtering brackets a nonlinear stage — never for any amplifier's fitted curve). Arfib, JAES 27(10), 1979, and Le Brun, JAES 27(4), 1979, for the waveshaping method. Parker, Zavalishin & Le Bivic, DAFx-16, and Bilbao, Esqueda, Parker & Välimäki, IEEE SPL 24(7), 2017, for antiderivative antialiasing. RBJ "Audio EQ Cookbook" for the shelf topology family.