Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::signal::SsbFrequencyShifterT< SampleType > Class Template Reference

The single-sideband frequency shifter. More...

#include <frequency_shifter_ssb.hpp>

Public Types

using Mode = FrequencyShiftMode
using Network = HilbertQuadratureNetworkT<double>

Public Member Functions

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 Public Member Functions

static constexpr double worst_case_gain ()
 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).
static double shift_hz_from_knob (double knob)
 The signed piecewise taper: a linear fine zone out to kLinZoneHz, geometric beyond it to kMaxShiftHz, mirrored about zero.
static double knob_from_shift_hz (double hz)
 Inverse of shift_hz_from_knob.
static double feedback_delay_ms_from_knob (double u)
 The loop-delay taper.

Static Public Attributes

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.

Detailed Description

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.

Member Typedef Documentation

◆ Mode

template<typename SampleType = float>
using pulp::signal::SsbFrequencyShifterT< SampleType >::Mode = FrequencyShiftMode

◆ Network

template<typename SampleType = float>
using pulp::signal::SsbFrequencyShifterT< SampleType >::Network = HilbertQuadratureNetworkT<double>

Member Function Documentation

◆ discard_history()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::discard_history ( )
inlinenoexcept

Audio-thread fault recovery: identical running state to reset() but logically invalidates the delay history instead of clearing its storage.

◆ feedback()

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::feedback ( ) const
inline

◆ feedback_delay_ms()

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::feedback_delay_ms ( ) const
inline

◆ feedback_delay_ms_from_knob()

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::feedback_delay_ms_from_knob ( double u)
inlinestatic

The loop-delay taper.

Geometric, because comb pitch is heard geometrically: halving the delay raises the comb's fundamental by an octave, so equal knob travel should buy equal intervals.

◆ knob_from_shift_hz()

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::knob_from_shift_hz ( double hz)
inlinestatic

Inverse of shift_hz_from_knob.

◆ latency_samples()

template<typename SampleType = float>
int pulp::signal::SsbFrequencyShifterT< SampleType >::latency_samples ( ) const
inlinenoexcept

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.

◆ mode()

template<typename SampleType = float>
Mode pulp::signal::SsbFrequencyShifterT< SampleType >::mode ( ) const
inline

◆ prepare()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::prepare ( double sample_rate)
inline

Sizes the two feedback delay lines for kMaxLoopMs at this rate and recomputes the DC-blocker pole and every smoothing increment.

May allocate; nothing else here does.

◆ process()

template<typename SampleType = float>
SampleType pulp::signal::SsbFrequencyShifterT< SampleType >::process ( SampleType x)
inline

One mono sample.

In stereo_split this returns the left (upper sideband) leg, spread and all — there is no second channel to put the other one on.

◆ process_stereo()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::process_stereo ( SampleType & left,
SampleType & right )
inline

One stereo frame.

Both channels share the carrier — one accumulator, one sin/cos pair — and each has its own Hilbert network, DC blocker and feedback line, so stereo content is preserved rather than summed.

◆ reset()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::reset ( )
inline

Clears every filter, line, accumulator and smoother, and snaps each smoother onto its target so a render from here is reproducible to the bit.

Allocates nothing.

◆ set_feedback()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::set_feedback ( double g)
inline

Barberpole feedback depth, in the range [0, kMaxFeedback].

◆ set_feedback_delay_ms()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::set_feedback_delay_ms ( double ms)
inline

Feedback-loop delay in ms, in the range [kMinDelayMs, kMaxLoopMs].

Sets the comb spacing that colours the spiral: short reads as a continuous glide, long as individually audible passes.

◆ set_mix()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::set_mix ( double wet01)
inline

Dry/wet, 0 .. 1.

◆ set_mode()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::set_mode ( Mode mode)
inline

◆ set_shift_hz()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::set_shift_hz ( double hz)
inline

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.

◆ set_stereo_spread()

template<typename SampleType = float>
void pulp::signal::SsbFrequencyShifterT< SampleType >::set_stereo_spread ( double spread01)
inline

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.

◆ shift_hz()

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::shift_hz ( ) const
inline

◆ shift_hz_from_knob()

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::shift_hz_from_knob ( double knob)
inlinestatic

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.

◆ worst_case_gain()

template<typename SampleType = float>
constexpr double pulp::signal::SsbFrequencyShifterT< SampleType >::worst_case_gain ( )
inlinestaticconstexpr

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.

Member Data Documentation

◆ kDcCornerHz

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kDcCornerHz = 5.0
staticconstexpr

DC blocker corner.

High enough to settle a DC step in a fraction of a second, low enough to leave the bottom of the audio band alone. [design parameter] default 5 Hz, range 1 .. 20 Hz.

◆ kDelaySmoothingMs

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kDelaySmoothingMs = 50.0
staticconstexpr

De-zippering time for the loop delay length.

Longer than the gains because moving a delay tap resamples what is already in the line, and a slower move turns that from a chirp into a glide. [design parameter] default 50 ms, range 10 .. 500 ms.

◆ kGainSmoothingMs

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kGainSmoothingMs = 20.0
staticconstexpr

De-zippering time for feedback, mix and stereo_spread — the gain controls.

Smoothing feedback also keeps a fast automation move from stepping the loop into a transient overshoot. [design parameter] default 20 ms, range 1 .. 100 ms.

◆ kGshiftBudget

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kGshiftBudget = 1.02
staticconstexpr

Allowed headroom above ideal unity for the retained sideband, which the registry's worst_case_gain is computed from.

The combine itself cannot exceed 1 (see the file doc block); what this actually pays for is the DC blocker's 2/(1 + p) supremum, plus margin. [design parameter] default 1.02, range 1.00 .. 1.05.

◆ kLinZoneHz

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kLinZoneHz = 20.0
staticconstexpr

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.

[design parameter] default 20 Hz, range 5 .. 50 Hz.

◆ kMaxFeedback

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kMaxFeedback = 0.90
staticconstexpr

Feedback ceiling.

[design parameter] default 0.90, range 0.5 .. 0.95 — the cap the loop-gain arithmetic in the file doc block is stated at.

◆ kMaxLoopMs

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kMaxLoopMs = 50.0
staticconstexpr

Feedback-loop delay ceiling, in ms.

Bounds the barberpole comb spacing without letting the loop stray into audible slapback. [design parameter] default 50 ms, range 10 .. 100 ms.

◆ kMaxShiftHz

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kMaxShiftHz = 5000.0
staticconstexpr

Shift magnitude ceiling, in Hz.

[design parameter] default 5000 Hz, range 1000 .. 20000 Hz. 5 kHz covers musical clangor without inviting whole-band fold-over.

◆ kMinDelayMs

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kMinDelayMs = 0.1
staticconstexpr

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.

◆ kShiftSmoothingMs

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kShiftSmoothingMs = 20.0
staticconstexpr

De-zippering time for shift_hz.

Long enough that a jumped automation value glides rather than clicks, short enough that a deliberate sweep still tracks. [design parameter] default 20 ms, range 1 .. 100 ms.

◆ kSplit

template<typename SampleType = float>
double pulp::signal::SsbFrequencyShifterT< SampleType >::kSplit = 0.5
staticconstexpr

Knob position where the taper crosses from linear to logarithmic.

[design parameter] default 0.5, range 0.3 .. 0.7.


The documentation for this class was generated from the following file: