Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_pitch_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Pitch — bake-layer catalog nodes.
4//
5// The home for the pitch family: nodes that move a signal's pitch rather than
6// its level, its spectrum's position, or its time base. It opens with the
7// time-domain WHAMMY shifter and is named for the family rather than for that
8// one member, because the DIATONIC HARMONY ENGINE is being built alongside it
9// and will land here as a second member. Its node belongs in this file, in its
10// own `harmony` namespace beside `whammy` below — it shares this file's
11// conventions (a shift expressed in real musical units, a pedal or interval
12// control, a wet leg a mix blends against dry) and differs in how the target
13// interval is CHOSEN: fixed by a pedal law here, derived from a detected pitch
14// and a key/scale there. Splitting those across two headers named after their
15// algorithms would scatter one set of decisions across two places.
16//
17// MONO, one port in and one out. `PitchShifterT` is a per-rail block: it has no
18// cross-channel coupling of any kind — no shared detector, no shared carrier,
19// no stereo mode — so a stereo instrument is exactly two independent copies,
20// which is what instancing two nodes already gives. Giving it two ports would
21// claim a coupling that does not exist in the code. Contrast the modulation
22// family's shifter, which is genuinely true-stereo because `stereo_split`
23// drives both rails from ONE carrier.
24//
25// ── REALIZATION vs INJECTABLE PARAM ───────────────────────────────────────
26//
27// The standing rule: anything that moves `latency_samples()` is frozen at
28// registration, because a node whose reported latency changes under the audio
29// thread breaks the host's delay compensation. A genuine topology change is a
30// realization too. Coefficients are params.
31//
32// For this node exactly ONE control is caught by that rule, and it is the whole
33// reason this member takes a construction argument at all:
34//
35// `window_ms` IS the latency. `latency_samples() = round(window_ms·fs/2000)` —
36// the crossfade window's centre, which the DSP header reports honestly rather
37// than claiming zero. At the default 40 ms window that is 960 samples at
38// 48 kHz. Exposing it as an injectable param would let automation move the
39// node's reported latency mid-render, which is the exact failure the rule
40// exists to prevent. So it is a construction argument, like the VCA
41// compressor's lookahead, and for the same reason: the DSP sizes its delay
42// line for the whole declared range at `prepare()`, so any value in range is
43// allocation-free — it is frozen here for the HOST's sake, not the
44// allocator's.
45//
46// The DSP header agrees on its own terms, calling `window_ms` "a VOICING
47// control, not a performance one". It is also the warble rate (Eq. 3.5), so
48// freezing it freezes the effect's character, which is a preset-level
49// decision rather than something a pedal sweeps.
50//
51// Everything else injects, and the two that look like they might not are worth
52// saying out loud:
53//
54// - `pedal_mode` (whammy / harmony / detune / dive) changes the LAW mapping
55// pedal position to semitones — it does not change the signal path, the
56// port count, or the latency. Same reasoning as the FET compressor's five
57// ratio buttons: a front-panel switch over one unchanged topology.
58// - `interp` (linear / 4-point Lagrange) changes the read STENCIL, not the
59// structure. `latency_samples()` does not depend on it, and cannot: it
60// reports the window centre. The two kernels do differ in group delay by
61// about one sample, which is a sub-0.1 % change on a 960-sample window and
62// is below what this block's honest-throughput latency figure claims to
63// resolve in the first place. If a future interpolant ever carried a
64// latency worth reporting, it would have to move to registration.
65//
66// ── A GAP WORTH KNOWING ABOUT ─────────────────────────────────────────────
67//
68// `CustomNodeType` has no way to declare latency. `SignalGraph::node_latency_
69// samples()` reads the PDC chain, which accumulates from plugin nodes; a custom
70// node contributes zero. So this node's ~960 samples are invisible to the
71// graph's delay compensation, and a host that mixes a whammy-bearing branch
72// against a dry branch will hear them time-misaligned.
73//
74// That is a framework gap, not something this file can fix, and it is the
75// second reason `window_ms` is frozen at registration: a host CAN be told the
76// figure out of band — `whammy_latency_samples()` below computes it from the
77// same expression the DSP uses — but only if the value is stable, which it is
78// only if it cannot be automated.
79//
80// ── WET AND DRY BOTH ──────────────────────────────────────────────────────
81//
82// Unlike a compressor, whose output IS the processed signal, a pitch shifter is
83// routinely blended: a harmony voice sits under the dry source, a detune
84// thickens it, and only a whammy dive is conventionally heard alone. The DSP
85// block owns the equal-power mix so the node exposes it as one control.
86//
87// The per-mode conventional defaults (`default_mix_for`: 1.0 whammy and dive,
88// 0.5 harmony, 0.4 detune) are DELIBERATELY NOT APPLIED when `pedal_mode`
89// changes. The DSP header is explicit that a mode change mid-performance must
90// not jump the level under the player's foot, and since `pedal_mode` injects,
91// applying them live would do exactly that. They belong to the preset layer:
92// `whammy_default_mix_for_mode()` exposes them so a preset can apply one at
93// load time, and the registry rows below quote them per mode.
94
99
101
102// ── The whammy / expression-pedal shifter ─────────────────────────────────
103//
104// One node type. Nothing here changes topology and nothing changes latency
105// except the crossfade window, which is therefore taken at construction.
106namespace whammy {
107
108inline constexpr const char* kTypeId = "pitch.whammy";
109
110// ── Injectable param ids ──────────────────────────────────────────────────
111// Node-local; the framework namespaces per node so two nodes never collide.
112inline constexpr state::ParamID kPedal = 1; // 0 = heel .. 1 = toe
113inline constexpr state::ParamID kPedalMode = 2; // stepped 0..3, see below
114inline constexpr state::ParamID kShiftSource = 3; // stepped 0 = pedal, 1 = direct
115inline constexpr state::ParamID kShiftSemitones = 4; // semitones, direct target
116inline constexpr state::ParamID kHeelSemis = 5; // semitones
117inline constexpr state::ParamID kToeSemis = 6; // semitones
118inline constexpr state::ParamID kIntervalASemis = 7; // semitones, harmony heel
119inline constexpr state::ParamID kIntervalBSemis = 8; // semitones, harmony toe
120inline constexpr state::ParamID kDetuneCents = 9; // cents
121inline constexpr state::ParamID kDiveFloorSemis = 10; // semitones, negative
122inline constexpr state::ParamID kGlideUpMs = 11; // ms
123inline constexpr state::ParamID kGlideDownMs = 12; // ms
124inline constexpr state::ParamID kMix = 13; // 0..1, equal-power
125inline constexpr state::ParamID kDetents = 14; // stepped 0/1
126inline constexpr state::ParamID kInterp = 15; // stepped 0 = linear, 1 = cubic
127inline constexpr state::ParamID kDriftDepth = 16; // 0..1
128
131inline constexpr float kModeWhammy = 0.0f;
132inline constexpr float kModeHarmony = 1.0f;
133inline constexpr float kModeDetune = 2.0f;
134inline constexpr float kModeDive = 3.0f;
135
137inline constexpr float kSourcePedal = 0.0f;
138inline constexpr float kSourceDirect = 1.0f;
139
141inline constexpr float kInterpLinear = 0.0f;
142inline constexpr float kInterpCubic = 1.0f;
143
145
149
153
174float whammy_worst_case_gain(double sample_rate);
175
183int whammy_latency_samples(double sample_rate, double window_ms = Shifter::kWindowMsDefault);
184
189
197
199
200} // namespace whammy
201
202namespace harmony {
203inline constexpr const char* kTypeId = "pitch.harmony_engine";
204
205enum : state::ParamID {
206 kKey = 1,
218};
219
221
222struct Instance {
224};
225
227
229
230int latency_samples(double sample_rate);
231
233
234} // namespace harmony
235
236} // namespace pulp::host::pitch
static constexpr double kWindowMsDefault
Definition pitch_shifter.hpp:222
The intelligent harmonizer — track a monophonic line, snap it to a key and scale, and add one or two ...
Definition forge_pitch_catalog.hpp:202
signal::HarmonyEngine Engine
Definition forge_pitch_catalog.hpp:220
ForgeNodeDescriptor descriptor()
CustomNodeType make_harmony_engine_node()
int latency_samples(double sample_rate)
@ kDryLevel
Definition forge_pitch_catalog.hpp:216
@ kV1Level
Definition forge_pitch_catalog.hpp:210
@ kV2Interval
Definition forge_pitch_catalog.hpp:212
@ kGlideMs
Definition forge_pitch_catalog.hpp:215
@ kHumanize
Definition forge_pitch_catalog.hpp:217
@ kV2Level
Definition forge_pitch_catalog.hpp:214
@ kV2Detune
Definition forge_pitch_catalog.hpp:213
@ kV2Enable
Definition forge_pitch_catalog.hpp:211
@ kV1Interval
Definition forge_pitch_catalog.hpp:208
@ kScale
Definition forge_pitch_catalog.hpp:207
@ kV1Detune
Definition forge_pitch_catalog.hpp:209
@ kKey
Definition forge_pitch_catalog.hpp:206
constexpr const char * kTypeId
Definition forge_pitch_catalog.hpp:203
Definition forge_pitch_catalog.hpp:106
constexpr state::ParamID kDetuneCents
Definition forge_pitch_catalog.hpp:120
ForgeNodeDescriptor descriptor()
constexpr state::ParamID kDiveFloorSemis
Definition forge_pitch_catalog.hpp:121
constexpr float kModeWhammy
The pedal_mode param's stepped encoding.
Definition forge_pitch_catalog.hpp:131
CustomNodeType make_whammy_node(double window_ms=Shifter::kWindowMsDefault)
The expression-pedal pitch shifter as a lowerable custom node.
constexpr float kModeDive
Definition forge_pitch_catalog.hpp:134
constexpr float kSourceDirect
Definition forge_pitch_catalog.hpp:138
float whammy_worst_case_gain(double sample_rate)
Worst-case linear gain for the Forge registry (series law 8).
signal::PitchShifter Shifter
Definition forge_pitch_catalog.hpp:144
constexpr state::ParamID kMix
Definition forge_pitch_catalog.hpp:124
constexpr state::ParamID kDetents
Definition forge_pitch_catalog.hpp:125
constexpr state::ParamID kHeelSemis
Definition forge_pitch_catalog.hpp:116
constexpr float kModeHarmony
Definition forge_pitch_catalog.hpp:132
constexpr float kInterpLinear
The interp param's stepped encoding.
Definition forge_pitch_catalog.hpp:141
float whammy_default_mix_for_mode(signal::PedalMode mode)
The conventional dry/wet default for a mode, for the PRESET layer.
constexpr state::ParamID kGlideUpMs
Definition forge_pitch_catalog.hpp:122
constexpr state::ParamID kPedalMode
Definition forge_pitch_catalog.hpp:113
constexpr state::ParamID kToeSemis
Definition forge_pitch_catalog.hpp:117
constexpr state::ParamID kShiftSource
Definition forge_pitch_catalog.hpp:114
constexpr state::ParamID kGlideDownMs
Definition forge_pitch_catalog.hpp:123
constexpr float kSourcePedal
The shift_source param's stepped encoding.
Definition forge_pitch_catalog.hpp:137
constexpr float kModeDetune
Definition forge_pitch_catalog.hpp:133
constexpr state::ParamID kIntervalASemis
Definition forge_pitch_catalog.hpp:118
constexpr state::ParamID kInterp
Definition forge_pitch_catalog.hpp:126
constexpr const char * kTypeId
Definition forge_pitch_catalog.hpp:108
constexpr state::ParamID kShiftSemitones
Definition forge_pitch_catalog.hpp:115
constexpr state::ParamID kPedal
Definition forge_pitch_catalog.hpp:112
constexpr state::ParamID kDriftDepth
Definition forge_pitch_catalog.hpp:127
signal::PedalMode decode_pedal_mode(float v)
Decodes the stepped pedal_mode value.
constexpr float kInterpCubic
Definition forge_pitch_catalog.hpp:142
int whammy_latency_samples(double sample_rate, double window_ms=Shifter::kWindowMsDefault)
The node's reported latency at a given window and sample rate, in samples.
constexpr state::ParamID kIntervalBSemis
Definition forge_pitch_catalog.hpp:119
Definition forge_pitch_catalog.hpp:100
PitchShifterT< float > PitchShifter
Definition pitch_shifter.hpp:862
HarmonyEngineT< float > HarmonyEngine
Definition harmony_engine.hpp:790
PedalMode
How the expression pedal maps to a shift target.
Definition pitch_shifter.hpp:168
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
The time-domain ratio pitch shifter — the one that warbles.
Definition custom_node_type.hpp:90
Definition forge_param_descriptor.hpp:127
Definition forge_pitch_catalog.hpp:222
Engine engine
Definition forge_pitch_catalog.hpp:223
Definition forge_pitch_catalog.hpp:146
Shifter shifter
Definition forge_pitch_catalog.hpp:147