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

The diatonic harmonizer: YinTrackerT + DiatonicMapT + one PitchShifterT per voice. More...

#include <harmony_engine.hpp>

Public Member Functions

 HarmonyEngineT ()
void prepare (double sample_rate)
 Sizes the tracker, the alignment delay, and both shifters.
void reset ()
void discard_history () noexcept
 Constant-time audio fault recovery.
void set_key (int root_pitch_class)
void set_scale (ScaleType scale)
void set_off_scale_policy (OffScalePolicy policy)
void set_voice_interval (int voice, int steps)
 Voice interval in SCALE STEPS, not semitones.
int voice_interval (int voice) const
void set_voice_detune_cents (int voice, double cents)
double voice_detune_cents (int voice) const
void set_voice_level_db (int voice, double db)
double voice_level_db (int voice) const
void set_voice_enabled (int voice, bool on)
bool voice_enabled (int voice) const
void set_dry_level_db (double db)
double dry_level_db () const
void set_glide_ms (double ms)
 Cents-domain portamento time.
double glide_ms () const
void set_humanize_cents (double cents)
 Seeded humanise depth in cents, 1σ.
double humanize_cents () const
void set_crossfade_ms (double ms)
 The shifter's crossfade window, shared by both voices.
double crossfade_ms () const
void set_interp (PitchInterp interp)
int latency_samples () const
 The delay the whole block imposes, and the delay the dry path is given.
int tracker_latency_samples () const
 The tracker window W, on its own.
int shifter_latency_samples () const
 The shifter's own throughput delay, on its own.
double tracked_f0_hz () const
bool voiced () const
const DiatonicMapping & voice_mapping (int voice) const
 The mapping the engine last computed for a voice, including whether the input was off-scale or the shift hit the octave clamp.
double voice_cents (int voice) const
 The voice's post-glide offset in cents, and the ratio derived from it.
double voice_ratio (int voice) const
int voice_shift_semitones (int voice) const
double mute_gain () const
 The wet-mute gate, 0 while unvoiced and 1 while voiced.
const YinTrackerT< SampleType > & tracker () const
const DiatonicMapT< SampleType > & diatonic_map () const
SampleType process (SampleType x)

Static Public Attributes

static constexpr int kMaxVoices = 2
 Voice slots.
static constexpr double kCrossfadeMsDefault = 20.0
 The shifter's crossfade window.
static constexpr double kCrossfadeMsMin = 10.0
static constexpr double kCrossfadeMsMax = 50.0
static constexpr double kGlideMsDefault = 60.0
 Cents-domain portamento, applied whenever a target moves — a key change, a scale change, an interval change, or the input crossing a scale-degree boundary.
static constexpr double kGlideMsMax = 500.0
static constexpr double kVoiceMuteMs = 10.0
 Wet-mute ramp applied when the tracker reports an unvoiced frame, so noise and silence do not get harmonized.
static constexpr double kDetuneMaxCents = 50.0
 Per-voice detune. [design parameter] default 0, range ±50 cents.
static constexpr double kHumanizeDefault = 0.0
 Optional seeded humanise drift, as a 1σ depth in cents.
static constexpr double kHumanizeMaxCents = 15.0
static constexpr int kV1IntervalDefault = 2
 Voice interval defaults, in scale steps: a 3rd above and a 5th above.
static constexpr int kV2IntervalDefault = 4
static constexpr double kLevelMinDb = -60.0
 Level range for the dry and voice mixers, in dB.
static constexpr double kLevelMaxDb = 6.0
static constexpr double kLevelMaxLinear = 1.9952623149688795
static constexpr double kWorstCaseGain
 The closed-form feed-forward bound at the declared parameter ceiling.
static constexpr double kMuteFloor = 1e-5
 Below this the mute gate is treated as fully closed, so an unvoiced passage costs nothing and cannot leak a denormal tail.

Detailed Description

template<typename SampleType = float>
class pulp::signal::HarmonyEngineT< SampleType >

The diatonic harmonizer: YinTrackerT + DiatonicMapT + one PitchShifterT per voice.

RT contract: as the file banner. prepare allocates; nothing else does.

Constructor & Destructor Documentation

◆ HarmonyEngineT()

template<typename SampleType = float>
pulp::signal::HarmonyEngineT< SampleType >::HarmonyEngineT ( )
inline

Member Function Documentation

◆ crossfade_ms()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::crossfade_ms ( ) const
inline

◆ diatonic_map()

