Time-domain ratio pitch shifter: dual-tap crossfaded modulated delay. More...
#include <pitch_shifter.hpp>
Public Member Functions | |
| double | dc_blocker_magnitude_peak () const |
| The DC blocker's MAGNITUDE-response peak, 2/(1+p) at Nyquist. | |
| PitchShifterT () | |
| void | prepare (double sample_rate) |
| Sizes the delay line for kWindowMsMax plus the stencil guard and prepares the composed primitives. | |
| void | reset () |
| Zeroes the line, the phase, and the filter state, and rewinds the drift stream (series law 2). | |
| void | discard_history () noexcept |
| Constant-time counterpart to reset() for hostile audio input. | |
| void | set_shift_source (ShiftSource source) |
| Selects the pedal law or the direct semitone target. Default pedal. | |
| ShiftSource | shift_source () const |
| void | set_shift_semitones (double semitones) |
| Direct shift target, used when shift_source() == ShiftSource::direct. | |
| double | shift_semitones () const |
| void | set_pedal (double e01) |
| Expression position, heel = 0 to toe = 1. | |
| double | pedal () const |
| void | set_pedal_mode (PedalMode mode) |
| Picks the pedal LAW only. | |
| PedalMode | pedal_mode () const |
| void | set_targets (double heel, double toe) |
| Whammy/dive endpoints in semitones. | |
| void | set_harmony (double a, double b) |
| Harmony A (heel) and B (toe) intervals in semitones. | |
| void | set_detune_cents (double cents) |
| void | set_dive_floor_semis (double semitones) |
| void | set_window_ms (double ms) |
| Crossfade window in ms. | |
| double | window_ms () const |
| void | set_glide_ms (double up_ms, double down_ms) |
| Independent rise/fall portamento on the SEMITONE signal, so a glide is perceptually even. | |
| double | glide_up_ms () const |
| double | glide_down_ms () const |
| void | set_mix (double wet01) |
| Equal-power dry↔wet. 0 is the untouched input; 1 is wet only. | |
| double | mix () const |
| void | set_detents (bool on) |
| Snap the pedal-law target table to musical intervals. | |
| bool | detents () const |
| void | set_interp (PitchInterp interp) |
| PitchInterp | interp () const |
| void | set_drift_depth (double d01) |
| Optional seeded pitch drift, 0 (default) . | |
| double | drift_depth () const |
| void | snap_to_target () |
| Jumps the glide to its current target with no portamento — what a harmony voice wants on a note-on, so it starts ON its interval instead of sliding up from the previous note's. | |
| double | target_semitones () const |
| The pre-slew target in semitones: the pedal law plus detents, or the direct value. | |
| double | pedal_law (double e01) const |
| The §5 pedal law alone, before detents and before the glide. | |
| double | current_semitones () const |
| The post-slew shift the block is applying right now, before drift. | |
| double | current_ratio () const |
| The ratio the taps ACTUALLY ramped at on the last processed sample, drift included. | |
| double | warble_hz () const |
| f_warble for the current ratio and window (Eq. | |
| double | tap_phase_pi (double hz) const |
| q = ψ/π for an input partial at hz: the half-window separation of the two taps in half-cycles of that partial. | |
| double | window_samples () const |
| Window length in samples at the prepared rate. | |
| int | latency_samples () const |
| Honest processing throughput, not phase-aligned PDC: the delay sweeps [0, L], so the crossfaded output's effective delay hovers at the window centre (Eq. | |
| SampleType | process (SampleType x) |
| One sample through the dry/wet mix. | |
| SampleType | process_wet (SampleType x) |
| The shifted leg alone, with no dry mixed in — for callers summing several shifted voices themselves and applying their own balance. | |
Static Public Member Functions | |
| static constexpr double | default_mix_for (PedalMode mode) |
| Per-mode conventional dry/wet default (§6). | |
| static constexpr bool | mode_uses_detents (PedalMode mode) |
| Whether a mode's targets are intervals the detent table can express. | |
Static Public Attributes | |
| static constexpr double | kWindowMsDefault = 40.0 |
| Crossfade window in ms — sets the warble rate (Eq. | |
| static constexpr double | kWindowMsMin = 10.0 |
| static constexpr double | kWindowMsMax = 100.0 |
| static constexpr double | kGlideMsDefault = 60.0 |
| Pedal portamento, rise and fall. | |
| static constexpr double | kGlideMsMin = 0.0 |
| static constexpr double | kGlideMsMax = 2000.0 |
| static constexpr double | kDetuneCentsDefault = 12.0 |
| PedalMode::detune offset at the toe, in cents. | |
| static constexpr double | kDetuneCentsMax = 50.0 |
| static constexpr double | kDiveFloorSemisDefault = -48.0 |
| PedalMode::dive floor. | |
| static constexpr double | kDiveFloorSemisMin = -72.0 |
| static constexpr double | kDiveFloorSemisMax = -24.0 |
| static constexpr double | kHeelSemisDefault = 0.0 |
| Whammy endpoints and harmony intervals. | |
| static constexpr double | kToeSemisDefault = 12.0 |
| static constexpr double | kIntervalASemisDefault = 7.0 |
| static constexpr double | kIntervalBSemisDefault = 12.0 |
| static constexpr double | kShiftSemisMin = -48.0 |
| The node's shift_semitones range. | |
| static constexpr double | kShiftSemisMax = 24.0 |
| static constexpr double | kSemitonesFloor = kDiveFloorSemisMin |
| Hard clamp on the slewed semitone signal, and therefore on r. | |
| static constexpr double | kSemitonesCeil = kShiftSemisMax |
| static constexpr double | kDetentTable [] |
| Detent table, in semitones: unison, 4ths, 5ths, octaves, double octaves. | |
| static constexpr int | kDetentCount = 9 |
| static constexpr double | kDetentSnapSemis = 0.35 |
| Detent capture half-width. | |
| static constexpr double | kDetentReleaseRatio = 1.5 |
| A Schmitt trigger needs TWO thresholds; the spec names the band but gives only one, which is not hysteresis — a target sitting exactly at the capture edge would still chatter across it. | |
| static constexpr double | kDetentReleaseLimitSemis = 1.0 |
| The release band is additionally clamped here, so no reachable (kDetentSnapSemis, kDetentReleaseRatio) pair can make two detents capture the same target. | |
| static constexpr int | kInterpMargin = 4 |
| Guard samples reserved for the read stencil. | |
| static constexpr double | kDriftMaxDepthPercent = 0.5 |
| Maximum 1σ pitch drift, as a percent of r, at drift_depth = 100 %. | |
| static constexpr std::uint32_t | kDriftSeed = 0x57484D59u |
| Fixed drift seed (series law 2: seeded, never automated, never exposed as a parameter). | |
| static constexpr double | kDcBlockerCornerHz = 5.0 |
| Corner of the wet leg's DC blocker, in Hz, converted to a pole at prepare as p = 1 − 2π·f_c/fs. | |
| static constexpr double | kClickBound = 0.25 |
| Sample-to-sample step ceiling the raised-cosine window must hold the wet output under. | |
| static constexpr double | kWorstCaseGain = 2.23606797749979 |
| Analytic peak-gain bound with an equal-power dry/wet mix and no feedback. | |
| static constexpr double | kDcBlockerPeakGain = 2.0 |
| The DC blocker's TIME-DOMAIN peak gain: the L1 norm of its impulse response, which is exactly 2 for every pole position. | |
| static constexpr double | kDefaultSampleRate = 48000.0 |
| Fallback sample rate for a block that is queried before prepare. | |
Time-domain ratio pitch shifter: dual-tap crossfaded modulated delay.
See the file doc block for the topology, the warble law, and the aliasing policy. The short version of the public surface:
|
inline |
|
inline |
The ratio the taps ACTUALLY ramped at on the last processed sample, drift included.
Identical to 2^(current_semitones/12) while drift_depth() == 0, which is the default — reporting the undrifted value would make the one control that is allowed to move the pitch invisible to every caller and every test.
|
inline |
The post-slew shift the block is applying right now, before drift.
|
inline |
The DC blocker's MAGNITUDE-response peak, 2/(1+p) at Nyquist.
Deliberately a separate accessor from kDcBlockerPeakGain above, and deliberately renamed: the two differ by a factor of ~2 and confusing them is exactly the defect this pair now documents. Use THIS one for steady-state questions — sideband amplitudes, frequency response — and the L1 constant for anything about a worst single sample.
|
inlinestaticconstexpr |
Per-mode conventional dry/wet default (§6).
Applied by the node or the preset layer, NOT by set_pedal_mode: a mode change mid-performance must not jump the level under the player's foot.
|
inline |
|
inlinenoexcept |
Constant-time counterpart to reset() for hostile audio input.
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
Honest processing throughput, not phase-aligned PDC: the delay sweeps [0, L], so the crossfaded output's effective delay hovers at the window centre (Eq.
3.6). Series law 5 — report the centre, never claim zero. Sample-rate invariant in wall-clock terms.
|
inline |
|
inlinestaticconstexpr |
Whether a mode's targets are intervals the detent table can express.
The spec states detents generally, which read literally makes two modes inert: detune's default target is 0.12 semitone, three times INSIDE the 0.35 capture band, so every detune setting would snap to unison and the mode would do nothing; and dive is a continuous collapse whose whole character is the un-stepped fall (its −48 floor is not even in the table). Detents quantise interval targets, so they apply where the targets are intervals.
|
inline |
|
inline |
The §5 pedal law alone, before detents and before the glide.
Exposed so the law can be asserted without rendering audio.
|
inline |
|
inline |
Sizes the delay line for kWindowMsMax plus the stencil guard and prepares the composed primitives.
The only allocating call (§3.6): N = ceil(window_max_ms · fs / 1000) + 2·kInterpMargin, which is the spec's + kInterpMargin plus a second margin so the low-end clamp and the i+2 stencil node both stay inside the buffer.
|
inline |
One sample through the dry/wet mix.
Call exactly one of process / process_wet per sample — both write the line and advance the phase.
|
inline |
The shifted leg alone, with no dry mixed in — for callers summing several shifted voices themselves and applying their own balance.
|
inline |
Zeroes the line, the phase, and the filter state, and rewinds the drift stream (series law 2).
The glide is snapped to the CURRENT target rather than to zero: a reset at +12 must not glide up from unison.
|
inline |
Snap the pedal-law target table to musical intervals.
Applies in whammy and harmony only (see mode_uses_detents) and never in ShiftSource::direct, where the caller's semitone value is the point.
|
inline |
|
inline |
|
inline |
Optional seeded pitch drift, 0 (default) .
. 1. At 0 the drift generator is not advanced at all, so the deterministic path is bypassed rather than merely zero-amplitude.
|
inline |
Independent rise/fall portamento on the SEMITONE signal, so a glide is perceptually even.
SlewMode::linear: a move takes this long whether it is one semitone or two octaves, and it arrives exactly.
|
inline |
Harmony A (heel) and B (toe) intervals in semitones.
|
inline |
|
inline |
Equal-power dry↔wet. 0 is the untouched input; 1 is wet only.
|
inline |
Expression position, heel = 0 to toe = 1.
|
inline |
Picks the pedal LAW only.
Deliberately does not touch the mix — see default_mix_for.
|
inline |
Direct shift target, used when shift_source() == ShiftSource::direct.
Clamped to the node's shift_semitones range.
|
inline |
Selects the pedal law or the direct semitone target. Default pedal.
|
inline |
Whammy/dive endpoints in semitones.
|
inline |
Crossfade window in ms.
This is a VOICING control, not a performance one: it sets the warble rate and the latency. The change is deferred — each tap latches the new length at the instant its own phase passes zero, where its delay AND its crossfade weight are both zero, so neither tap steps. The cost is that for at most half a warble cycle the two taps run different window lengths, during which the fading tap's ratio is 1 − (1−r)·L_b/L_a rather than r — a brief detune on a tap that is on its way out, in exchange for no click at all. latency_samples() reports the requested window immediately.
|
inline |
|
inline |
|
inline |
Jumps the glide to its current target with no portamento — what a harmony voice wants on a note-on, so it starts ON its interval instead of sliding up from the previous note's.
|
inline |
q = ψ/π for an input partial at hz: the half-window separation of the two taps in half-cycles of that partial.
Even → that partial shifts with no warble; odd → its carrier is suppressed and it splits into two lines at ±f_warble. See the file doc block.
|
inline |
The pre-slew target in semitones: the pedal law plus detents, or the direct value.
Pure — safe to call from a test or a UI thread.
|
inline |
f_warble for the current ratio and window (Eq.
3.5). Zero at unison, where the taps freeze into a static two-tap comb.
|
inline |
|
inline |
Window length in samples at the prepared rate.
|
staticconstexpr |
Sample-to-sample step ceiling the raised-cosine window must hold the wet output under.
Lives here rather than in the test so the acceptance criterion is computed from a shipped constant. A naive single-tap sawtooth spikes near 2.0 at a reset. [design parameter] default 0.25, range 0.15 .. 0.40 (with a 1.0-peak input; the floor allows legitimate fast-transient content, the ceiling still rejects an unsuppressed reset click).
|
staticconstexpr |
Corner of the wet leg's DC blocker, in Hz, converted to a pole at prepare as p = 1 − 2π·f_c/fs.
Specified as a FREQUENCY rather than as DcBlocker's pole because the pole is not sample-rate invariant and the default one is not as gentle as it looks: a first-order blocker's corner is (1−p)·fs/(2π), so the primitive's 0.995 sits at 38 Hz at 48 kHz — −2.0 dB at 50 Hz and −0.57 dB at 100 Hz. That lands squarely on the oct-down "bass drop" this module exists to do (a guitar low E shifted down an octave is 41 Hz), so the blocker would be eating the effect it is insuring. At 5 Hz the same 41 Hz partial loses 0.065 dB. Series law 7: the corner is in Hz so the behaviour is identical at every sample rate. [design parameter] default 5 Hz, range 1 .. 20 Hz.
|
staticconstexpr |
The DC blocker's TIME-DOMAIN peak gain: the L1 norm of its impulse response, which is exactly 2 for every pole position.
This used to return 2/(1+p) — the peak of the MAGNITUDE response at Nyquist, 1.000327 at the default corner — and call it "the exact bound". A magnitude-response peak bounds the response to a STEADY SINUSOID; it says nothing about the largest single sample. For y[n] = x[n] − x[n−1] + p·y[n−1] the impulse response is h[0] = 1, h[n] = p^(n−1)(p−1), so the worst-case sample gain is 1 + (1−p)/(1−p) = 2 — 5.9 dB above what was claimed, reachable on sustained near-DC content (bass, organ pedal).
The suite certified the old bound with a single 997 Hz sine, which is precisely the signal for which a magnitude-response bound IS valid. That is why it shipped.
|
staticconstexpr |
Fallback sample rate for a block that is queried before prepare.
|
staticconstexpr |
|
staticconstexpr |
The release band is additionally clamped here, so no reachable (kDetentSnapSemis, kDetentReleaseRatio) pair can make two detents capture the same target.
Half the closest detent spacing (−7/−5 and +5/+7, 2 semitones apart), so it is a property of the table.
|
staticconstexpr |
A Schmitt trigger needs TWO thresholds; the spec names the band but gives only one, which is not hysteresis — a target sitting exactly at the capture edge would still chatter across it.
The release half-width is this ratio times the capture half-width. [design parameter] default 1.5, range 1.05 .. 2.0.
|
staticconstexpr |
Detent capture half-width.
A Schmitt band so a resting pedal does not chatter between neighbours: below this the target snaps to the detent. [design parameter] default 0.35 semitone, range 0.1 .. 1.0 (the floor keeps the band audible above pedal-CV jitter, the ceiling keeps neighbouring detents — closest spacing 2 semitones — from overlapping).
|
staticconstexpr |
Detent table, in semitones: unison, 4ths, 5ths, octaves, double octaves.
12-TET, definitional — +5 → 1.334840, +7 → 1.498307.
|
staticconstexpr |
PedalMode::detune offset at the toe, in cents.
[design parameter] default +12, range ±50 (the mode is meaningless below about ±1).
|
staticconstexpr |
|
staticconstexpr |
PedalMode::dive floor.
Sets r_min = 2^(floor/12); r never reaches zero, which would be DC. [design parameter] default −48, range −72 .. −24.
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
Maximum 1σ pitch drift, as a percent of r, at drift_depth = 100 %.
0.5 % is about 8.6 cents — an analog wobble, not a vibrato. [design parameter] default 0.5, range 0.05 .. 5.
|
staticconstexpr |
Fixed drift seed (series law 2: seeded, never automated, never exposed as a parameter).
Any non-zero constant works.
|
staticconstexpr |
Pedal portamento, rise and fall.
[design parameter] default 60, range 0 .. 2000.
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
Whammy endpoints and harmony intervals.
[design parameter] defaults 0 / +12 and +7 / +12, ranges as the node's heel_semis / toe_semis (−48 .. +24) and interval_*_semis (±24).
|
staticconstexpr |
Guard samples reserved for the read stencil.
The floor of 4 is a hard requirement of the 4-point stencil, not tunable headroom; values above 4 only pad against future higher-order interpolants. [design parameter] default 4, range 4 .. 8.
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
Hard clamp on the slewed semitone signal, and therefore on r.
[kSemitonesFloor, kSemitonesCeil] = [2^-6, 2^2] = [0.015625, 4].
|
staticconstexpr |
|
staticconstexpr |
The node's shift_semitones range.
Note it does NOT reach the dive floor's −72: the internal clamp below is the union of the two, because a dive to −72 has to be expressible even though the direct-shift knob stops at −48.
|
staticconstexpr |
|
staticconstexpr |
Crossfade window in ms — sets the warble rate (Eq.
3.5) and the latency (Eq. 3.6). [design parameter] default 40, range 10 .. 100.
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
Analytic peak-gain bound with an equal-power dry/wet mix and no feedback.
This is Forge's worst_case_gain, so it is a headroom figure downstream plans against and understating it is the expensive direction.
√5, not √2. The dry leg is bounded by the input peak and the wet leg by kDcBlockerPeakGain = 2, and an equal-power mix maximises cos θ · 1 + sin θ · 2 at √(1² + 2²). It was √2 — derived from both legs being bounded by 1 — which understated the real peak by 3.9 dB.