|
| void | prepare (double sample_rate) |
| | Sizes the two feedback delay lines for kMaxLoopMs at this rate and recomputes the DC-blocker pole and every smoothing increment.
|
| void | reset () |
| | Clears every filter, line, accumulator and smoother, and snaps each smoother onto its target so a render from here is reproducible to the bit.
|
| void | discard_history () noexcept |
| | Audio-thread fault recovery: identical running state to reset() but logically invalidates the delay history instead of clearing its storage.
|
| void | set_shift_hz (double hz) |
| | Signed shift in Hz: positive shifts the retained sideband up, negative down.
|
| double | shift_hz () const |
| void | set_feedback (double g) |
| | Barberpole feedback depth, in the range [0, kMaxFeedback].
|
| double | feedback () const |
| void | set_feedback_delay_ms (double ms) |
| | Feedback-loop delay in ms, in the range [kMinDelayMs, kMaxLoopMs].
|
| double | feedback_delay_ms () const |
| void | set_mode (Mode mode) |
| Mode | mode () const |
| void | set_mix (double wet01) |
| | Dry/wet, 0 .. 1.
|
| void | set_stereo_spread (double spread01) |
| | stereo_split only: scales how far apart the two channels are driven.
|
| SampleType | process (SampleType x) |
| | One mono sample.
|
| void | process_stereo (SampleType &left, SampleType &right) |
| | One stereo frame.
|
| int | latency_samples () const noexcept |
| | Zero, and reported exactly rather than estimated (series law 5).
|
|
| static constexpr double | kMaxShiftHz = 5000.0 |
| | Shift magnitude ceiling, in Hz.
|
| static constexpr double | kLinZoneHz = 20.0 |
| | Top of the taper's linear fine zone, in Hz — the region where detune and barberpole live and where a 1 Hz taper error is audible as pitch instability.
|
| static constexpr double | kSplit = 0.5 |
| | Knob position where the taper crosses from linear to logarithmic.
|
| static constexpr double | kMaxFeedback = 0.90 |
| | Feedback ceiling.
|
| static constexpr double | kMinDelayMs = 0.1 |
| | Feedback-loop delay floor, in ms.
|
| static constexpr double | kMaxLoopMs = 50.0 |
| | Feedback-loop delay ceiling, in ms.
|
| static constexpr double | kGshiftBudget = 1.02 |
| | Allowed headroom above ideal unity for the retained sideband, which the registry's worst_case_gain is computed from.
|
| static constexpr double | kDcCornerHz = 5.0 |
| | DC blocker corner.
|
| static constexpr double | kShiftSmoothingMs = 20.0 |
| | De-zippering time for shift_hz.
|
| static constexpr double | kGainSmoothingMs = 20.0 |
| | De-zippering time for feedback, mix and stereo_spread — the gain controls.
|
| static constexpr double | kDelaySmoothingMs = 50.0 |
| | De-zippering time for the loop delay length.
|
template<typename SampleType = float>
class pulp::signal::SsbFrequencyShifterT< SampleType >
The single-sideband frequency shifter.
up and down are mirror images of one signed control: because Δf is signed, down at Δf is up at −Δf, and both are reachable from either. They are both present because a host-facing enum that names the sideband reads better than one that asks a user to negate a knob, and dual_mono is present for the same reason — it is up's behaviour under a name that says "deliberately no stereo differentiation", so a preset can distinguish "I
want both channels the same" from "I have not thought about stereo yet". Only stereo_split is a different topology.
template<typename SampleType = float>
Zero, and reported exactly rather than estimated (series law 5).
The network is IIR allpass: there is no bulk delay on the in-phase branch, and the impulse response is non-zero at sample 0. Its group delay IS frequency-dependent and a couple of samples in places — that is dispersion, not latency, and a host cannot compensate for it with a delay, so reporting it would misinform rather than inform. This is also why the network is IIR at all: a linear-phase FIR Hilbert would cost (taps−1)/2 samples and inflate the feedback loop by the same amount.
template<typename SampleType = float>
Signed shift in Hz: positive shifts the retained sideband up, negative down.
Clamped to ±kMaxShiftHz.
Smoothed LINEARLY, not geometrically, and that is the correct law here rather than a shortcut. LogRampedValueT is the house smoother for pitch and frequency because those are heard as ratios — but it requires both endpoints strictly positive, and Δf is signed and passes through zero every time a sweep reverses direction. It is also the wrong shape: a frequency SHIFT is additive by definition, so equal time should give equal hertz.
template<typename SampleType = float>
stereo_split only: scales how far apart the two channels are driven.
At 1 the channels move by +|Δf| and −|Δf|; at 0.5 by ±|Δf|/2; at 0 neither moves and both carry the (allpass, full-magnitude) input.
It scales the CARRIER FREQUENCY, not the quadrature term's depth, and the difference is not cosmetic. Scaling the quadrature term — L = I·c
− s·Q·s, which is how the spec's §7 formula reads — is not a partial shift at all: at s = 0.5 it produces 75 % of the upper sideband plus 25 % of the lower, two tones 10 Hz apart, and at s = 0 it produces an even split of both, which is ring modulation. Neither has any energy at the frequency the spec's own worked table says that setting should produce. Scaling the carrier gives exactly that table: one tone per channel at f ± spread·Δf, and at spread 0 the carrier stops and both channels pass the input at full magnitude.
template<typename SampleType = float>
The signed piecewise taper: a linear fine zone out to kLinZoneHz, geometric beyond it to kMaxShiftHz, mirrored about zero.
Both branches meet exactly at kSplit (the log branch's exponent is 0 there, and x⁰ = 1 needs no pow), and both are strictly increasing, so an automation sweep finds no plateau or reversal to snag on.
template<typename SampleType = float>
The bound Forge's registry cites (series law 8): the steady-state envelope of a comb-like loop at the feedback ceiling, 1/(1 − kMaxFeedback·kGshiftBudget).
The acceptance suite measures both factors and the resulting peak rather than trusting this.
It is deliberately the SAFE envelope, not the observed peak: a shift decorrelates recirculated energy (nothing lands twice at the same frequency, unlike a comb filter), so the measured peak sits well under it. Registering the observed number would be registering a bound that happens to hold for the programme material it was measured on.
template<typename SampleType = float>
Feedback-loop delay floor, in ms.
Numerical, not musical: the loop's log taper divides by it, and log(0) is undefined, so the knob's bottom position resolves here. Any value far below the shortest comb-relevant delay works. [design parameter] default 0.1 ms, range 0.01 .. 1 ms.