template<typename SampleType = float>
const DiatonicMapT< SampleType > & pulp::signal::HarmonyEngineT< SampleType >::diatonic_map ( ) const
inline

◆ discard_history()

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

Constant-time audio fault recovery.

Controls are retained while every history-bearing component is logically returned to silence.

◆ dry_level_db()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::dry_level_db ( ) const
inline

◆ glide_ms()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::glide_ms ( ) const
inline

◆ humanize_cents()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::humanize_cents ( ) const
inline

◆ latency_samples()

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::latency_samples ( ) const
inline

The delay the whole block imposes, and the delay the dry path is given.

This is max(W, shifter_latency) rather than the tracker's W alone, and the difference matters: the wet legs have their OWN throughput delay through the shifter, so delaying the dry by W and leaving the wet alone would leave the two misaligned by |W − shifter_latency| — 720 samples, 15 ms, at the shipped defaults. Instead the shifters are fed from a pre-delay of latency_samples() − shifter_latency so BOTH legs come out at latency_samples(). At the defaults the max is W, so the reported number is the tracker window exactly, as specified.

A ratio shifter's output is not a delayed copy of its input, so wet alignment is exact only in the sense that the two legs' window centres coincide — which is the strongest statement resampling permits.

◆ mute_gain()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::mute_gain ( ) const
inline

The wet-mute gate, 0 while unvoiced and 1 while voiced.

◆ prepare()

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

Sizes the tracker, the alignment delay, and both shifters.

The only allocating call.

◆ process()

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

◆ reset()

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

◆ set_crossfade_ms()

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

The shifter's crossfade window, shared by both voices.

◆ set_dry_level_db()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_dry_level_db ( double db)
inline

◆ set_glide_ms()

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

Cents-domain portamento time.

0 steps instantly, which is still click-safe because the shifter's crossfade masks a ratio jump within one window.

◆ set_humanize_cents()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_humanize_cents ( double cents)
inline

Seeded humanise depth in cents, 1σ.

Off by default; deterministic when on (series law 2).

◆ set_interp()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_interp ( PitchInterp interp)
inline

◆ set_key()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_key ( int root_pitch_class)
inline

◆ set_off_scale_policy()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_off_scale_policy ( OffScalePolicy policy)
inline

◆ set_scale()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_scale ( ScaleType scale)
inline

◆ set_voice_detune_cents()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_voice_detune_cents ( int voice,
double cents )
inline

◆ set_voice_enabled()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_voice_enabled ( int voice,
bool on )
inline

◆ set_voice_interval()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_voice_interval ( int voice,
int steps )
inline

Voice interval in SCALE STEPS, not semitones.

+2 is a third, and what that means in semitones depends on the degree — which is the point.

◆ set_voice_level_db()

template<typename SampleType = float>
void pulp::signal::HarmonyEngineT< SampleType >::set_voice_level_db ( int voice,
double db )
inline

◆ shifter_latency_samples()

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::shifter_latency_samples ( ) const
inline

The shifter's own throughput delay, on its own.

◆ tracked_f0_hz()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::tracked_f0_hz ( ) const
inline

◆ tracker()

template<typename SampleType = float>
const YinTrackerT< SampleType > & pulp::signal::HarmonyEngineT< SampleType >::tracker ( ) const
inline

◆ tracker_latency_samples()

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::tracker_latency_samples ( ) const
inline

The tracker window W, on its own.

◆ voice_cents()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::voice_cents ( int voice) const
inline

The voice's post-glide offset in cents, and the ratio derived from it.

◆ voice_detune_cents()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::voice_detune_cents ( int voice) const
inline

◆ voice_enabled()

template<typename SampleType = float>
bool pulp::signal::HarmonyEngineT< SampleType >::voice_enabled ( int voice) const
inline

◆ voice_interval()

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::voice_interval ( int voice) const
inline

◆ voice_level_db()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::voice_level_db ( int voice) const
inline

◆ voice_mapping()

template<typename SampleType = float>
const DiatonicMapping & pulp::signal::HarmonyEngineT< SampleType >::voice_mapping ( int voice) const
inline

The mapping the engine last computed for a voice, including whether the input was off-scale or the shift hit the octave clamp.

◆ voice_ratio()

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::voice_ratio ( int voice) const
inline

◆ voice_shift_semitones()

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::voice_shift_semitones ( int voice) const
inline

◆ voiced()

template<typename SampleType = float>
bool pulp::signal::HarmonyEngineT< SampleType >::voiced ( ) const
inline

