Classes | |
| struct | Correction |
| A discontinuity's correction, split across the two samples it touches. More... | |
| struct | DcoProfile |
| OSC-DCO profile — data-driven timing parameters, no waveform or hardware capture. More... | |
| class | DcoOscillator |
| A divider-clocked oscillator front-end composing the shared VaOscillator. More... | |
| class | MinBlepAccumulator |
| Accumulates causal minBLEP corrections for discontinuities found between adjacent output samples. More... | |
| struct | PhaseEvent |
| A single phase discontinuity within one sample period. More... | |
| class | PhaseAccumulator |
| Phase accumulator over the unit circle [0, 1), suitable as the clock for through-zero FM, hard sync, and per-sample modulation. More... | |
| class | VaOscillator |
| Virtual-analog oscillator over the static shapes. More... | |
| struct | VcoTuning |
| The analog exponential pitch converter, as a pure control-voltage → frequency map. More... | |
| struct | WaveshaperParams |
| A per-shape lumped memoryless nonlinearity — the "analog character" the output stage imprints. More... | |
| class | VcoOscillator |
| Virtual-analog VCO over the static shapes, with a circuit-flavored deterministic path. More... | |
| class | WtOscillator |
| Modern wavetable oscillator over a set of band-limited WavetableTs. More... | |
| class | LofiWtOscillator |
| Dedicated variable-clock ZOH wavetable oscillator (the lo-fi tier). More... | |
Enumerations | |
| enum class | DcoDivider { integer_n , fractional_n } |
| The divider scheme a profile selects. More... | |
| enum class | MinBlepInsertResult { inserted , zero_height , invalid_position , non_finite_height , capacity_exceeded } |
| enum class | PhaseEventKind { wrap_forward , wrap_backward , sync } |
| What produced a phase discontinuity. More... | |
| enum class | VaShape { sine , saw , square , triangle } |
| The static shapes. More... | |
Functions | |
| double | 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 | 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 | poly_blep (double d, double height) noexcept |
| polyBLEP correction for a step in the VALUE. | |
| Correction | poly_blamp (double d, double slope_change) noexcept |
| polyBLAMP correction for a break in the SLOPE. | |
| double | 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. | |
| int | threshold_crossings (double phase_before_advance, double increment, double threshold, std::span< double > out) noexcept |
| Sub-sample positions at which the phase crosses threshold (mod 1) during one sample, written to out in chronological order. | |
Variables | |
| constexpr double | kNoDiscontinuity = -1.0 |
| A sub-sample position that means "no discontinuity here". | |
|
strong |
|
strong |
|
strong |
|
strong |
|
inlinenoexcept |
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.
Where BLEP is discontinuous at the origin (it cancels a jump in the value), BLAMP is continuous there and only its slope breaks — it cancels a jump in the derivative, leaving the value alone. Support is [−1, 1), and the residual vanishes at both ends, so it perturbs nothing outside the two samples it owns.
|
inlinenoexcept |
Unit BLEP residual at x, the signed distance from the discontinuity to the sample, in samples: negative before it, positive after it.
This is the correction that turns a unit step u(x) into its bandlimited counterpart: u(x) + blep_residual(x) rises smoothly from 0 at x = -1 through 0.5 at the discontinuity to 1 at x = 1, where a trivial step instead jumps. Support is exactly [−1, 1); outside it the residual is zero, which is precisely the truncation this kernel trades accuracy for.
x = 0 belongs to the after branch, matching u(0) = 1 — the sample that lands exactly on a discontinuity is the first one that has taken the step.
That single choice is why poly_blep does NOT evaluate both of its taps through this function: a before tap landing exactly on the discontinuity has not taken the step and needs the other limit. This is the mathematical residual, and at its one discontinuity it can only answer for one side; a caller that knows which side its sample is on should say so rather than ask here and hope.
|
inlinenoexcept |
polyBLAMP correction for a break in the SLOPE.
d follows poly_blep's convention exactly. slope_change is the change in slope measured after minus before, in units of value per sample — not per phase unit, and not per second. A triangle traverses its full range four times per cycle, so at its apex the slope goes from +4·increment to -4·increment and slope_change is -8·increment; converting to per-sample units is where the caller's increment enters, and it is the caller's job.
Both returned terms are ADDED to the trivial signal.
Out-of-window positions return nothing, exactly as in poly_blep. What this does NOT need is that function's per-tap side selection: BLAMP is continuous at the origin — it corrects a break in the slope and leaves the value alone — so both sides agree there (at 1/6) and reading either is the same answer. That continuity is also why the boundary bug poly_blep guards against never showed up on a triangle: only a kernel that steps can be read off the wrong side of its own step.
|
inlinenoexcept |
polyBLEP correction for a step in the VALUE.
d is the discontinuity's sub-sample position: it occurs d samples after the before sample, equivalently 1 - d samples before the after sample. Outside [0, 1] there is no correction and both terms are zero — a discontinuity always lands inside the sample it was found in, so a position outside that window is a caller error, and none is the only answer that cannot make the output worse than leaving it alone. kNoDiscontinuity is outside the window by construction and so is guaranteed, not merely likely, to produce nothing. A non-finite d lands here too.
height is the size of the jump measured after minus before — negative for a saw wrapping down, positive for a square's rising edge. Both returned terms are ADDED to the trivial signal (see the file-level sign convention).
── Why this does not route both taps through blep_residual ─────────────
The taps sit on OPPOSITE sides of the discontinuity — that is what before and after mean — and the residual STEPS across it, so each tap has to read its own side. The distinction is invisible in the interior and decisive at the two boundaries, which are ordinary operating points rather than corner cases:
blep_residual resolves x = 0 to the right limit — correct for the after tap, and exactly backwards for the before one. Reading both through it gives the d = 0 tap a correction of the wrong SIGN, which does not merely under-correct: it moves the sample a full height the wrong way, landing it outside the waveform's own range and leaving the boundary wrap worse corrected than if it had been ignored. Selecting the side per tap is what makes both boundaries land on the step's midpoint, which is what a bandlimited step does there.
With d confined to [0, 1] each tap's side is known outright, so both are branch-free evaluations of their own branch rather than a lookup that has to re-decide which side it is on.
|
inlinenoexcept |
Sub-sample positions at which the phase crosses threshold (mod 1) during one sample, written to out in chronological order.
Returns the count.
This is PhaseAccumulator's wrap-finding inversion generalized from integer levels to a level offset by threshold: the phase is linear in sub-sample time, so the unwrapped phase reaches level m + threshold at exactly t = (m + threshold - phase_before) / increment. Shifting the phase by -threshold turns "cross the threshold" back into "cross an integer", which is a problem the accumulator has already solved exactly.
It is that shared derivation, not a re-implementation, that makes an apex or a pulse edge land with the same accuracy as a wrap — and at threshold = 0 this reproduces the accumulator's own wrap positions, which is asserted rather than assumed (test_osc_va.cpp).
Ranges match the accumulator's for the same reason they do there: (0, 1] going forward, [0, 1) going backward. Closing the other side would drop a crossing landing exactly on a sample boundary, and a dropped crossing is an uncorrected discontinuity.
Returns 0 for a zero or non-finite increment: a phase that is not advancing crosses nothing, and absorbing the non-finite case here keeps it from reaching the kernels. At most out.size() crossings are reported; the surplus is dropped, which needs an instantaneous frequency above out.size() times the sample rate.
|
inlinenoexcept |
Sub-sample position of a period wrap, for a phase accumulator that has just wrapped through its boundary.
phase_after is the phase remaining past the boundary — the accumulator's value once the period has been subtracted — and increment is the per-sample phase step, both in the same units (a [0, 1) phase and a normalized increment, typically). The wrap happened phase_after / increment samples before the current sample, so the position relative to the preceding sample is one minus that.
The result feeds poly_blep/poly_blamp as d, with the before term landing on the sample that preceded the wrap and after on the current one. This is a convenience for the predictable case only: a hard-sync reset or a through-zero-FM direction change is not derivable from the increment, and those callers compute d from the event that caused the discontinuity.
Returns kNoDiscontinuity when increment is not positive or not finite: a phase that is not advancing has no wrap to place, so the correction must be exactly zero.
The guard is not decoration. A frozen phase makes this 0.0 / 0.0, and NaN survives both kernels' range checks to land in the output buffer. Nor can the guard simply return 0: d = 0 is a legitimate operating point meaning "the discontinuity sits on the `before` sample", and it yields a half-step correction — so returning it would inject a step on every sample of a stopped oscillator, which is the opposite of the intent.
|
inlineconstexpr |
A sub-sample position that means "no discontinuity here".
It sits outside [0, 1], which poly_blep and poly_blamp reject outright, so it produces exactly zero from both. No in-range value could serve: 0 and 1 are the two ENDS of the window, not spare sentinels, and each is a real operating point carrying a half-height correction.