Bandlimited-step (BLEP) and bandlimited-ramp (BLAMP) correction kernels. More...
#include <cmath>Go to the source code of this file.
Classes | |
| struct | pulp::signal::osc::Correction |
| A discontinuity's correction, split across the two samples it touches. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
| namespace | pulp::signal::osc |
Functions | |
| double | pulp::signal::osc::blep_residual (double x) noexcept |
| Unit BLEP residual at x, the signed distance from the discontinuity to the sample, in samples: negative before it, positive after it. | |
| double | pulp::signal::osc::blamp_residual (double x) noexcept |
| Unit BLAMP residual at x — the integral of blep_residual, and the correction that turns a trivial ramp max(x, 0) into its bandlimited counterpart. | |
| Correction | pulp::signal::osc::poly_blep (double d, double height) noexcept |
| polyBLEP correction for a step in the VALUE. | |
| Correction | pulp::signal::osc::poly_blamp (double d, double slope_change) noexcept |
| polyBLAMP correction for a break in the SLOPE. | |
| double | pulp::signal::osc::wrap_position (double phase_after, double increment) noexcept |
| Sub-sample position of a period wrap, for a phase accumulator that has just wrapped through its boundary. | |
Variables | |
| constexpr double | pulp::signal::osc::kNoDiscontinuity = -1.0 |
| A sub-sample position that means "no discontinuity here". | |
Bandlimited-step (BLEP) and bandlimited-ramp (BLAMP) correction kernels.
A trivially-generated waveform — a saw that jumps, a square that switches, a triangle that turns a corner — is not bandlimited. Sampling it folds every component above Nyquist back into the audible band. These kernels supply the correction that a generator adds to the trivial signal to suppress that fold.
Two kinds of discontinuity need two kernels:
── What these kernels are NOT ────────────────────────────────────────────
polyBLEP is a two-point polynomial approximation to the ideal BLEP, and the approximation is the dominant error, not a rounding detail. The ideal bandlimited step's residual has infinite support and must be windowed and tabulated (minBLEP/BLIT territory); polyBLEP truncates it to the two samples straddling the discontinuity and fits a polynomial through them. The residual this leaves is real and measurable: it buys tens of dB over the trivial waveform, NOT the ~100 dB a tabulated kernel reaches. test_osc_blep.cpp measures the actual figure rather than asserting an aspiration.
Use these where a cheap, allocation-free, table-free correction that improves on the trivial waveform is the right trade. Do not present the result as alias-free, and do not gate a deep-floor claim on them.
── Sign convention (read before calling) ─────────────────────────────────
Every kernel here returns a correction to ADD to the trivial signal. There is no subtract path and no hidden negation: the sign lives entirely in the caller's height / slope_change argument, which is always measured as after minus before. A saw that wraps from +1 down to −1 has height = -1 - (+1) = -2, and the kernel's output is added like any other.
This is the one place a mistake is silent. Applying a correction with the wrong sign does not fail loudly — it doubles the discontinuity instead of removing it, making aliasing worse while a naive "did the test run?" check still passes. test_osc_blep.cpp pins the convention by asserting that the inverted correction measurably degrades the residual.
── Why the phase increment is not a kernel argument ──────────────────────
These kernels take the discontinuity's position in samples, not in phase units, so the phase increment does not appear in the kernel math — it is purely the phase-to-samples conversion, and it belongs to whatever advances the phase. wrap_position performs that conversion for the common case of a phase accumulator wrapping through a period boundary; a hard-sync or through-zero-FM generator, whose discontinuities are not predictable from the increment at all, computes the position its own way and calls the same kernels. Keeping the increment out of the kernel is what lets one kernel serve all three.
RT contract: every function here is a branch-light polynomial evaluation. No allocation, no I/O, no locks, no transcendentals, no state.
Precision: the kernels compute in double throughout and take/return double. A float generator narrows once, at the point it adds the correction to its own sample — the kernel never sees float. The polynomial coefficients are exactly representable, but the position d typically comes from a division, and evaluating a cubic at a float-rounded position costs more than the narrowing does.