Member Data Documentation

◆ kCrossfadeMsDefault

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kCrossfadeMsDefault = 20.0
staticconstexpr

The shifter's crossfade window.

Sets the splice ("warble") rate: f_splice = |1−r|·1000/kCrossfadeMs, so 20 ms and a +4 semitone third splice at 13.0 Hz. Larger windows lower the warble but lengthen the transition smear. [design parameter] default 20 ms, range 10 .. 50.

◆ kCrossfadeMsMax

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kCrossfadeMsMax = 50.0
staticconstexpr

◆ kCrossfadeMsMin

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kCrossfadeMsMin = 10.0
staticconstexpr

◆ kDetuneMaxCents

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kDetuneMaxCents = 50.0
staticconstexpr

Per-voice detune. [design parameter] default 0, range ±50 cents.

◆ kGlideMsDefault

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kGlideMsDefault = 60.0
staticconstexpr

Cents-domain portamento, applied whenever a target moves — a key change, a scale change, an interval change, or the input crossing a scale-degree boundary.

[design parameter] default 60 ms, range 0 .. 500.

◆ kGlideMsMax

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kGlideMsMax = 500.0
staticconstexpr

◆ kHumanizeDefault

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kHumanizeDefault = 0.0
staticconstexpr

Optional seeded humanise drift, as a 1σ depth in cents.

[design parameter] default 0 (off), range 0 .. 15 cents.

◆ kHumanizeMaxCents

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kHumanizeMaxCents = 15.0
staticconstexpr

◆ kLevelMaxDb

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kLevelMaxDb = 6.0
staticconstexpr

◆ kLevelMaxLinear

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kLevelMaxLinear = 1.9952623149688795
staticconstexpr

◆ kLevelMinDb

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kLevelMinDb = -60.0
staticconstexpr

Level range for the dry and voice mixers, in dB.

[design parameter] defaults 0 dB, range −60 .. +6.

◆ kMaxVoices

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::kMaxVoices = 2
staticconstexpr

Voice slots.

Capped at 2 because that is the catalog node's slot allocation (v1_*/v2_*) and what kWorstCaseGain sums over; more voices is a capacity change, not a tuning change. [design parameter] default 2, range {1, 2}.

◆ kMuteFloor

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kMuteFloor = 1e-5
staticconstexpr

Below this the mute gate is treated as fully closed, so an unvoiced passage costs nothing and cannot leak a denormal tail.

[design parameter] default 1e-5 (−100 dBFS), range 1e-7 .. 1e-3.

◆ kV1IntervalDefault

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::kV1IntervalDefault = 2
staticconstexpr

Voice interval defaults, in scale steps: a 3rd above and a 5th above.

[design parameter] defaults +2 / +4, range ±14.

◆ kV2IntervalDefault

template<typename SampleType = float>
int pulp::signal::HarmonyEngineT< SampleType >::kV2IntervalDefault = 4
staticconstexpr

◆ kVoiceMuteMs

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kVoiceMuteMs = 10.0
staticconstexpr

Wet-mute ramp applied when the tracker reports an unvoiced frame, so noise and silence do not get harmonized.

[design parameter] default 10 ms, range 2 .. 50.

◆ kWorstCaseGain

template<typename SampleType = float>
double pulp::signal::HarmonyEngineT< SampleType >::kWorstCaseGain
staticconstexpr
Initial value:
=
static constexpr double kLevelMaxLinear
Definition harmony_engine.hpp:418
static constexpr int kMaxVoices
Voice slots.
Definition harmony_engine.hpp:380
static constexpr double kDcBlockerPeakGain
The DC blocker's TIME-DOMAIN peak gain: the L1 norm of its impulse response, which is exactly 2 for e...
Definition pitch_shifter.hpp:353

The closed-form feed-forward bound at the declared parameter ceiling.

There is no feedback path, so this is arithmetic rather than an invariant to discover: 10^(6/20) · (dry + 2·voice·dc_l1).

This was 3.0, from "unity dry plus two unity voices" — each wet voice bounded by 1 because the tap crossfade is convex. But each voice's wet leg passes a DC blocker, whose worst-case SAMPLE gain is the L1 norm of its impulse response, exactly 2 — not the 2/(1+p) = 1.000327 magnitude peak the derivation used. Inherited from PitchShifterT, along with the error: a magnitude-response peak bounds a steady sinusoid, not a single sample. Understating a registry headroom figure by 4.4 dB is the expensive direction.


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