auracle_grammar/compile.rs
1//! Compile a [`PatchTree`] term into a playable quiver [`Patch`].
2//!
3//! Every compiled voice gets the mandatory output chain
4//! `<audio> → DC blocker → VCA (amp ADSR) → Limiter → StereoOutput` and two
5//! external controls (`pitch` in V/Oct, `gate` in volts) fanned out to every
6//! pitched source and every envelope — no evolved patch can bypass the
7//! limiter or end up unplayable.
8//!
9//! The tail is built once per channel: a subtree that produces true stereo
10//! (reverb, chorus) keeps its two tanks all the way to the output rather than
11//! having the right one discarded.
12//!
13//! ## Validation mode
14//!
15//! Patches are wired under [`ValidationMode::Warn`], not `Strict`: quiver's
16//! `Strict` rejects *warning-class* pairs, which includes blessed idioms this
17//! compiler leans on (a unipolar mod envelope driving a bipolar FM input, the
18//! bipolar pitch [`Offset`] driving V/Oct inputs). The type discipline
19//! that `Strict` would enforce is already guaranteed by construction: the
20//! term's Audio/Mod sorts are Rust types, and this compiler only emits
21//! known-good connection shapes. Compile errors (invalid ports, cycles) are
22//! still hard failures; accumulated warnings are returned for inspection and
23//! property tests assert they stay within the expected classes.
24//!
25//! ## Parameter mapping
26//!
27//! Genome parameters are normalized `[0, 1]`; this module owns their musical
28//! mapping. Ranges are deliberately **bounded away from pathology** (max
29//! resonance 0.85, max delay feedback 0.7) — the grammar cannot express the
30//! most degenerate settings, which is safety layer 3 of the vetting design.
31
32use std::collections::HashMap;
33use std::sync::Arc;
34
35use quiver::modules::{
36 Attenuverter, Bitcrusher, Chorus, Clock, Compressor, DelayLine, Distortion, Ducker,
37 EnvelopeFollower, Euclidean, Flanger, FormantOsc, Granular, KarplusStrong, Limiter, LogicAnd,
38 LogicOr, LogicXor, Max, Min, NoiseGate, ParametricEq, Phaser, Rectifier, Reverb, SampleAndHold,
39 ScaleQuantizer, Supersaw, Tremolo, VcSwitch, Vibrato,
40};
41use quiver::prelude::*;
42use quiver::{AtomicF64, ExternalInput};
43
44use crate::term::{
45 rect_mode_index, AudioNode, DriveMode, FilterKind, ModNode, ModOp, PairOp, PatchTree,
46};
47
48/// quiver reads `Adsr.shape`, `Vca.response` and `Limiter.soft` as *gates* at
49/// the 2.5 V threshold, not as continuous curve amounts — 5 V and 10 V do the
50/// same thing. These two names say which side of the threshold we mean.
51const GATE_TRUE: f64 = 5.0;
52const GATE_FALSE: f64 = 0.0;
53/// Filter keytracking amount (`Svf`/`DiodeLadderFilter` port 5). quiver applies
54/// `2^(voct · amt)`, so 0.5 moves the corner half an octave per octave played:
55/// enough that a patch still speaks two octaves above where it was dialled in,
56/// not so much that a bass patch turns thin in the upper register. Fixed rather
57/// than a knob because a `keytrack` genome field is a grammar-shape change.
58const KEYTRACK_AMT: f64 = 0.5;
59/// Attack time of every [`ModNode::Follow`] detector, normalized on quiver's
60/// `0.1 + 99.9·x` ms map — so 0.05 is ≈5 ms. Fixed rather than a knob because
61/// the faceplate is already at its four-knob budget, and because a follower
62/// that is slow on the *attack* stops being an envelope follower: it misses
63/// the transient, which is the only part of a note whose dynamics carry
64/// timbral information the rest of the patch does not already have. Release
65/// is the musical choice, so release gets the knob.
66const FOLLOW_ATTACK: f64 = 0.05;
67/// Number of allpass stages in the phaser, as quiver's `stages` CV (< 0.33 →
68/// 2, < 0.66 → 4, else 6). Pinned to 6: fewer stages give fewer notches, and
69/// a two-notch phaser on a bright source is hard to tell from a chorus.
70const PHASER_STAGES: f64 = 1.0;
71/// Phaser stereo spread (0 = mono, 1 = the two sweeps 180° apart). A little
72/// under half keeps the notches audibly decorrelated without the swimming,
73/// phase-cancelling collapse a full 180° gives on a mono playback system.
74const PHASER_SPREAD: f64 = 0.35;
75/// Phaser wet/dry. A phaser *is* the interference between wet and dry, so an
76/// even blend is the only setting at which the notches reach full depth;
77/// `#pdepth` and `#pfb` are the expressive controls and this is not one.
78const PHASER_MIX: f64 = 0.5;
79/// The formant oscillator's own vibrato depth (quiver's port 3, a fixed
80/// 5.5 Hz LFO on pitch). Pinned off: a pre-baked vibrato at a rate the patch
81/// cannot name is exactly what the modulation slot exists to replace, and now
82/// that [`Offset`]-based pitch modulation exists the grammar can express the
83/// same gesture with a rate, a waveform and a depth of its own.
84const FORMANT_VIBRATO: f64 = 0.0;
85/// Flanger wet/dry. A flanger *is* the interference between the swept comb and
86/// the dry signal, so an even blend is where the notches reach full depth —
87/// the same argument as [`PHASER_MIX`], and `#fdepth`/`#ffb` are the
88/// expressive controls.
89const FLANGER_MIX: f64 = 0.5;
90/// Flanger stereo spread (0 = mono, 1 = the two sweeps 180° apart). Matched to
91/// [`PHASER_SPREAD`] for the same reason and, incidentally, because a non-zero
92/// spread is what makes quiver's ports 11/12 differ at all — at 0 they are
93/// bit-identical and the stereo pair would be a lie.
94const FLANGER_SPREAD: f64 = 0.35;
95/// EQ low-shelf corner, on quiver's `50·10^cv` Hz map — 0.2 is ≈79 Hz, under
96/// the fundamental of most of what this instrument plays, so the shelf lifts
97/// or cuts *weight* rather than re-voicing the note.
98const EQ_LOW_FREQ: f64 = 0.2;
99/// EQ mid-bell centre, on quiver's `200·40^cv` Hz map — 0.5 is ≈1.26 kHz, the
100/// presence region where a synth patch reads as forward or recessed.
101const EQ_MID_FREQ: f64 = 0.5;
102/// EQ mid-bell Q, on quiver's `0.5 + 9.5·cv` map — 0.35 is Q ≈ 3.8, so the
103/// bell is about a third of an octave wide. Narrow enough that the band is a
104/// *place* rather than a broad tilt the two shelves already cover, wide enough
105/// that a full cut is a scoop and not a notch.
106const EQ_MID_Q: f64 = 0.35;
107/// EQ high-shelf corner, on quiver's `2000 + 10000·cv` Hz map — 0.5 is 7 kHz,
108/// above the highest fundamental the keyboard reaches, so the shelf is
109/// unambiguously air and never a second mid control.
110const EQ_HIGH_FREQ: f64 = 0.5;
111/// Granular pitch shift. Pinned to no transposition: quiver reads this port as
112/// ±24 semitones, and a granulator that also transposes is a second pitch
113/// source fighting the keyboard for the same note.
114const GRANULAR_PITCH: f64 = 0.0;
115/// Granular position randomization. A little spray decorrelates the grain
116/// starts so overlapping grains stop phase-summing into a single tone; at 0
117/// the module is an odd stutter rather than a texture.
118const GRANULAR_SPRAY: f64 = 0.15;
119/// Granular buffer freeze (a quiver `Gate` port). Pinned open: freeze is a
120/// performance gesture, not a genome parameter, and a frozen buffer in an
121/// evolved patch is a patch that ignores the keyboard — every note after the
122/// first would replay the first one's audio.
123const GRANULAR_FREEZE: f64 = 0.0;
124/// Compressor attack, on quiver's `0.1 + 99.9·x` ms map — 0.15 is ≈15 ms.
125/// Slow enough to let a transient through before the gain moves, which is what
126/// makes a compressed patch still sound plucked. Fixed rather than a knob
127/// because ratio and threshold are the character and the faceplate is at its
128/// four-knob budget; the ballistics are where that budget spends least.
129const COMP_ATTACK: f64 = 0.15;
130/// Compressor release, on quiver's `10 + 990·x` ms map — 0.35 is ≈357 ms.
131/// Long enough that the gain does not chatter on a decaying note, short enough
132/// that a sidechain pump recovers inside one beat at any tempo the phrase
133/// implies. Fixed for the same reason as [`COMP_ATTACK`].
134const COMP_RELEASE: f64 = 0.35;
135/// Ducker attack, on quiver's `0.1 + 99.9·x` ms map — 0.05 is ≈5 ms. A ducker
136/// that opens slowly is a ducker you cannot hear working: the whole gesture is
137/// the *edge* of the key's transient, and anything past ~10 ms puts the duck
138/// behind the hit that caused it.
139const DUCK_ATTACK: f64 = 0.05;
140/// Gate attack, on quiver's `0.1 + 49.9·x` ms map — 0.02 is ≈1.1 ms. Same
141/// argument as [`DUCK_ATTACK`], and more so: a gate that opens slowly eats the
142/// transient it was opened by, which is the one part of the note that carried
143/// the information.
144const GATE_ATTACK: f64 = 0.02;
145/// [`quiver::modules::Euclidean`]'s pattern rotation. Pinned to no rotation:
146/// with `steps` and `pulses` both live, rotation only chooses *which* of the
147/// pattern's rests the cycle starts on, which is a phase and not a timbre —
148/// and a phase is inaudible in a five-second phrase that fires the pattern
149/// once or twice.
150const EUCLID_ROTATION: f64 = 0.0;
151/// Attenuverter level (gain = `level/5`) on the **input** of a
152/// [`ModOp::Quantize`], and its inverse on the output.
153///
154/// quiver's `ScaleQuantizer` reads and writes **V/Oct**: it snaps to the
155/// nearest scale degree on a fixed 1/12 V grid. Handed a modulator at its
156/// native ±5 V that is ±60 semitones, so the port emits 121 steps — finer than
157/// the destination can show and, after the mod cable's own attenuation, finer
158/// than the ear can hear. It would have been a quantizer that reviews as
159/// correct and sounds continuous.
160///
161/// So the input is scaled *into* a musical window and the output scaled back
162/// out, leaving the cable's gain unchanged and only the **grid** resized. At
163/// level 0.5 (gain 0.1) a ±5 V source arrives as ±0.5 V = ±6 semitones, so the
164/// scale gets 13 chromatic degrees to choose from — and the effective grid
165/// referred to the mod cable is `(1/12)/0.1 = 0.833 V`.
166///
167/// That number is chosen so the headline case lands exactly: on the pitch
168/// [`Offset`] at full `mod_depth` the cable's gain is 0.1, so a grid step is
169/// `0.833 · 0.1 = 1/12 V` — **one semitone**, over the ±6 semitones
170/// [`map::mod_depth_pitch`] allows. A quantized random melody is in tune at
171/// depth 1.0 and in a stretched tuning below it, which is the honest
172/// consequence of putting one attenuverter between every modulator and its
173/// destination: depth scales interval size.
174const QUANTIZE_IN_LEVEL: f64 = 0.5;
175/// The output side of [`QUANTIZE_IN_LEVEL`] — `25 / QUANTIZE_IN_LEVEL`, so the
176/// two gains multiply to exactly 1 and the op is transparent in scale.
177/// `Attenuverter` is `in · level/5` with no clamp, and a pinned port default
178/// is not range-checked, so a level above 5 V is a real gain of 10.
179const QUANTIZE_OUT_LEVEL: f64 = 25.0 / QUANTIZE_IN_LEVEL;
180/// Normalized cutoff of the voice's DC blocker. quiver maps `cutoff` as
181/// `20·1000^x` and then hard-clamps to 20 Hz, so 0.0 is the lowest corner the
182/// engine can produce — measured at −17 dB at 8 Hz, −2.7 dB at C1 and −0.8 dB
183/// at C2, which blocks offset without auditing as a bass cut.
184const DC_BLOCK_CUTOFF: f64 = 0.0;
185
186/// How a normalized knob value maps to the volts written to its handle.
187#[derive(Clone, Copy, Debug)]
188pub enum ParamMap {
189 /// Pass through (0..1 knob CV).
190 Unit,
191 /// Bounded resonance (`0.85·x`).
192 Resonance,
193 /// Bounded delay feedback (`0.7·x`).
194 Feedback,
195 /// Bounded feedback on a bipolar port (`(2x−1)·0.7`), where the sign of
196 /// the feedback is itself a timbre.
197 FeedbackBipolar,
198 /// Crossfader position (`(2x−1)·5 V`).
199 XfadePos,
200 /// Wavefolder threshold (`0.1 + 0.9·x`).
201 FoldThreshold,
202 /// Shelf/bell gain on a ±5 V port (`(2x−1)·5`), where knob centre must be
203 /// 0 dB.
204 GainBipolar,
205 /// Formant shift on a ±5 V port (`(2x−1)·5`), where knob centre is no
206 /// shift.
207 FormantShift,
208 /// A [`quiver::modules::Clock`] tempo (`10·x`), i.e. the port's whole
209 /// 0–10 V range.
210 ClockRate,
211 /// Euclidean step count (`0.14 + 0.86·x`), i.e. 4..16 rather than 2..16.
212 EuclidSteps,
213 /// Euclidean pulse density (`0.25 + 0.74·x`), bounded off both degenerate
214 /// ends at every step count.
215 EuclidPulses,
216 /// A [`quiver::modules::SlewLimiter`] time (`0.4·x`), i.e. the bottom of a
217 /// port whose own map is already square-law.
218 SlewTime,
219 /// Transposition on the pitch shifter's ±5 V `shift` port
220 /// (`(2x−1)·2.5`), i.e. ∓12 semitones with unison at knob centre.
221 Semitones,
222 /// The ducker's `amount`, a bipolar CV summed onto a knob base of 1.0
223 /// (`(x−1)·5`).
224 DuckAmount,
225 /// A dynamics detector threshold, geometric over 0.05–5 V, on a port
226 /// quiver reads as `cv · 5` volts.
227 DetectorThreshold,
228 /// The same threshold on the ducker's `ModulatedParam` port, which reads
229 /// as `(0.2 + cv/5)·5` volts.
230 DuckThreshold,
231 /// Mod depth for a ±5 V source into a **normalized** 0..1 port.
232 ModDepthBipolar,
233 /// Mod depth for a 0–10 V source into a **normalized** 0..1 port.
234 ModDepthUnipolar,
235 /// Mod depth for a ±5 V source into the **pitch** [`Offset`] (V/Oct).
236 ModDepthPitch,
237 /// Mod depth for a 0–10 V source into the **pitch** [`Offset`] (V/Oct).
238 ModDepthPitchUnipolar,
239 /// Mod depth for a ±5 V source into a **±5 V gain** port (the EQ bands).
240 ModDepthGain,
241 /// Mod depth for a 0–10 V source into a **±5 V gain** port.
242 ModDepthGainUnipolar,
243 /// Mod depth for a ±5 V source into the pitch shifter's **semitone** port.
244 ModDepthShift,
245 /// Mod depth for a 0–10 V source into the **semitone** port.
246 ModDepthShiftUnipolar,
247 /// Mod depth for a ±5 V source into a [`quiver::prelude::ModulatedParam`]
248 /// knob+CV port, where ±5 V spans the whole normalized parameter.
249 ModDepthParamCv,
250 /// Mod depth for a 0–10 V source into a `ModulatedParam` knob+CV port.
251 ModDepthParamCvUnipolar,
252 /// Mod depth for a ±5 V source into a **dynamics threshold** port.
253 ModDepthDetector,
254 /// Mod depth for a 0–10 V source into a dynamics threshold port.
255 ModDepthDetectorUnipolar,
256 /// **Categorical.** Wavetable select, as a table *index* rather than a
257 /// 0..1 knob: `i ↦ i/7`, the same [`map::table_cv`] the port used to be
258 /// pinned to. See [`Self::clamp_input`] for why the domain is not 0..1.
259 TableIndex,
260 /// **Categorical.** Octave select, as an index `0..=4` (`i ↦ i−2`
261 /// octaves), *minus the octave already baked into this voice's pitch
262 /// [`Offset`]* — which is the `i8` payload.
263 ///
264 /// The relative form is the whole trick, and it is what keeps this change
265 /// from moving a single sample of an existing patch. quiver **sums** every
266 /// cable into a patched input and **writes** the default into an unpatched
267 /// one, so a cable carrying the absolute offset would have to be added in
268 /// a different place in the sum than the [`Offset`]'s own constant is:
269 /// `(pitch + oct) + detune` instead of `pitch + (oct + detune)`, which
270 /// disagree in the last bit, and again with a pitch-mod cable in the sum.
271 /// A trim is `+0.0` at compile time, and `x + 0.0` is exactly `x`.
272 ///
273 /// It also means the panel and the compiler can never double-count: a
274 /// recompile re-bakes whatever octave the tree now holds and re-zeroes the
275 /// trim in the same breath.
276 OctaveTrim(i8),
277}
278
279impl ParamMap {
280 /// Clamp a value arriving from the panel to this map's input domain.
281 ///
282 /// Continuous knobs are 0..1 and always were. The two categorical sites
283 /// that became live send a *category index*, so the blanket
284 /// `value.clamp(0.0, 1.0)` the live path used to apply would have folded
285 /// all eight wavetables onto the first two.
286 pub fn clamp_input(self, x: f64) -> f64 {
287 match self {
288 ParamMap::TableIndex => x.round().clamp(0.0, 7.0),
289 ParamMap::OctaveTrim(_) => x.round().clamp(0.0, 4.0),
290 _ => x.clamp(0.0, 1.0),
291 }
292 }
293
294 /// Map a normalized value to the wire value.
295 pub fn apply(self, x: f64) -> f64 {
296 match self {
297 ParamMap::Unit => x,
298 ParamMap::Resonance => map::resonance(x),
299 ParamMap::Feedback => map::feedback(x),
300 ParamMap::FeedbackBipolar => map::feedback_bipolar(x),
301 ParamMap::XfadePos => map::xfade_pos(x),
302 ParamMap::FoldThreshold => map::fold_threshold(x),
303 ParamMap::GainBipolar => map::gain_bipolar(x),
304 ParamMap::FormantShift => map::formant_shift(x),
305 ParamMap::ClockRate => map::clock_rate(x),
306 ParamMap::EuclidSteps => map::euclid_steps(x),
307 ParamMap::EuclidPulses => map::euclid_pulses(x),
308 ParamMap::SlewTime => map::slew_time(x),
309 ParamMap::Semitones => map::semitones(x),
310 ParamMap::DuckAmount => map::duck_amount(x),
311 ParamMap::DetectorThreshold => map::detector_threshold(x),
312 ParamMap::DuckThreshold => map::duck_threshold(x),
313 ParamMap::ModDepthBipolar => map::mod_depth_bipolar(x),
314 ParamMap::ModDepthUnipolar => map::mod_depth_unipolar(x),
315 ParamMap::ModDepthPitch => map::mod_depth_pitch(x),
316 ParamMap::ModDepthPitchUnipolar => map::mod_depth_pitch_unipolar(x),
317 ParamMap::ModDepthGain => map::mod_depth_gain(x),
318 ParamMap::ModDepthGainUnipolar => map::mod_depth_gain_unipolar(x),
319 ParamMap::ModDepthShift => map::mod_depth_shift(x),
320 ParamMap::ModDepthShiftUnipolar => map::mod_depth_shift_unipolar(x),
321 ParamMap::ModDepthParamCv => map::mod_depth_param_cv(x),
322 ParamMap::ModDepthParamCvUnipolar => map::mod_depth_param_cv_unipolar(x),
323 ParamMap::ModDepthDetector => map::mod_depth_detector(x),
324 ParamMap::ModDepthDetectorUnipolar => map::mod_depth_detector_unipolar(x),
325 ParamMap::TableIndex => map::table_cv(x),
326 ParamMap::OctaveTrim(baked) => (x - 2.0) - baked as f64,
327 }
328 }
329}
330
331/// Which *destination* a modulation cable is headed for.
332///
333/// The attenuverter level that means "full depth" is a property of the
334/// destination's volt scale, not of the knob: a normalized 0..1 CV port, the
335/// V/Oct pitch [`Offset`] and a ±5 V gain port all want different levels for
336/// the same musical amount. [`Compiler::wire_mod`] picks the source polarity;
337/// this picks the scale, and the two together choose the taper.
338#[derive(Clone, Copy, Debug)]
339enum DepthScale {
340 /// A 0..1 CV port (cutoff, morph, depth, drive, position, …) — where
341 /// almost every mod slot in this grammar lands.
342 Normalized,
343 /// The pitch [`Offset`]'s summing input, in V/Oct.
344 Pitch,
345 /// A ±5 V gain port, read by quiver as `cv/5 · 12` dB.
346 Gain,
347 /// The pitch shifter's `shift` port, read by quiver as `cv/5 · 24`
348 /// semitones.
349 Shift,
350 /// A [`quiver::prelude::ModulatedParam`] knob+CV port — the ducker's
351 /// `amount` and `threshold`. The CV is summed onto the module's own knob
352 /// base after `cv / 5`, so ±5 V spans the parameter's *whole* normalized
353 /// range rather than the 0..1 the plain CV ports carry.
354 ParamCv,
355 /// A dynamics detector threshold: a 0..1 CV port whose *knob* is
356 /// geometric, so the useful settings crowd the bottom of it.
357 Detector,
358}
359
360impl DepthScale {
361 /// The taper for this destination, given the source's polarity.
362 fn taper(self, unipolar: bool) -> ParamMap {
363 match (self, unipolar) {
364 (DepthScale::Normalized, false) => ParamMap::ModDepthBipolar,
365 (DepthScale::Normalized, true) => ParamMap::ModDepthUnipolar,
366 (DepthScale::Pitch, false) => ParamMap::ModDepthPitch,
367 (DepthScale::Pitch, true) => ParamMap::ModDepthPitchUnipolar,
368 (DepthScale::Gain, false) => ParamMap::ModDepthGain,
369 (DepthScale::Gain, true) => ParamMap::ModDepthGainUnipolar,
370 (DepthScale::Shift, false) => ParamMap::ModDepthShift,
371 (DepthScale::Shift, true) => ParamMap::ModDepthShiftUnipolar,
372 (DepthScale::ParamCv, false) => ParamMap::ModDepthParamCv,
373 (DepthScale::ParamCv, true) => ParamMap::ModDepthParamCvUnipolar,
374 (DepthScale::Detector, false) => ParamMap::ModDepthDetector,
375 (DepthScale::Detector, true) => ParamMap::ModDepthDetectorUnipolar,
376 }
377 }
378}
379
380/// A live control: the atomic the audio thread reads, plus the knob mapping.
381#[derive(Clone)]
382pub struct ParamHandle {
383 /// Shared with the running patch — writing it changes the sound on the
384 /// next sample, no recompilation.
385 pub value: Arc<AtomicF64>,
386 /// Normalized-to-volts mapping.
387 pub map: ParamMap,
388}
389
390impl ParamHandle {
391 /// Write a knob value in the site's own units — 0..1 for a continuous
392 /// knob, a category index for the two live categorical sites (see
393 /// [`ParamMap::clamp_input`], which is why this is no longer a bare
394 /// `clamp(0.0, 1.0)`).
395 pub fn set_normalized(&self, x: f64) {
396 self.value.set(self.map.apply(self.map.clamp_input(x)));
397 }
398}
399
400/// The node name of the mandatory amp envelope. Every compiled voice has
401/// exactly one, and [`CompiledVoice::seed_env_phase`] is the only thing that
402/// reaches for it by name.
403const AMP_ADSR: &str = "voice:adsr";
404/// `Adsr`'s `env` output port id (0–10 V unipolar).
405const ADSR_ENV_PORT: PortId = 10;
406/// quiver's `Adsr` runs its exponential segments until it is within this of
407/// the target, then snaps. Mirrored here so the seeder knows when a segment
408/// has finished rather than guessing at a settling time.
409const ADSR_EXP_DONE: f64 = 1.0e-3;
410/// Ticks [`CompiledVoice::seed_env_phase`] will spend per segment. The
411/// envelope is running its fastest possible segment (1 ms) while it seeds, so
412/// ~7 time constants is a few hundred ticks at any sane rate; this is the
413/// guard rail, not the expected cost.
414const SEED_MAX_TICKS: usize = 4096;
415
416/// A compiled, playable voice: the patch plus its external control handles.
417pub struct CompiledVoice {
418 /// The compiled quiver patch (output already selected and compiled).
419 pub patch: Patch,
420 /// Pitch control, V/Oct (0 V = C4). Shared with the patch.
421 pub pitch: Arc<AtomicF64>,
422 /// Gate control (≥ 2.5 V = on). Shared with the patch.
423 pub gate: Arc<AtomicF64>,
424 /// Live parameter handles, keyed by the knob's trace address
425 /// (`node/0#cut`, `amp#attack`, `node/0#table`, `node/0#oct`, …).
426 /// Everything the panel can move without a recompile.
427 pub params: HashMap<String, ParamHandle>,
428 /// Signal-kind warnings accumulated while wiring (Warn mode).
429 pub warnings: Vec<String>,
430 /// Where each term node's audio leaves it: trace key → the **name** of the
431 /// quiver node carrying its output, and the port id on that node.
432 ///
433 /// Named rather than `NodeId`-keyed because that is what
434 /// `StateObserver::add_subscriptions` takes, and because a name survives
435 /// the patch being rebuilt while a `NodeId` does not — the rack asks for a
436 /// tap by the key of the module the player is looking at, and the answer
437 /// has to still mean something after the next swap.
438 ///
439 /// **Every tap here already has a consumer**, which is why nothing needs
440 /// `StateObserver::sync_output_keepalive`: the genome is a typed tree, so
441 /// each module's output feeds exactly one parent and quiver is already
442 /// producing it. That call exists for ports nothing reads, and it dirties
443 /// the patch — a recompile the audio thread would have to be staged around.
444 /// Metering here costs no recompile at all.
445 pub taps: HashMap<String, (String, PortId)>,
446}
447
448impl CompiledVoice {
449 /// Where the amp envelope is right now, 0..1 — quiver's 0–10 V `env`
450 /// output scaled back down.
451 ///
452 /// Reads the routing's last computed value, so it is meaningful after the
453 /// voice has ticked at least once and zero before that.
454 pub fn env_phase(&self) -> f64 {
455 self.patch
456 .get_node_id_by_name(AMP_ADSR)
457 .and_then(|n| self.patch.get_output_value(n, ADSR_ENV_PORT))
458 .map(|v| (v * 0.1).clamp(0.0, 1.0))
459 .unwrap_or(0.0)
460 }
461
462 /// Fast-forward this voice's amp envelope to `level` (0..1), with the gate
463 /// **already high**, so a note carried across a patch swap resumes where it
464 /// was instead of re-attacking from silence.
465 ///
466 /// ## Why this is a pre-roll and not a setter
467 ///
468 /// The plan asked for an envelope-phase getter *and setter* on this type.
469 /// The getter is above and is honest. The setter cannot be: quiver's
470 /// `Adsr` keeps `stage` and `level` private and exposes no parameter,
471 /// state-serialization or introspection surface for them (`GraphModule`
472 /// gives it `port_spec`/`tick`/`reset`/`set_sample_rate`/`type_id` and
473 /// nothing else), so there is no way to write a level into it from
474 /// outside. The alternative was to fork the ADSR into this crate to gain
475 /// two accessors, which would put a hand-copy of quiver's envelope
476 /// arithmetic — `Libm` transcendentals and all — on the critical path of
477 /// every patch the instrument has ever rendered. That is a rendered-audio
478 /// change dressed as a refactor.
479 ///
480 /// So the envelope is driven to the level the same way the player would:
481 /// the attack and decay CVs are pinned to their fastest (1 ms) settings,
482 /// the patch is ticked in silence until `env` arrives, and the CVs are put
483 /// back. It costs a few hundred ticks — well inside the swap's silent
484 /// window, which already budgets a whole voice compile per quantum — and
485 /// it leaves the envelope in the *stage* the level implies, which is the
486 /// part that actually matters:
487 ///
488 /// - below sustain, only Attack can be there, so the rise stops on arrival
489 /// and the envelope goes on attacking at its real rate;
490 /// - at or above sustain, the note is in Decay or Sustain, so the rise runs
491 /// to the peak (which is what puts quiver's stage machine into Decay) and
492 /// then falls to the level.
493 ///
494 /// A side effect worth naming: the pre-roll is real audio, so it also
495 /// primes the new voice's filters and delay lines with ~15 ms of its own
496 /// signal rather than handing the fade-in an empty reverb. Tails still do
497 /// not transfer across a rewire — that is [R3] and is accepted.
498 ///
499 /// Returns whether anything was seeded.
500 ///
501 /// [R3]: the panel plan's §4 risk register.
502 pub fn seed_env_phase(&mut self, level: f64) -> bool {
503 let level = level.clamp(0.0, 1.0);
504 let Some(adsr) = self.patch.get_node_id_by_name(AMP_ADSR) else {
505 return false;
506 };
507 // A percussive patch (sustain 0) whose note has already decayed has no
508 // phase to carry, and neither has a note that never sounded.
509 if level <= 0.0 {
510 return false;
511 }
512 let (Some(attack), Some(decay), Some(sustain)) = (
513 self.params.get("amp#attack").cloned(),
514 self.params.get("amp#decay").cloned(),
515 self.params.get("amp#sustain").cloned(),
516 ) else {
517 return false;
518 };
519 let sustain = sustain.value.get();
520 let (a0, d0) = (attack.value.get(), decay.value.get());
521 // `ParamMap::Unit` on both, and quiver maps 0 V to its 1 ms floor.
522 attack.value.set(0.0);
523 decay.value.set(0.0);
524
525 let peak = if level < sustain { level } else { 1.0 };
526 for _ in 0..SEED_MAX_TICKS {
527 let now = self
528 .patch
529 .get_output_value(adsr, ADSR_ENV_PORT)
530 .unwrap_or(0.0)
531 * 0.1;
532 // The exponential attack snaps to exactly 1.0 (and hands the stage
533 // machine over to Decay) once it is within `ADSR_EXP_DONE`.
534 if now >= peak - ADSR_EXP_DONE {
535 break;
536 }
537 self.patch.tick();
538 }
539 if level >= sustain {
540 for _ in 0..SEED_MAX_TICKS {
541 let now = self
542 .patch
543 .get_output_value(adsr, ADSR_ENV_PORT)
544 .unwrap_or(0.0)
545 * 0.1;
546 if now <= level {
547 break;
548 }
549 self.patch.tick();
550 }
551 }
552
553 attack.value.set(a0);
554 decay.value.set(d0);
555 true
556 }
557}
558
559/// Bounded musical mappings from normalized genome parameters.
560mod map {
561 /// Resonance: cap below self-oscillation screech.
562 pub fn resonance(x: f64) -> f64 {
563 0.85 * x
564 }
565 /// Delay feedback: cap below runaway.
566 pub fn feedback(x: f64) -> f64 {
567 0.7 * x
568 }
569 /// Feedback on a port whose *sign* is musical (the phaser's resonance:
570 /// negative feedback notches, positive peaks). Knob centre is no
571 /// feedback, and the ends stop short of quiver's own ±0.95 clamp so the
572 /// allpass chain never sits on the edge of ringing.
573 pub fn feedback_bipolar(x: f64) -> f64 {
574 (2.0 * x - 1.0) * 0.7
575 }
576 /// Wavetable select: the CV that lands table `i` of eight.
577 ///
578 /// The port is a **crossfade position**, not a quantizer, and getting that
579 /// wrong is inaudible in a code review and unmissable at the keyboard.
580 /// quiver computes `table_pos = cv·7`, takes `idx = floor(table_pos)` and
581 /// then blends table `idx` into table `idx+1` by `frac + morph`
582 /// (`quiver::modules::Wavetable`, oscillators.rs). So the cell-centre
583 /// convention that is right for the *quantized* `mode` port below is
584 /// exactly wrong here: `(i + 0.5)/8` put every table at a fractional
585 /// position, which meant picking `sine` gave 56% sine and 44% triangle —
586 /// and left `morph`, the knob this module exists for, with only the top
587 /// half of its travel doing anything before `frac + morph` clamped at 1.
588 ///
589 /// `i/7` lands `frac` on exactly 0 for every table, so the plate names what
590 /// you hear and morph sweeps the whole way to the next shape. (`i = 7`
591 /// gives `table_pos = 7`, which quiver clamps to `idx = 6, frac = 1.0` —
592 /// i.e. table 7 at full blend, still exact.)
593 ///
594 /// The index is an `f64` because the site is live: a smoothed write ramps
595 /// *through* the fractional positions between two tables, which is the
596 /// morph this port was always capable of and the panel could never ask
597 /// for. Integral inputs are the shapes the grammar can name.
598 pub fn table_cv(index: f64) -> f64 {
599 index / 7.0
600 }
601 /// Distortion mode select. quiver quantizes this port as `cv·3.99`, and
602 /// its slot 2 is foldback, which this grammar deliberately does not
603 /// expose (that module is [`crate::term::AudioNode::Fold`]), so the three
604 /// values step *over* it: 0.125 → soft, 0.375 → hard, 0.875 → tube.
605 pub fn drive_mode_cv(index: usize) -> f64 {
606 match index {
607 0 => 0.125,
608 1 => 0.375,
609 _ => 0.875,
610 }
611 }
612 /// Wavefolder threshold: keep off the hard-zero fold-everything corner.
613 pub fn fold_threshold(x: f64) -> f64 {
614 0.1 + 0.9 * x
615 }
616 /// Shelf/bell gain on a bipolar ±5 V port. quiver's `ParametricEq` reads
617 /// each band as `cv/5 · 12` dB, so this spans ±12 dB with **unity at knob
618 /// centre** — the only sane home position for a tone control, and the
619 /// reason a freshly placed eq is audibly a no-op until you move it.
620 pub fn gain_bipolar(x: f64) -> f64 {
621 (2.0 * x - 1.0) * 5.0
622 }
623 /// Formant shift on a bipolar ±5 V port. quiver's `FormantOsc` applies
624 /// `2^(cv/5)` to every formant frequency, so the full sweep is 0.5×–2×
625 /// (an octave either way) with **no shift at knob centre**. Both ends stay
626 /// vocal: at 2× the /i/ formants land where a child's do, and at 0.5×
627 /// where a very large chest does. Passing the raw 0..1 knob instead would
628 /// have given 1.0×–1.15× and no downward shift at all.
629 pub fn formant_shift(x: f64) -> f64 {
630 (2.0 * x - 1.0) * 5.0
631 }
632 /// Transposition on the pitch shifter's bipolar `shift` port. quiver reads
633 /// it as `cv/5 · 24` semitones and hard-clamps at ±24 (`PitchShifter`,
634 /// nonlinear.rs), so a volt is 4.8 semitones and the port's full swing is
635 /// two octaves each way.
636 ///
637 /// Half of it is the knob: **±12 semitones with unison at centre**. Two
638 /// reasons for stopping there rather than at the rail. Musically, an
639 /// octave either way is the whole harmony vocabulary this module has —
640 /// the module aliases by design (no oversampling) and two octaves up is a
641 /// 4× resample of a buffer that is already grainy. Structurally, the knob
642 /// and the modulation cable **sum on this one port** (as on the wavefolder
643 /// threshold), so leaving half the port free means a fully modulated,
644 /// fully transposed shifter lands exactly on quiver's ±24 clamp instead of
645 /// pinning against it for most of the sweep.
646 pub fn semitones(x: f64) -> f64 {
647 (2.0 * x - 1.0) * SHIFT_PEAK_V
648 }
649 /// The ducker's `amount` knob, in volts on its bipolar CV port.
650 ///
651 /// This port is **not** a plain CV: quiver reads it through a
652 /// [`ModulatedParam`](quiver::prelude::ModulatedParam) whose value is
653 /// `base + cv/5` over a `Linear{0, 1}` range, and `Ducker::new` sets
654 /// `base = 1.0`. The base is only reachable through `set_amount` on the
655 /// Rust struct — there is no port for it — so the *knob* has to arrive as
656 /// the CV, and it arrives as a **negative offset from full depth**: knob
657 /// 1.0 is 0 V (duck all the way), knob 0.0 is −5 V (do not duck at all).
658 ///
659 /// Passing the raw 0..1 knob instead would have run the parameter from
660 /// 1.0 to 1.2 and clamped — a control that is at full depth across its
661 /// entire travel and reviews as correct because the cable is there.
662 pub fn duck_amount(x: f64) -> f64 {
663 (x - DUCK_AMOUNT_BASE) * PARAM_CV_FULL_SCALE_V
664 }
665 /// Detector level, in volts, for a dynamics threshold knob — **geometric**
666 /// over 0.05–5 V rather than linear over 0–5 V.
667 ///
668 /// Every one of quiver's three dynamics modules reads its threshold as a
669 /// straight `cv · 5` volts against a smoothed `|x|` detector, and passing
670 /// the raw knob through would have been the wave-2A eq bug in reverse: not
671 /// a control that is too small to hear, but one whose entire useful range
672 /// is squeezed into the bottom tenth of its travel.
673 ///
674 /// The reason is that this instrument's sources are nowhere near a common
675 /// level. Measured as mean `|x|` on a held note through the voice tail: a
676 /// sine vco is 3.18 V, a supersaw ≈0.6 V, and a **plucked string 0.14 V** —
677 /// 27 dB below the vco, and the pluck is precisely what a gate or a ducker
678 /// is most often keyed from. A linear 0–5 V knob puts every source but the
679 /// oscillators under knob position 0.1; the default gate threshold of 0.35
680 /// measured as 1.75 V, which no key in the palette ever reaches, so the
681 /// gate sat shut for the whole note and read as a fixed −10 dB pad.
682 ///
683 /// 0.05–5 V is 40 dB, which covers that spread with the midpoint (0.5 V)
684 /// between a supersaw and a pluck. Geometric, because level is.
685 fn detector_volts(x: f64) -> f64 {
686 DETECT_MIN_V * (DETECT_MAX_V / DETECT_MIN_V).powf(x.clamp(0.0, 1.0))
687 }
688 /// [`detector_volts`] on the compressor's and gate's plain CV ports, which
689 /// quiver reads as `clamp(cv, 0, 1) · 5` volts.
690 pub fn detector_threshold(x: f64) -> f64 {
691 detector_volts(x) / PARAM_CV_FULL_SCALE_V
692 }
693 /// [`detector_volts`] on the ducker's `ModulatedParam` port.
694 ///
695 /// Same shape as [`duck_amount`] — the knob arrives as an offset from
696 /// quiver's own base — but the range is `Linear{0, 5}` **volts** of key
697 /// level, so the port resolves to `(0.2 + cv/5)·5 = 1 + cv` volts and the
698 /// knob is the wanted level minus one.
699 pub fn duck_threshold(x: f64) -> f64 {
700 detector_volts(x) - DUCK_THRESHOLD_BASE * PARAM_CV_FULL_SCALE_V
701 }
702 /// Attenuverter level, in volts, for a destination whose full modulation
703 /// excursion is `peak` volts, driven by a **±5 V** source.
704 ///
705 /// The attenuverter's gain is `level / 5`, so a ±5 V source arrives at
706 /// `±level` volts: the level *is* the peak excursion.
707 fn mod_level_bipolar(peak: f64, x: f64) -> f64 {
708 peak * x
709 }
710 /// The same, driven by a **0–10 V** source (the mod envelope and the
711 /// follower). Half the level for the same peak excursion, so both source
712 /// families reach the same depth at the same knob position.
713 fn mod_level_unipolar(peak: f64, x: f64) -> f64 {
714 peak * x * 0.5
715 }
716 /// Peak excursion for a **normalized 0..1** destination port: half of full
717 /// scale at knob 1.0.
718 const PEAK_NORMALIZED: f64 = 0.5;
719 /// Peak excursion for the **pitch [`Offset`]**, in volts — which on a
720 /// V/Oct summing input is numerically octaves, so knob 1.0 is ±0.5 octave.
721 const PEAK_PITCH: f64 = 0.5;
722 /// Peak excursion for a **±5 V gain** port: the whole port, i.e. ±12 dB at
723 /// knob 1.0.
724 const PEAK_GAIN: f64 = 5.0;
725 /// Half of the pitch shifter's `shift` port, in volts — the same half the
726 /// knob gets (see [`semitones`]), so knob and cable each own one octave
727 /// and their sum lands on quiver's ±24-semitone clamp rather than through
728 /// it.
729 pub(super) const SHIFT_PEAK_V: f64 = 2.5;
730 /// Volts of CV that move a [`ModulatedParam`](quiver::prelude::ModulatedParam)
731 /// across its whole normalized range — quiver's
732 /// `ModulatedParam::CV_FULL_SCALE_VOLTS`.
733 pub(super) const PARAM_CV_FULL_SCALE_V: f64 = 5.0;
734 /// `Ducker::new`'s `amount` knob base. The CV port offsets *this*.
735 pub(super) const DUCK_AMOUNT_BASE: f64 = 1.0;
736 /// `Ducker::new`'s `threshold` knob base, on its 0–5 V range.
737 pub(super) const DUCK_THRESHOLD_BASE: f64 = 0.2;
738 /// Quietest detector level a dynamics threshold knob can ask for, in
739 /// volts. Under a plucked string's own envelope, so knob 0 is "trigger on
740 /// anything" for every source in the palette.
741 pub(super) const DETECT_MIN_V: f64 = 0.05;
742 /// Loudest — the nominal full scale of quiver audio, so knob 1 is
743 /// "trigger on nothing short of a bare oscillator".
744 pub(super) const DETECT_MAX_V: f64 = 5.0;
745 /// Peak excursion for a `ModulatedParam` port: half of the parameter's
746 /// full normalized range, matching [`PEAK_NORMALIZED`]'s convention — but
747 /// **ten times its volts**, because on this port a normalized unit costs
748 /// 5 V rather than 1. Getting that wrong is the eq's ±1.2 dB bug again,
749 /// with the ducker's depth knob doing nothing across its whole travel.
750 const PEAK_PARAM_CV: f64 = 2.5;
751 /// Peak excursion for a **dynamics threshold** port, in the port's own
752 /// 0..1 CV units — 0.1, i.e. ±0.5 V of detector level. A tenth of full
753 /// scale rather than a half, for the reason on [`mod_depth_detector`].
754 const PEAK_DETECTOR: f64 = 0.1;
755 /// Modulation depth for a ±5 V source (LFO, S&H), expressed as an
756 /// [`quiver::modules::Attenuverter`] level in volts (its gain is
757 /// `level / 5`), so knob 1.0 = ±5 octaves of cutoff.
758 ///
759 /// Every destination this taper reaches is *normalized*, not volt-scaled,
760 /// which is what makes one curve serve all of them: `Svf.fm` sums straight
761 /// into a 0..1 cutoff CV whose full span is 20 Hz–20 kHz (~10 octaves),
762 /// `Wavefolder.threshold` lives in 0.1..1, and the palette's other slots —
763 /// `Wavetable.morph`, `KarplusStrong.damping`, `Distortion.drive`,
764 /// `Bitcrusher.bits`, `Chorus.depth`, `Reverb.size`, `Phaser.depth`,
765 /// `Flanger.depth`, `Tremolo.depth`, `Vibrato.depth`, `FormantOsc.vowel`,
766 /// `Granular.position` — are all 0..1 CVs on the same convention. A raw
767 /// ±5 V cable is ~5× full scale, so every knob position above ~0.2 only
768 /// clipped the modulator harder into a square wave — 97% of the travel did
769 /// nothing.
770 ///
771 /// The two destinations that are *not* normalized get their own tapers
772 /// ([`mod_depth_pitch`], [`mod_depth_gain`]) rather than borrowing this
773 /// one, because "half of full scale" means a different number of volts on
774 /// each of them.
775 ///
776 /// `DelayLine.time` is the one destination whose port is 0..1 but whose
777 /// *musical* range is exponential (1 ms · 2000^cv), so a given depth buys
778 /// far more motion there than anywhere else. That is the classic tape-wow
779 /// gesture rather than a defect, but it is the slot to look at first if a
780 /// dedicated taper is ever wanted.
781 ///
782 /// The taper is deliberately **linear, not square-law**. A square taper
783 /// gives a nicer knob feel, but this function is not only a knob mapping:
784 /// the grammar draws `mod_depth ~ U(0,1)` and the compiler applies the same
785 /// curve, so squaring also reshapes the *evolutionary prior* toward weak
786 /// modulation. Measured over eight seeds of the closed-loop synthetic-taste
787 /// gate, the square taper cost the posterior 0.11 of its correlation with
788 /// ground truth (0.59 vs 0.70) — the pool simply stopped moving enough for
789 /// timbral-movement preferences to be learnable. Linear costs nothing and
790 /// is still perfectly dialable once the 10× scale error is gone: the
791 /// musically useful first ±2 octaves occupy the bottom 40% of the sweep.
792 pub fn mod_depth_bipolar(x: f64) -> f64 {
793 mod_level_bipolar(PEAK_NORMALIZED, x)
794 }
795 /// Modulation depth for a 0–10 V source (the mod envelope, the follower).
796 /// Half the bipolar scale, so both source families reach the same depth at
797 /// the same knob position.
798 pub fn mod_depth_unipolar(x: f64) -> f64 {
799 mod_level_unipolar(PEAK_NORMALIZED, x)
800 }
801 /// Modulation depth for a ±5 V source landing on the **pitch**
802 /// [`Offset`]'s summing input.
803 ///
804 /// That input is V/Oct and the attenuverter's gain is `level / 5`, so a
805 /// ±5 V source arrives at `±level` **volts** — and on a V/Oct wire a volt
806 /// is an octave. The level is therefore numerically the octave depth, and
807 /// knob 1.0 gives **±0.5 octave** (±6 semitones).
808 ///
809 /// That ceiling is a deliberate compromise and reads as one on the knob: a
810 /// musical vibrato is ±50 cents, which sits at ~8% of the sweep — a small
811 /// corner to dial in. Capping tighter would make that corner usable and
812 /// put the other pitch-mod idiom, a mod envelope dropping a note in from
813 /// several semitones above, out of reach entirely. Six semitones covers
814 /// both; the vibrato end is fiddly and the alternative was not having it.
815 ///
816 /// Linear rather than square-law for the reason documented at length on
817 /// [`mod_depth_bipolar`] — the grammar draws `mod_depth ~ U(0,1)`, so the
818 /// curve here is an evolutionary prior and not only a knob feel.
819 ///
820 /// It currently evaluates to the same number as [`mod_depth_bipolar`],
821 /// which is a coincidence of two peaks both being 0.5 and not a shared
822 /// derivation: one is half of a normalized port's full scale, the other is
823 /// half an octave. They are two names so that retuning either cannot
824 /// silently move the other.
825 pub fn mod_depth_pitch(x: f64) -> f64 {
826 mod_level_bipolar(PEAK_PITCH, x)
827 }
828 /// [`mod_depth_pitch`] for a 0–10 V source: an envelope reaching the same
829 /// ±0.5 octave at the same knob position, as one-sided motion.
830 pub fn mod_depth_pitch_unipolar(x: f64) -> f64 {
831 mod_level_unipolar(PEAK_PITCH, x)
832 }
833 /// Modulation depth for a ±5 V source landing on a **±5 V gain** port
834 /// (the EQ's three bands).
835 ///
836 /// Ten times [`mod_depth_bipolar`], and it has to be: the destination is
837 /// volt-scaled, not normalized. quiver reads the band as `cv/5 · 12` dB,
838 /// so the normalized taper's ±0.5 V would be ±1.2 dB at *full* depth —
839 /// around the level JND, i.e. a mod slot that does nothing across its
840 /// whole travel. This reaches the port's own ±5 V, so knob 1.0 is a
841 /// ±12 dB pump on the mid band and the bottom of the sweep is where the
842 /// subtle settings live.
843 pub fn mod_depth_gain(x: f64) -> f64 {
844 mod_level_bipolar(PEAK_GAIN, x)
845 }
846 /// [`mod_depth_gain`] for a 0–10 V source.
847 pub fn mod_depth_gain_unipolar(x: f64) -> f64 {
848 mod_level_unipolar(PEAK_GAIN, x)
849 }
850 /// Modulation depth for a ±5 V source landing on the pitch shifter's
851 /// **semitone** port.
852 ///
853 /// Five times [`mod_depth_bipolar`], because the destination is
854 /// volt-scaled: quiver reads the port as `cv/5 · 24` semitones, so the
855 /// normalized taper's ±0.5 V would be ±2.4 semitones at *full* depth —
856 /// dialable, but it would put the module's whole reason for existing (a
857 /// modulated harmony line, a warble that crosses a semitone) in the top
858 /// fifth of the knob. This reaches [`SHIFT_PEAK_V`], the same half of the
859 /// port the knob owns, so full depth is ±12 semitones and the classic
860 /// slow detune-warble sits around 0.05 rather than under 0.01.
861 pub fn mod_depth_shift(x: f64) -> f64 {
862 mod_level_bipolar(SHIFT_PEAK_V, x)
863 }
864 /// [`mod_depth_shift`] for a 0–10 V source (a mod envelope sweeping a
865 /// note in from up to an octave away, one-sided).
866 pub fn mod_depth_shift_unipolar(x: f64) -> f64 {
867 mod_level_unipolar(SHIFT_PEAK_V, x)
868 }
869 /// Modulation depth for a ±5 V source landing on a `ModulatedParam`
870 /// knob+CV port (the ducker's `amount`).
871 ///
872 /// The parameter is normalized 0..1 like every `DepthScale::Normalized`
873 /// destination, but it is *reached* in volts on a ±5 V scale, so the same
874 /// musical depth costs ten times the attenuverter level. Full depth is
875 /// ±0.5 of the duck amount — half the parameter, matching
876 /// [`PEAK_NORMALIZED`]'s "half of full scale" everywhere else.
877 pub fn mod_depth_param_cv(x: f64) -> f64 {
878 mod_level_bipolar(PEAK_PARAM_CV, x)
879 }
880 /// [`mod_depth_param_cv`] for a 0–10 V source.
881 pub fn mod_depth_param_cv_unipolar(x: f64) -> f64 {
882 mod_level_unipolar(PEAK_PARAM_CV, x)
883 }
884 /// Modulation depth for a ±5 V source landing on a **dynamics threshold**
885 /// port (the compressor's and the gate's).
886 ///
887 /// The one destination where "half of full scale" is the wrong answer in
888 /// the *other* direction. The port spans 0..1 for 0..5 V, but the knob
889 /// reads it geometrically (see [`detector_volts`]) and the settings that
890 /// matter live between 0.05 V and 1 V — so the normalized taper's ±0.5 in
891 /// CV, i.e. ±2.5 V, would hold the threshold pinned at one rail or the
892 /// other for most of every cycle and the module would simply switch on and
893 /// off. [`PEAK_DETECTOR`] is ±0.5 V instead: around a typical setting that
894 /// is a full sweep from "always open" to a dozen dB above the key, and the
895 /// bottom of the knob buys the few-dB movement that reads as breathing.
896 pub fn mod_depth_detector(x: f64) -> f64 {
897 mod_level_bipolar(PEAK_DETECTOR, x)
898 }
899 /// [`mod_depth_detector`] for a 0–10 V source.
900 pub fn mod_depth_detector_unipolar(x: f64) -> f64 {
901 mod_level_unipolar(PEAK_DETECTOR, x)
902 }
903 /// Tempo for a [`quiver::modules::Clock`], which drives the euclidean
904 /// generator and the sample-and-hold op.
905 ///
906 /// The `bpm` port is `CvUnipolar`, and quiver's `voltage_range` for that
907 /// kind is **0–10 V, not 0–1**: `cv_to_bpm` is `20 · 15^(cv/10)`, so the
908 /// raw 0..1 knob would have spanned 20 BPM to 21.4 BPM — a rate control
909 /// with a 7% range, which is the class of defect this file has now found
910 /// four times. `10·x` spans the port's real 20–300 BPM, i.e. 0.33–5 Hz of
911 /// clock, which is the rhythmic band a five-second phrase can show.
912 pub fn clock_rate(x: f64) -> f64 {
913 x.clamp(0.0, 1.0) * CLOCK_CV_FULL_SCALE_V
914 }
915 /// Volts of `bpm` CV that reach the top of quiver's tempo map.
916 const CLOCK_CV_FULL_SCALE_V: f64 = 10.0;
917 /// Euclidean step count: quiver's `2 + (cv·14.99)` restricted to **4..16**
918 /// rather than 2..16.
919 ///
920 /// The two shortest patterns are dropped because they are what makes the
921 /// density knob below un-mappable, not because a two-step rhythm is
922 /// uninteresting: a pattern of two can hold either one pulse or two, and a
923 /// density floor low enough to keep a 1-of-16 rhythm reachable rounds to
924 /// *zero* pulses there. Four is the shortest count at which one CV floor
925 /// serves the whole range, and a 4-step pattern is still a bar of four.
926 pub fn euclid_steps(x: f64) -> f64 {
927 0.14 + 0.86 * x.clamp(0.0, 1.0)
928 }
929 /// Euclidean pulse density, bounded off both degenerate ends **at every
930 /// step count**.
931 ///
932 /// quiver takes `pulses = (cv · steps) as usize`, so a raw knob has two
933 /// dead corners the grammar would otherwise draw: below `1/steps` the
934 /// pattern has no pulses at all and the cable carries a constant 0 V, and
935 /// at exactly 1.0 every step fires and it carries a constant 5 V. The
936 /// first is not a rounding corner — with steps uniform over the range it
937 /// is about one draw in seven.
938 ///
939 /// The floor is `1/4`, the reciprocal of [`euclid_steps`]'s coarsest
940 /// count, so a pulse survives however few steps there are; the ceiling
941 /// leaves at least one rest for the same reason at the other end. Between
942 /// them every setting is a rhythm at every step count: 1..3 of four,
943 /// 4..15 of sixteen.
944 pub fn euclid_pulses(x: f64) -> f64 {
945 0.25 + 0.74 * x.clamp(0.0, 1.0)
946 }
947 /// Slew time for a [`quiver::modules::SlewLimiter`] `rise`/`fall` port.
948 ///
949 /// quiver's own map is `0.001 + cv²·10` seconds, which is already
950 /// square-law — so the musically useful glide times (10 ms to ~1.5 s) all
951 /// live below cv 0.39 and a raw knob would spend three fifths of its
952 /// travel freezing the modulator solid. `0.4·x` puts the whole range on
953 /// the plate: full travel is a 1.6 s glide and a uniform draw averages
954 /// ≈0.4 s, which reads as portamento rather than as a mute.
955 ///
956 /// [`crate::term::ModNode::Rand`]'s own `glide` knob keeps the raw map it
957 /// shipped with — it is a saved-patch parameter, and re-tapering it would
958 /// change how every existing S&H sounds.
959 pub fn slew_time(x: f64) -> f64 {
960 SLEW_MAX_CV * x.clamp(0.0, 1.0)
961 }
962 /// Top of the slew knob, on quiver's `0.001 + cv²·10` s map — 1.6 s.
963 const SLEW_MAX_CV: f64 = 0.4;
964 /// Detune: ±50 cents expressed in V/Oct.
965 pub fn detune_voct(x: f64) -> f64 {
966 (x * 2.0 - 1.0) * (50.0 / 1200.0)
967 }
968 /// Crossfader position: 0..1 → −5..+5 V.
969 pub fn xfade_pos(x: f64) -> f64 {
970 (x * 2.0 - 1.0) * 5.0
971 }
972}
973
974/// What a subtree hands back: mono (`right == None`) or a true stereo pair.
975///
976/// Only [`Reverb`] and [`Chorus`] widen — everything else is a mono processor,
977/// and feeding one a stereo signal downmixes (see [`Compiler::feed`]). Carrying
978/// the pair instead of dropping it is the whole point of having a chorus.
979#[derive(Clone, Copy)]
980struct Sig {
981 left: PortRef,
982 right: Option<PortRef>,
983}
984
985impl Sig {
986 fn mono(port: PortRef) -> Self {
987 Sig {
988 left: port,
989 right: None,
990 }
991 }
992 fn stereo(left: PortRef, right: PortRef) -> Self {
993 Sig {
994 left,
995 right: Some(right),
996 }
997 }
998}
999
1000struct Compiler {
1001 patch: Patch,
1002 pitch_out: PortRef,
1003 gate_out: PortRef,
1004 params: HashMap<String, ParamHandle>,
1005 /// Where each term node's audio leaves it — see [`CompiledVoice::taps`].
1006 /// Recorded by [`Self::build`] as the tree is walked, because that is the
1007 /// only point at which the association between a trace key and the quiver
1008 /// node that *ends* its chain exists: a term node compiles to anywhere
1009 /// between one and half a dozen quiver nodes, and which of them carries
1010 /// the audio out is a fact about the arm that built it.
1011 taps: Vec<(String, PortRef)>,
1012}
1013
1014impl Compiler {
1015 /// Pin the control input `port` on `node` to a constant `value`. Used only
1016 /// for fixed wiring decisions; user knobs go through [`Self::knob`].
1017 ///
1018 /// Implemented as [`Patch::set_param_by_id`]: the override is baked into
1019 /// the port's *default* when quiver builds its routing, so pinning costs
1020 /// no node and no per-sample work. (An earlier version cabled a pooled
1021 /// [`Offset`] node into each site, on the belief that a port default could
1022 /// not be overridden from outside the module; `set_param_by_id` does
1023 /// exactly that, and quiver's gather writes the identical constant either
1024 /// way, so this is bit-exact and strictly cheaper.)
1025 ///
1026 /// The one thing a baked default cannot do is coexist with a cable: gather
1027 /// sums the cables into a patched port and **ignores** its default. So any
1028 /// port that also receives a knob or modulation cable must be pinned by a
1029 /// real cable instead (the wavefolder threshold sums `#thresh` plus its
1030 /// mod source on one port), and [`Self::wire_pitch`] keeps a real
1031 /// [`Offset`] node because it *sums with* the incoming pitch CV rather
1032 /// than replacing an unpatched default.
1033 fn constant(&mut self, value: f64, node: NodeId, port: &str) -> Result<(), PatchError> {
1034 if self.patch.set_param_by_id(node, port, value) {
1035 Ok(())
1036 } else {
1037 Err(PatchError::InvalidPort {
1038 node,
1039 name: Some(port.to_string()),
1040 port: None,
1041 available: Vec::new(),
1042 })
1043 }
1044 }
1045
1046 /// Add a **live** knob: an [`ExternalInput`] whose atomic value the
1047 /// audio thread reads every sample, registered under the knob's trace
1048 /// address. Turning the knob writes the atomic — the sound changes
1049 /// immediately, and all filter/delay state survives.
1050 fn knob(
1051 &mut self,
1052 key: &str,
1053 site: &str,
1054 raw: f64,
1055 pmap: ParamMap,
1056 bipolar: bool,
1057 target: PortRef,
1058 ) -> Result<(), PatchError> {
1059 self.knob_to(key, site, raw, pmap, bipolar, &[target])
1060 }
1061
1062 /// [`Self::knob`] with the same atomic cabled to several ports.
1063 ///
1064 /// One trace site must stay one knob: the S&H glide drives a
1065 /// [`SlewLimiter`]'s `rise` *and* `fall`, and adding a second
1066 /// [`Self::knob`] for the second port would register a second node under
1067 /// the same name and silently overwrite the first's [`ParamHandle`] — the
1068 /// handle the panel then drags would move only half the module.
1069 fn knob_to(
1070 &mut self,
1071 key: &str,
1072 site: &str,
1073 raw: f64,
1074 pmap: ParamMap,
1075 bipolar: bool,
1076 targets: &[PortRef],
1077 ) -> Result<(), PatchError> {
1078 let value = Arc::new(AtomicF64::new(pmap.apply(raw)));
1079 let input = if bipolar {
1080 ExternalInput::cv_bipolar(Arc::clone(&value))
1081 } else {
1082 ExternalInput::cv(Arc::clone(&value))
1083 };
1084 let n = self.patch.add(format!("{key}:{site}!"), input);
1085 for target in targets {
1086 self.patch.connect(n.out("out"), *target)?;
1087 }
1088 self.params
1089 .insert(format!("{key}#{site}"), ParamHandle { value, map: pmap });
1090 Ok(())
1091 }
1092
1093 /// Wire a modulation term into `target`. `ModNode::None` wires nothing.
1094 ///
1095 /// `owner` is the *modulated* module — that is where `describe.rs`
1096 /// advertises the `#mdepth` knob, and the mod source's own nodes and knobs
1097 /// hang off `<owner>/m` (`node/m:lfo`, `node/m#rate`). The slot key is
1098 /// derived here rather than passed in: it is `<owner>/m` at every call
1099 /// site, and a hand-built key that disagrees with `owner` would put the
1100 /// depth knob on one module and the LFO's knobs under another.
1101 ///
1102 /// The depth is an [`Attenuverter`] driven by a real [`Self::knob`], not a
1103 /// baked-in cable attenuation. Turning it used to require a full
1104 /// recompile — a 6 ms fade-out, per-quantum voice rebuild and fade-in for
1105 /// the length of the drag, while every neighbouring knob swept
1106 /// continuously.
1107 ///
1108 /// `owner_input` is the signal the owning module is *about to process*,
1109 /// tapped before it enters. Only [`ModNode::Follow`] reads it, and it is
1110 /// `None` exactly where there is nothing to tap — a source's own mod slot
1111 /// (a wavetable or a pluck generates its input rather than receiving
1112 /// one). That case wires no modulation at all rather than failing: the
1113 /// grammar and the panel can both express "follower on an oscillator",
1114 /// and the honest compilation of it is silence on that cable, not a
1115 /// refusal to compile a term the prior can draw.
1116 ///
1117 /// `scale` says what kind of port `target` is. The source's polarity is
1118 /// decided here, but "how many volts is full depth" is a property of the
1119 /// destination, and the two together pick the taper — see [`DepthScale`].
1120 fn wire_mod(
1121 &mut self,
1122 m: &ModNode,
1123 owner: &str,
1124 depth: f64,
1125 target: PortRef,
1126 owner_input: Option<Sig>,
1127 scale: DepthScale,
1128 ) -> Result<(), PatchError> {
1129 let key = &format!("{owner}/m");
1130 let Some((src, unipolar)) = self.build_mod(m, key, owner_input)? else {
1131 return Ok(());
1132 };
1133 let att = self.patch.add(format!("{key}:depth"), Attenuverter::new());
1134 self.patch.connect(src, att.in_("in"))?;
1135 self.knob(
1136 owner,
1137 "mdepth",
1138 depth,
1139 scale.taper(unipolar),
1140 true,
1141 att.in_("level"),
1142 )?;
1143 self.patch.connect(att.out("out"), target)?;
1144 Ok(())
1145 }
1146
1147 /// Build a modulation term and return `(its output port, whether it swings
1148 /// 0–10 V rather than ±5 V)`, or `None` for a term that produces nothing.
1149 ///
1150 /// This is the recursion [`ModNode`] gained when modulation became a sort:
1151 /// [`ModNode::Op`] builds its subterm first and processes it,
1152 /// [`ModNode::Pair`] builds two. Subterm keys follow the audio tree's
1153 /// convention — `<key>/0` and `<key>/1` — which never collides with an
1154 /// audio node's, because every modulation key sits under a `/m`.
1155 ///
1156 /// # The polarity flag is a *scale* claim, not a sign claim
1157 ///
1158 /// It selects between [`map::mod_level_bipolar`]'s "a ±5 V source arrives
1159 /// at ±level" and [`map::mod_level_unipolar`]'s halving for an 0–10 V one,
1160 /// so what it really asks is **"can this term reach 10 V?"**. A gate
1161 /// reaches 5, so `Euclid` and the logic ops answer *no* even though they
1162 /// never go negative — answering yes would halve their depth for nothing.
1163 /// The shapers pass their subterm's answer through, because none of them
1164 /// changes the magnitude scale: rectifying ±5 V gives 0–5 V, which is
1165 /// still a signal whose extreme is 5 V.
1166 fn build_mod(
1167 &mut self,
1168 m: &ModNode,
1169 key: &str,
1170 owner_input: Option<Sig>,
1171 ) -> Result<Option<(PortRef, bool)>, PatchError> {
1172 let built = match m {
1173 ModNode::None => return Ok(None),
1174 ModNode::Lfo { wave, rate, .. } => {
1175 let lfo = self.patch.add(format!("{key}:lfo"), Lfo::new(self.sr()));
1176 self.knob(key, "rate", *rate, ParamMap::Unit, false, lfo.in_("rate"))?;
1177 (lfo.out(wave.port_name()), false)
1178 }
1179 ModNode::Rand { rate, glide, .. } => {
1180 // S&H burble: white noise sampled on an internal square-LFO
1181 // clock. The knob drives the clock rate.
1182 let clk = self.patch.add(format!("{key}:rclk"), Lfo::new(self.sr()));
1183 self.knob(key, "rate", *rate, ParamMap::Unit, false, clk.in_("rate"))?;
1184 let noise = self
1185 .patch
1186 .add(format!("{key}:rnoise"), NoiseGenerator::new());
1187 let snh = self.patch.add(format!("{key}:snh"), SampleAndHold::new());
1188 self.patch.connect(noise.out("white"), snh.in_("in"))?;
1189 self.patch.connect(clk.out("sqr"), snh.in_("trig"))?;
1190 // Glide turns the same source into two different modulators:
1191 // at 0 it is the stepped burble, and as it opens the steps
1192 // become a smooth random walk — the classic sample-and-glide.
1193 // Symmetric (one knob into both `rise` and `fall`) because an
1194 // asymmetric slew on a random signal reads as a *shape*, not
1195 // as glide, and that is a second timbral choice this module
1196 // has no faceplate room to offer.
1197 let slew = self
1198 .patch
1199 .add(format!("{key}:glide"), SlewLimiter::new(self.sr()));
1200 self.patch.connect(snh.out("out"), slew.in_("in"))?;
1201 self.knob_to(
1202 key,
1203 "glide",
1204 *glide,
1205 ParamMap::Unit,
1206 false,
1207 &[slew.in_("rise"), slew.in_("fall")],
1208 )?;
1209 (slew.out("out"), false)
1210 }
1211 ModNode::Follow { sens, release, .. } => {
1212 // The tap is the owning module's *own* input, taken before it
1213 // enters — so the follower measures what the module is about
1214 // to process rather than what it produced, which would be a
1215 // feedback loop through the parameter it drives.
1216 let Some(input) = owner_input else {
1217 return Ok(None);
1218 };
1219 let f = self
1220 .patch
1221 .add(format!("{key}:follow"), EnvelopeFollower::new(self.sr()));
1222 self.feed(input, f.in_("in"))?;
1223 self.knob(key, "sens", *sens, ParamMap::Unit, false, f.in_("gain"))?;
1224 self.knob(
1225 key,
1226 "rel",
1227 *release,
1228 ParamMap::Unit,
1229 false,
1230 f.in_("release"),
1231 )?;
1232 self.constant(FOLLOW_ATTACK, f.id(), "attack")?;
1233 // 0–10 V detector output, so it shares the mod envelope's
1234 // taper rather than the bipolar one.
1235 (f.out("out"), true)
1236 }
1237 ModNode::Env { attack, decay, .. } => {
1238 let env = self.patch.add(format!("{key}:env"), Adsr::new(self.sr()));
1239 self.patch.connect(self.gate_out, env.in_("gate"))?;
1240 self.knob(
1241 key,
1242 "att",
1243 *attack,
1244 ParamMap::Unit,
1245 false,
1246 env.in_("attack"),
1247 )?;
1248 self.knob(key, "dec", *decay, ParamMap::Unit, false, env.in_("decay"))?;
1249 // AD shape: no sustain plateau, quick release.
1250 self.constant(0.0, env.id(), "sustain")?;
1251 self.constant(0.1, env.id(), "release")?;
1252 // Exponential contour, as on the amp envelope — a linear filter
1253 // sweep reads as a fader move, not as a decay.
1254 self.constant(GATE_TRUE, env.id(), "shape")?;
1255 (env.out("env"), true)
1256 }
1257 ModNode::Euclid {
1258 rate,
1259 steps,
1260 pulses,
1261 ..
1262 } => {
1263 let clk = self.patch.add(format!("{key}:eclk"), Clock::new(self.sr()));
1264 self.knob(
1265 key,
1266 "erate",
1267 *rate,
1268 ParamMap::ClockRate,
1269 false,
1270 clk.in_("bpm"),
1271 )?;
1272 let eu = self
1273 .patch
1274 .add(format!("{key}:euclid"), Euclidean::new(self.sr()));
1275 self.patch.connect(clk.out("out"), eu.in_("clock"))?;
1276 self.knob(
1277 key,
1278 "esteps",
1279 *steps,
1280 ParamMap::EuclidSteps,
1281 false,
1282 eu.in_("steps"),
1283 )?;
1284 self.knob(
1285 key,
1286 "epulses",
1287 *pulses,
1288 ParamMap::EuclidPulses,
1289 false,
1290 eu.in_("pulses"),
1291 )?;
1292 self.constant(EUCLID_ROTATION, eu.id(), "rotation")?;
1293 // `reset` stays unpatched: quiver's gather writes the port's
1294 // own 0 V default, and the pattern is already re-armed by its
1295 // own step counter wrapping. A reset cable would need a
1296 // per-note trigger, and a euclidean pattern that restarts on
1297 // every note is a fixed rhythm rather than a running one.
1298 //
1299 // The sample-and-hold is what turns the pattern into a
1300 // **gate**. quiver's `Euclidean` emits a `Trigger`, and its
1301 // implementation takes that literally: `out` is `GATE_HIGH_V`
1302 // on the single sample the clock's edge lands on and 0 V for
1303 // every other sample of the step. One sample in two thousand
1304 // is inaudible on any destination in this grammar — it is a
1305 // modulator that measures as a dead cable — so the pattern is
1306 // latched on the same clock that produced it. Holding it
1307 // stretches each hit across its whole step, which makes the
1308 // duty cycle `pulses/steps` and the output a real rhythm.
1309 //
1310 // Both modules are edge-triggered from the same clock and the
1311 // hold reads the generator, so quiver's topological order
1312 // evaluates the pattern first and the latch sees the fresh
1313 // step rather than the previous one.
1314 let gate = self.patch.add(format!("{key}:egate"), SampleAndHold::new());
1315 self.patch.connect(eu.out("out"), gate.in_("in"))?;
1316 self.patch.connect(clk.out("out"), gate.in_("trig"))?;
1317 (gate.out("out"), false)
1318 }
1319 ModNode::Op {
1320 kind,
1321 p0,
1322 p1,
1323 input,
1324 ..
1325 } => {
1326 let Some((src, unipolar)) =
1327 self.build_mod(input, &format!("{key}/0"), owner_input)?
1328 else {
1329 return Ok(None);
1330 };
1331 self.build_mod_op(*kind, *p0, *p1, key, src, unipolar)?
1332 }
1333 ModNode::Pair { kind, a, b, .. } => {
1334 let (a, b) = (
1335 self.build_mod(a, &format!("{key}/0"), owner_input)?,
1336 self.build_mod(b, &format!("{key}/1"), owner_input)?,
1337 );
1338 // A branch that produced nothing collapses to the other one
1339 // rather than to a constant, matching
1340 // [`ModNode::normalized`]. In practice only a `Follow` on a
1341 // source can get here — every other empty branch was already
1342 // folded away — and the honest compilation of "follow an
1343 // oscillator" is the rest of the term, not silence.
1344 match (a, b) {
1345 (None, None) => return Ok(None),
1346 (Some(x), None) | (None, Some(x)) => x,
1347 (Some(a), Some(b)) => self.build_mod_pair(*kind, key, a, b)?,
1348 }
1349 }
1350 };
1351 Ok(Some(built))
1352 }
1353
1354 /// One [`ModOp`] over an already-built modulation signal.
1355 fn build_mod_op(
1356 &mut self,
1357 kind: ModOp,
1358 p0: f64,
1359 p1: f64,
1360 key: &str,
1361 src: PortRef,
1362 unipolar: bool,
1363 ) -> Result<(PortRef, bool), PatchError> {
1364 Ok(match kind {
1365 ModOp::Quantize => {
1366 // Scale into the quantizer's grid and back out again — see
1367 // [`QUANTIZE_IN_LEVEL`], which is where the whole musical
1368 // argument for this module lives.
1369 let a_in = self.patch.add(format!("{key}:qin"), Attenuverter::new());
1370 self.patch.connect(src, a_in.in_("in"))?;
1371 self.constant(QUANTIZE_IN_LEVEL, a_in.id(), "level")?;
1372 let q = self
1373 .patch
1374 .add(format!("{key}:quant"), ScaleQuantizer::new(self.sr()));
1375 self.patch.connect(a_in.out("out"), q.in_("in"))?;
1376 self.knob(key, "qroot", p0, ParamMap::Unit, false, q.in_("root"))?;
1377 // Straight through: quiver's own `(cv·6.99) as u8` is the
1378 // seven-way selector, so the knob *is* the categorical and
1379 // `crate::term::quant_scale_index` reads it the same way.
1380 self.knob(key, "qscale", p1, ParamMap::Unit, false, q.in_("scale"))?;
1381 let a_out = self.patch.add(format!("{key}:qout"), Attenuverter::new());
1382 self.patch.connect(q.out("out"), a_out.in_("in"))?;
1383 self.constant(QUANTIZE_OUT_LEVEL, a_out.id(), "level")?;
1384 (a_out.out("out"), unipolar)
1385 }
1386 ModOp::Slew => {
1387 let s = self
1388 .patch
1389 .add(format!("{key}:slew"), SlewLimiter::new(self.sr()));
1390 self.patch.connect(src, s.in_("in"))?;
1391 self.knob(key, "rise", p0, ParamMap::SlewTime, false, s.in_("rise"))?;
1392 self.knob(key, "fall", p1, ParamMap::SlewTime, false, s.in_("fall"))?;
1393 (s.out("out"), unipolar)
1394 }
1395 ModOp::Rectify => {
1396 let r = self.patch.add(format!("{key}:rect"), Rectifier::new());
1397 self.patch.connect(src, r.in_("in"))?;
1398 // `mode` is a choice of output *port*, not a CV: quiver's
1399 // `Rectifier` publishes all three at once and has no mode
1400 // input at all. The knob therefore picks a cable, and the
1401 // register of what it picked lives in the plate label.
1402 //
1403 // On a source that never goes negative — a mod envelope, a
1404 // follower, a euclidean gate — `full` and `positive` are both
1405 // the identity and `negative` is silence. That is a real dead
1406 // corner and it is left visible rather than special-cased:
1407 // rectification is a statement about a *bipolar* signal, and
1408 // hiding the fact that it says nothing about a unipolar one
1409 // would make the knob lie in the other direction.
1410 //
1411 // No [`Self::knob`] and so no [`ParamHandle`]: like every
1412 // other enum site in this grammar, turning it is a structural
1413 // change and the host has to recompile (`live::set_param`
1414 // returns false and the panel calls `set_patch`). Registering
1415 // a handle nothing reads would be a knob that drags smoothly
1416 // and never changes the sound.
1417 let port = match rect_mode_index(p0) {
1418 0 => "full",
1419 1 => "half_pos",
1420 _ => "half_neg",
1421 };
1422 (r.out(port), unipolar)
1423 }
1424 ModOp::Hold => {
1425 let clk = self.patch.add(format!("{key}:hclk"), Clock::new(self.sr()));
1426 self.knob(key, "hrate", p0, ParamMap::ClockRate, false, clk.in_("bpm"))?;
1427 let snh = self.patch.add(format!("{key}:hold"), SampleAndHold::new());
1428 self.patch.connect(src, snh.in_("in"))?;
1429 self.patch.connect(clk.out("out"), snh.in_("trig"))?;
1430 (snh.out("out"), unipolar)
1431 }
1432 })
1433 }
1434
1435 /// One [`PairOp`] over two already-built modulation signals.
1436 fn build_mod_pair(
1437 &mut self,
1438 kind: PairOp,
1439 key: &str,
1440 a: (PortRef, bool),
1441 b: (PortRef, bool),
1442 ) -> Result<(PortRef, bool), PatchError> {
1443 // Min, max and the switch hand back one of their inputs, so the pair
1444 // can reach 10 V if either branch can. The logic gates emit a 5 V gate
1445 // whatever they were fed.
1446 let unipolar = a.1 || b.1;
1447 Ok(match kind {
1448 PairOp::Min => {
1449 let n = self.patch.add(format!("{key}:min"), Min::new());
1450 self.patch.connect(a.0, n.in_("a"))?;
1451 self.patch.connect(b.0, n.in_("b"))?;
1452 (n.out("out"), unipolar)
1453 }
1454 PairOp::Max => {
1455 let n = self.patch.add(format!("{key}:max"), Max::new());
1456 self.patch.connect(a.0, n.in_("a"))?;
1457 self.patch.connect(b.0, n.in_("b"))?;
1458 (n.out("out"), unipolar)
1459 }
1460 PairOp::And => {
1461 let n = self.patch.add(format!("{key}:and"), LogicAnd::new());
1462 self.patch.connect(a.0, n.in_("a"))?;
1463 self.patch.connect(b.0, n.in_("b"))?;
1464 (n.out("out"), false)
1465 }
1466 PairOp::Or => {
1467 let n = self.patch.add(format!("{key}:or"), LogicOr::new());
1468 self.patch.connect(a.0, n.in_("a"))?;
1469 self.patch.connect(b.0, n.in_("b"))?;
1470 (n.out("out"), false)
1471 }
1472 PairOp::Xor => {
1473 let n = self.patch.add(format!("{key}:xor"), LogicXor::new());
1474 self.patch.connect(a.0, n.in_("a"))?;
1475 self.patch.connect(b.0, n.in_("b"))?;
1476 (n.out("out"), false)
1477 }
1478 PairOp::Switch => {
1479 // quiver's `VcSwitch` needs a *third* input to choose with,
1480 // and `Pair` has only two branches to offer. The contract
1481 // proposed the voice gate; that is wrong, and measurably so:
1482 // the gate is high for the whole of every note and low only
1483 // between notes, when the VCA is shut — so `b` would be
1484 // selected for every sample anybody hears and the `a` branch
1485 // would be a module on the rack that is never once audible.
1486 //
1487 // `b` is its own control instead: the switch passes `b` while
1488 // `b` is above the 2.5 V gate threshold and `a` the rest of
1489 // the time. That makes `Switch(pad, euclid)` "punch this
1490 // rhythm in over that modulator", which is what the module is
1491 // for, and every branch is heard.
1492 let n = self.patch.add(format!("{key}:sw"), VcSwitch::new());
1493 self.patch.connect(a.0, n.in_("a"))?;
1494 self.patch.connect(b.0, n.in_("b"))?;
1495 self.patch.connect(b.0, n.in_("cv"))?;
1496 (n.out("out"), unipolar)
1497 }
1498 })
1499 }
1500
1501 /// Feed a subtree's output into a mono input. A stereo pair is summed at
1502 /// −6 dB per side (two cables into one input sum in quiver's gather), which
1503 /// is the level-preserving downmix for the correlated dry path.
1504 fn feed(&mut self, sig: Sig, target: PortRef) -> Result<(), PatchError> {
1505 match sig.right {
1506 None => {
1507 self.patch.connect(sig.left, target)?;
1508 }
1509 Some(r) => {
1510 self.patch.connect_attenuated(sig.left, target, 0.5)?;
1511 self.patch.connect_attenuated(r, target, 0.5)?;
1512 }
1513 }
1514 Ok(())
1515 }
1516
1517 /// Block DC ahead of the VCA, using an [`Svf`] highpass parked at its
1518 /// lowest corner.
1519 ///
1520 /// `DiodeLadderFilter::diode_sat` is asymmetric by design (`tanh(1.2x)` up,
1521 /// `tanh(0.8x)` down) and audio reaches it at nominal ±5 V, so it emits real
1522 /// DC. Blocking downstream of the VCA would be too late: the amp envelope
1523 /// has already multiplied that offset into a per-note thump whose spectrum
1524 /// reaches far above the offset itself. Removing it first means the thump is
1525 /// never created.
1526 ///
1527 /// An `x[n] − x[n−1] + R·y[n−1]` one-pole at 5 Hz, assembled from a `Mixer`
1528 /// and two `UnitDelay`s, is the textbook answer and measures correctly — but
1529 /// its state is not sanitized, so after a note ends it rings on as a smooth
1530 /// sub-audio decay that never reaches zero. That residue is inaudible and
1531 /// harmless to play, and *ruinous* to the feature extractor: spectral
1532 /// flatness is a geometric mean, and a tail of near-DC frames drags the
1533 /// phrase mean down by two orders of magnitude, which would silently
1534 /// corrupt every preference observation. `Svf` flushes its state, so its
1535 /// tail lands on exact zero.
1536 fn dc_blocker(&mut self, name: &str, input: PortRef) -> Result<PortRef, PatchError> {
1537 let f = self.patch.add(format!("{name}:hp"), Svf::new(self.sr()));
1538 self.patch.connect(input, f.in_("in"))?;
1539 self.constant(DC_BLOCK_CUTOFF, f.id(), "cutoff")?;
1540 self.constant(0.0, f.id(), "res")?;
1541 Ok(f.out("hp"))
1542 }
1543
1544 /// Build one channel of the mandatory voice tail:
1545 /// `input → DC blocker → VCA → Limiter`. Called twice when the tree ends
1546 /// in a stereo module, sharing the one amp envelope.
1547 fn voice_tail(
1548 &mut self,
1549 side: &str,
1550 input: PortRef,
1551 env: PortRef,
1552 block_dc: bool,
1553 ) -> Result<PortRef, PatchError> {
1554 let blocked = if block_dc {
1555 self.dc_blocker(&format!("voice:dc{side}"), input)?
1556 } else {
1557 input
1558 };
1559
1560 let vca = self.patch.add(format!("voice:vca{side}"), Vca::new());
1561 self.patch.connect(blocked, vca.in_("in"))?;
1562 self.patch.connect(env, vca.in_("cv"))?;
1563 // Exponential response. A linear VCA fed a linear envelope loses its
1564 // last 20 dB in the final instant of the decay, which is why the
1565 // instrument read as "wrong" before anyone could name why.
1566 self.constant(GATE_TRUE, vca.id(), "response")?;
1567
1568 let limiter = self
1569 .patch
1570 .add(format!("voice:limiter{side}"), Limiter::new(self.sr()));
1571 self.patch.connect(vca.out("out"), limiter.in_("in"))?;
1572 // A safety net, not a tone stage. Three things had to change together:
1573 // the threshold is 1.0 (= 5 V, the top of nominal quiver audio) rather
1574 // than the 0.8 default, which put every patch permanently inside the
1575 // knee; `soft` is off, so there is no continuous tanh shaping with a
1576 // release that pumps against the amp envelope; and the sidechain is
1577 // patched from the same signal, because quiver's gather writes the
1578 // port default (0 V) to an unpatched input, so the detector was
1579 // reading silence and the stage was in fact a bare hard clipper at
1580 // 4 V. Real limiting lives on the master bus, across the voice sum.
1581 self.constant(1.0, limiter.id(), "threshold")?;
1582 self.constant(GATE_FALSE, limiter.id(), "soft")?;
1583 self.patch
1584 .connect(vca.out("out"), limiter.in_("sidechain"))?;
1585 Ok(limiter.out("out"))
1586 }
1587
1588 /// Route pitch (plus a per-source V/Oct offset) into a `voct` input.
1589 ///
1590 /// Returns the [`Offset`]'s own `in` port — the grammar's one **pitch
1591 /// modulation** site. quiver's gather sums every cable into a patched
1592 /// input, so a second cable here adds to the incoming keyboard CV rather
1593 /// than replacing it, which is precisely why this stage is a real node and
1594 /// not a baked default (see [`Self::constant`]). A volt on that wire is an
1595 /// octave, so what arrives is transposition: vibrato, or a pitch envelope.
1596 ///
1597 /// A third cable lands here too, and it is the reason `oct` is a live
1598 /// site: an [`ExternalInput`] carrying an **octave trim**, zero at compile
1599 /// time, that the panel writes when the octave chip is cycled. Changing
1600 /// the octave used to be a recompile — a fade-out, a per-quantum voice
1601 /// rebuild and a re-attack of every held note, for a number that is one
1602 /// addition on a CV wire. See [`ParamMap::OctaveTrim`] for why the live
1603 /// value is a *trim* rather than the octave itself; the short version is
1604 /// that a trim of `+0.0` is the additive identity in the gather sum and an
1605 /// absolute octave is not, so this costs a node and no samples.
1606 fn wire_pitch(
1607 &mut self,
1608 key: &str,
1609 octave: i8,
1610 detune: f64,
1611 target: PortRef,
1612 ) -> Result<PortRef, PatchError> {
1613 let offset = octave as f64 + map::detune_voct(detune);
1614 let node = self.patch.add(format!("{key}:pitch"), Offset::new(offset));
1615 self.patch.connect(self.pitch_out, node.in_("in"))?;
1616 self.patch.connect(node.out("out"), target)?;
1617 self.knob(
1618 key,
1619 "oct",
1620 (octave + 2) as f64,
1621 ParamMap::OctaveTrim(octave),
1622 // The trim is signed, and the port is `CvBipolar`; a unipolar
1623 // `ExternalInput` here would be a validation warning on every
1624 // source in every patch.
1625 true,
1626 node.in_("in"),
1627 )?;
1628 Ok(node.in_("in"))
1629 }
1630
1631 fn sr(&self) -> f64 {
1632 self.patch.sample_rate()
1633 }
1634
1635 /// Build the audio subtree rooted at `node`; returns its output signal.
1636 /// Build `node` and record where its audio came out.
1637 ///
1638 /// A wrapper rather than a line in each of the thirty-odd arms of
1639 /// [`Self::build_node`]: the tap is the same fact for every kind — the
1640 /// `Sig` the arm returned — and writing it once means a new production
1641 /// cannot forget to. The left channel is the tap for a stereo `Sig`;
1642 /// metering both would report a width, not a level, and the flow
1643 /// animation asks how much signal is on the wire.
1644 fn build(&mut self, node: &AudioNode, key: &str) -> Result<Sig, PatchError> {
1645 let sig = self.build_node(node, key)?;
1646 self.taps.push((key.to_string(), sig.left));
1647 Ok(sig)
1648 }
1649
1650 fn build_node(&mut self, node: &AudioNode, key: &str) -> Result<Sig, PatchError> {
1651 match node {
1652 AudioNode::Vco {
1653 wave,
1654 octave,
1655 detune,
1656 mod_depth,
1657 modulation,
1658 ..
1659 } => {
1660 let vco = self.patch.add(format!("{key}:vco"), Vco::new(self.sr()));
1661 let pitch_in = self.wire_pitch(key, *octave, *detune, vco.in_("voct"))?;
1662 // The mod cable joins the keyboard CV at the pitch offset, not
1663 // at the oscillator: everything downstream of the summing node
1664 // sees one V/Oct signal, so vibrato and transposition are the
1665 // same mechanism and cannot disagree.
1666 self.wire_mod(
1667 modulation,
1668 key,
1669 *mod_depth,
1670 pitch_in,
1671 // A source generates its own input, so there is nothing
1672 // for a follower to tap — as on the wavetable and pluck.
1673 None,
1674 DepthScale::Pitch,
1675 )?;
1676 Ok(Sig::mono(vco.out(wave.port_name())))
1677 }
1678 AudioNode::Supersaw {
1679 octave,
1680 detune,
1681 mix,
1682 mod_depth,
1683 modulation,
1684 ..
1685 } => {
1686 let saw = self
1687 .patch
1688 .add(format!("{key}:supersaw"), Supersaw::new(self.sr()));
1689 let pitch_in = self.wire_pitch(key, *octave, 0.5, saw.in_("voct"))?;
1690 self.wire_mod(
1691 modulation,
1692 key,
1693 *mod_depth,
1694 pitch_in,
1695 None,
1696 DepthScale::Pitch,
1697 )?;
1698 self.knob(
1699 key,
1700 "det",
1701 *detune,
1702 ParamMap::Unit,
1703 false,
1704 saw.in_("detune"),
1705 )?;
1706 self.knob(key, "smix", *mix, ParamMap::Unit, false, saw.in_("mix"))?;
1707 Ok(Sig::mono(saw.out("out")))
1708 }
1709 AudioNode::Noise { color, .. } => {
1710 let noise = self
1711 .patch
1712 .add(format!("{key}:noise"), NoiseGenerator::new());
1713 Ok(Sig::mono(noise.out(color.port_name())))
1714 }
1715 // A `Vca` with nothing patched into its audio input, which is a
1716 // constant zero: quiver reads an unpatched port as 0.0, and the
1717 // gain that multiplies it cannot make it anything else.
1718 //
1719 // Chosen over the obvious `Offset::new(0.0)` because `Offset`'s
1720 // ports are `CvBipolar`, so feeding one to an audio consumer would
1721 // raise an Audio/CV signal-kind warning on every patch holding a
1722 // hole. `Vca` is `Audio` in and `Audio` out, so this production
1723 // adds no cable, no warning, and no new class to the allowlist in
1724 // `every_prior_sample_compiles`.
1725 AudioNode::Silence { .. } => {
1726 let z = self.patch.add(format!("{key}:silence"), Vca::new());
1727 Ok(Sig::mono(z.out("out")))
1728 }
1729 AudioNode::Wavetable {
1730 table,
1731 octave,
1732 morph,
1733 mod_depth,
1734 modulation,
1735 ..
1736 } => {
1737 let wt = self
1738 .patch
1739 .add(format!("{key}:wavetable"), Wavetable::new(self.sr()));
1740 // Detune 0.5 is the no-offset centre of `map::detune_voct`:
1741 // this module has no detune site, but pitch still goes
1742 // through the same Offset every other source uses.
1743 self.wire_pitch(key, *octave, 0.5, wt.in_("v_oct"))?;
1744 // The table is an enum site, and it used to be a baked default
1745 // on the reasoning that "changing it is a recompile either
1746 // way". That reasoning was circular: it was a recompile
1747 // *because* it was baked. The port is a crossfade position —
1748 // see [`map::table_cv`], which exists entirely to explain that
1749 // — so a live write does not switch tables, it **morphs**
1750 // between them, over the live path's own ~25 ms smoothing
1751 // ramp. This is the one control the module is named for, and
1752 // it was the only one that cost a fade-out, a voice rebuild
1753 // and a re-attack of every held note.
1754 //
1755 // Bit-exact with the constant it replaces: quiver writes an
1756 // unpatched port's default and sums a patched one's cables
1757 // starting from `+0.0`, and `0.0 + table_cv` is `table_cv`.
1758 self.knob(
1759 key,
1760 "table",
1761 table.index() as f64,
1762 ParamMap::TableIndex,
1763 false,
1764 wt.in_("table"),
1765 )?;
1766 // No hard sync: the grammar has no second oscillator to sync
1767 // *to*, and quiver retriggers phase on any positive edge, so
1768 // an unpinned Gate-kind port would be one stray cable away
1769 // from turning the oscillator into a buzz.
1770 self.constant(0.0, wt.id(), "sync")?;
1771 self.knob(key, "morph", *morph, ParamMap::Unit, false, wt.in_("morph"))?;
1772 self.wire_mod(
1773 modulation,
1774 key,
1775 *mod_depth,
1776 wt.in_("morph"),
1777 None,
1778 DepthScale::Normalized,
1779 )?;
1780 Ok(Sig::mono(wt.out("out")))
1781 }
1782 AudioNode::Pluck {
1783 octave,
1784 damping,
1785 brightness,
1786 mod_depth,
1787 modulation,
1788 ..
1789 } => {
1790 let ks = self
1791 .patch
1792 .add(format!("{key}:pluck"), KarplusStrong::new(self.sr()));
1793 self.wire_pitch(key, *octave, 0.5, ks.in_("voct"))?;
1794 // The note gate is the pluck. quiver edge-detects it, so a
1795 // held note excites the string exactly once and then rings —
1796 // which is why this source ignores the amp envelope's sustain
1797 // in a way no other source does.
1798 self.patch.connect(self.gate_out, ks.in_("trigger"))?;
1799 self.knob(
1800 key,
1801 "damp",
1802 *damping,
1803 ParamMap::Unit,
1804 false,
1805 ks.in_("damping"),
1806 )?;
1807 self.knob(
1808 key,
1809 "bright",
1810 *brightness,
1811 ParamMap::Unit,
1812 false,
1813 ks.in_("brightness"),
1814 )?;
1815 // No inharmonicity. `stretch` detunes the string's partials
1816 // away from the harmonic series, and quiver applies it as a
1817 // one-pole allpass inside the feedback loop, so it also moves
1818 // the pitch — a fifth knob that makes the module play out of
1819 // tune is not the fifth knob to have.
1820 self.constant(0.0, ks.id(), "stretch")?;
1821 self.wire_mod(
1822 modulation,
1823 key,
1824 *mod_depth,
1825 ks.in_("damping"),
1826 None,
1827 DepthScale::Normalized,
1828 )?;
1829 Ok(Sig::mono(ks.out("out")))
1830 }
1831 AudioNode::Formant {
1832 vowel,
1833 shift,
1834 octave,
1835 mod_depth,
1836 modulation,
1837 ..
1838 } => {
1839 let fo = self
1840 .patch
1841 .add(format!("{key}:formant"), FormantOsc::new(self.sr()));
1842 // Detune 0.5 is `map::detune_voct`'s no-offset centre: this
1843 // module has no detune site, but its pitch still goes through
1844 // the same Offset as every other source.
1845 self.wire_pitch(key, *octave, 0.5, fo.in_("v_oct"))?;
1846 self.knob(key, "vowel", *vowel, ParamMap::Unit, false, fo.in_("vowel"))?;
1847 self.knob(
1848 key,
1849 "fshift",
1850 *shift,
1851 ParamMap::FormantShift,
1852 true,
1853 fo.in_("formant_shift"),
1854 )?;
1855 self.constant(FORMANT_VIBRATO, fo.id(), "vibrato")?;
1856 // The vowel knob and the mod cable sum on one port, as on the
1857 // wavefolder threshold.
1858 self.wire_mod(
1859 modulation,
1860 key,
1861 *mod_depth,
1862 fo.in_("vowel"),
1863 None,
1864 DepthScale::Normalized,
1865 )?;
1866 Ok(Sig::mono(fo.out("out")))
1867 }
1868 AudioNode::Mix { balance, a, b, .. } => {
1869 let a_out = self.build(a, &format!("{key}/0"))?;
1870 let b_out = self.build(b, &format!("{key}/1"))?;
1871 let xf = self.patch.add(format!("{key}:mix"), Crossfader::new());
1872 self.feed(a_out, xf.in_("a"))?;
1873 self.feed(b_out, xf.in_("b"))?;
1874 self.knob(
1875 key,
1876 "bal",
1877 *balance,
1878 ParamMap::XfadePos,
1879 true,
1880 xf.in_("pos"),
1881 )?;
1882 Ok(Sig::mono(xf.out("out")))
1883 }
1884 AudioNode::Filter {
1885 kind,
1886 cutoff,
1887 resonance,
1888 mod_depth,
1889 input,
1890 modulation,
1891 ..
1892 } => {
1893 let in_out = self.build(input, &format!("{key}/0"))?;
1894 let (filt, out_port) = match kind {
1895 FilterKind::SvfLp | FilterKind::SvfBp | FilterKind::SvfHp => {
1896 let f = self.patch.add(format!("{key}:svf"), Svf::new(self.sr()));
1897 let port = match kind {
1898 FilterKind::SvfLp => "lp",
1899 FilterKind::SvfBp => "bp",
1900 _ => "hp",
1901 };
1902 (f, port)
1903 }
1904 FilterKind::Ladder => {
1905 let f = self
1906 .patch
1907 .add(format!("{key}:ladder"), DiodeLadderFilter::new(self.sr()));
1908 (f, "out")
1909 }
1910 };
1911 self.feed(in_out, filt.in_("in"))?;
1912 self.knob(
1913 key,
1914 "cut",
1915 *cutoff,
1916 ParamMap::Unit,
1917 false,
1918 filt.in_("cutoff"),
1919 )?;
1920 self.knob(
1921 key,
1922 "res",
1923 *resonance,
1924 ParamMap::Resonance,
1925 false,
1926 filt.in_("res"),
1927 )?;
1928 // Keyboard tracking. Without it `keytrack_amt` stays at
1929 // quiver's 0.0 default and the corner never moves: a patch
1930 // dialled in at cutoff 0.3 (≈159 Hz) speaks at C3 and is gone
1931 // by C6. Half-tracking keeps the timbre recognisable across the
1932 // keyboard without following pitch so exactly that the filter
1933 // stops colouring anything.
1934 self.patch.connect(self.pitch_out, filt.in_("keytrack"))?;
1935 self.constant(KEYTRACK_AMT, filt.id(), "keytrack_amt")?;
1936 self.wire_mod(
1937 modulation,
1938 key,
1939 *mod_depth,
1940 filt.in_("fm"),
1941 Some(in_out),
1942 DepthScale::Normalized,
1943 )?;
1944 Ok(Sig::mono(filt.out(out_port)))
1945 }
1946 AudioNode::Fold {
1947 threshold,
1948 mod_depth,
1949 input,
1950 modulation,
1951 ..
1952 } => {
1953 let in_out = self.build(input, &format!("{key}/0"))?;
1954 let fold = self.patch.add(
1955 format!("{key}:fold"),
1956 Wavefolder::new(map::fold_threshold(*threshold)),
1957 );
1958 self.feed(in_out, fold.in_("in"))?;
1959 // `#thresh` is a live knob cabled into port 1, *not* the
1960 // constructor argument. `Wavefolder::new` only sets that port's
1961 // default, and quiver's gather ignores a default the moment any
1962 // cable arrives — so as soon as `wire_mod` patched the fold, the
1963 // threshold knob went silently dead. Two cables into one input
1964 // sum, which is exactly the offset-plus-modulation the module
1965 // has no dedicated port for.
1966 self.knob(
1967 key,
1968 "thresh",
1969 *threshold,
1970 ParamMap::FoldThreshold,
1971 false,
1972 fold.in_("threshold"),
1973 )?;
1974 self.wire_mod(
1975 modulation,
1976 key,
1977 *mod_depth,
1978 fold.in_("threshold"),
1979 Some(in_out),
1980 DepthScale::Normalized,
1981 )?;
1982 Ok(Sig::mono(fold.out("out")))
1983 }
1984 AudioNode::Delay {
1985 time,
1986 feedback,
1987 mix,
1988 mod_depth,
1989 input,
1990 modulation,
1991 ..
1992 } => {
1993 let in_out = self.build(input, &format!("{key}/0"))?;
1994 let dl = self
1995 .patch
1996 .add(format!("{key}:delay"), DelayLine::new(self.sr()));
1997 self.feed(in_out, dl.in_("in"))?;
1998 self.knob(key, "time", *time, ParamMap::Unit, false, dl.in_("time"))?;
1999 self.knob(
2000 key,
2001 "fb",
2002 *feedback,
2003 ParamMap::Feedback,
2004 false,
2005 dl.in_("feedback"),
2006 )?;
2007 self.knob(key, "dmix", *mix, ParamMap::Unit, false, dl.in_("mix"))?;
2008 // Modulating delay time is the only way this grammar reaches
2009 // tape wow, flange and doppler smear; the knob and the mod
2010 // cable sum on one port, as on the wavefolder.
2011 self.wire_mod(
2012 modulation,
2013 key,
2014 *mod_depth,
2015 dl.in_("time"),
2016 Some(in_out),
2017 DepthScale::Normalized,
2018 )?;
2019 Ok(Sig::mono(dl.out("out")))
2020 }
2021 AudioNode::Chorus {
2022 rate,
2023 depth,
2024 mix,
2025 mod_depth,
2026 input,
2027 modulation,
2028 ..
2029 } => {
2030 let in_out = self.build(input, &format!("{key}/0"))?;
2031 let ch = self
2032 .patch
2033 .add(format!("{key}:chorus"), Chorus::new(self.sr()));
2034 self.feed(in_out, ch.in_("in"))?;
2035 self.knob(key, "crate", *rate, ParamMap::Unit, false, ch.in_("rate"))?;
2036 self.knob(
2037 key,
2038 "cdepth",
2039 *depth,
2040 ParamMap::Unit,
2041 false,
2042 ch.in_("depth"),
2043 )?;
2044 self.knob(key, "cmix", *mix, ParamMap::Unit, false, ch.in_("mix"))?;
2045 self.wire_mod(
2046 modulation,
2047 key,
2048 *mod_depth,
2049 ch.in_("depth"),
2050 Some(in_out),
2051 DepthScale::Normalized,
2052 )?;
2053 // Width is the entire reason a chorus exists; port 10 (`out`)
2054 // is the mono sum of the two voices and throws it away.
2055 Ok(Sig::stereo(ch.out("left"), ch.out("right")))
2056 }
2057 AudioNode::Reverb {
2058 size,
2059 damp,
2060 mix,
2061 mod_depth,
2062 input,
2063 modulation,
2064 ..
2065 } => {
2066 let in_out = self.build(input, &format!("{key}/0"))?;
2067 let rv = self
2068 .patch
2069 .add(format!("{key}:reverb"), Reverb::new(self.sr()));
2070 self.feed(in_out, rv.in_("in"))?;
2071 self.knob(key, "rsize", *size, ParamMap::Unit, false, rv.in_("size"))?;
2072 self.knob(
2073 key,
2074 "rdamp",
2075 *damp,
2076 ParamMap::Unit,
2077 false,
2078 rv.in_("damping"),
2079 )?;
2080 self.knob(key, "rmix", *mix, ParamMap::Unit, false, rv.in_("mix"))?;
2081 self.wire_mod(
2082 modulation,
2083 key,
2084 *mod_depth,
2085 rv.in_("size"),
2086 Some(in_out),
2087 DepthScale::Normalized,
2088 )?;
2089 // The decorrelation between the two tanks *is* the reverb.
2090 Ok(Sig::stereo(rv.out("left"), rv.out("right")))
2091 }
2092 AudioNode::Distortion {
2093 drive,
2094 tone,
2095 mode,
2096 mod_depth,
2097 input,
2098 modulation,
2099 ..
2100 } => {
2101 let in_out = self.build(input, &format!("{key}/0"))?;
2102 let ds = self
2103 .patch
2104 .add(format!("{key}:dist"), Distortion::new(self.sr()));
2105 self.feed(in_out, ds.in_("in"))?;
2106 self.knob(key, "drive", *drive, ParamMap::Unit, false, ds.in_("drive"))?;
2107 self.knob(key, "tone", *tone, ParamMap::Unit, false, ds.in_("tone"))?;
2108 self.constant(map::drive_mode_cv(mode.index()), ds.id(), "mode")?;
2109 // Fully wet. quiver's `mix` blends the shaped signal back
2110 // against the dry one, which is a *second* wet/dry control on
2111 // top of whatever mixer the patch already has — and at low
2112 // drive the module is nearly transparent anyway, so the knob
2113 // would spend most of its travel duplicating `#drive`.
2114 self.constant(1.0, ds.id(), "mix")?;
2115 self.wire_mod(
2116 modulation,
2117 key,
2118 *mod_depth,
2119 ds.in_("drive"),
2120 Some(in_out),
2121 DepthScale::Normalized,
2122 )?;
2123 Ok(Sig::mono(ds.out("out")))
2124 }
2125 AudioNode::Bitcrush {
2126 bits,
2127 downsample,
2128 mod_depth,
2129 input,
2130 modulation,
2131 ..
2132 } => {
2133 let in_out = self.build(input, &format!("{key}/0"))?;
2134 let bc = self.patch.add(format!("{key}:crush"), Bitcrusher::new());
2135 self.feed(in_out, bc.in_("in"))?;
2136 self.knob(key, "bits", *bits, ParamMap::Unit, false, bc.in_("bits"))?;
2137 self.knob(
2138 key,
2139 "dsamp",
2140 *downsample,
2141 ParamMap::Unit,
2142 false,
2143 bc.in_("downsample"),
2144 )?;
2145 self.wire_mod(
2146 modulation,
2147 key,
2148 *mod_depth,
2149 bc.in_("bits"),
2150 Some(in_out),
2151 DepthScale::Normalized,
2152 )?;
2153 Ok(Sig::mono(bc.out("out")))
2154 }
2155 AudioNode::Phaser {
2156 rate,
2157 depth,
2158 feedback,
2159 mod_depth,
2160 input,
2161 modulation,
2162 ..
2163 } => {
2164 let in_out = self.build(input, &format!("{key}/0"))?;
2165 let ph = self
2166 .patch
2167 .add(format!("{key}:phaser"), Phaser::new(self.sr()));
2168 self.feed(in_out, ph.in_("in"))?;
2169 self.knob(key, "prate", *rate, ParamMap::Unit, false, ph.in_("rate"))?;
2170 self.knob(
2171 key,
2172 "pdepth",
2173 *depth,
2174 ParamMap::Unit,
2175 false,
2176 ph.in_("depth"),
2177 )?;
2178 self.knob(
2179 key,
2180 "pfb",
2181 *feedback,
2182 ParamMap::FeedbackBipolar,
2183 true,
2184 ph.in_("feedback"),
2185 )?;
2186 self.constant(PHASER_STAGES, ph.id(), "stages")?;
2187 self.constant(PHASER_SPREAD, ph.id(), "spread")?;
2188 self.constant(PHASER_MIX, ph.id(), "mix")?;
2189 self.wire_mod(
2190 modulation,
2191 key,
2192 *mod_depth,
2193 ph.in_("depth"),
2194 Some(in_out),
2195 DepthScale::Normalized,
2196 )?;
2197 // Ports 11/12 are the spread pair; port 10 is the mono sweep
2198 // and discards the decorrelation `spread` exists to create.
2199 Ok(Sig::stereo(ph.out("left"), ph.out("right")))
2200 }
2201 AudioNode::Flanger {
2202 rate,
2203 depth,
2204 feedback,
2205 mod_depth,
2206 input,
2207 modulation,
2208 ..
2209 } => {
2210 let in_out = self.build(input, &format!("{key}/0"))?;
2211 let fl = self
2212 .patch
2213 .add(format!("{key}:flanger"), Flanger::new(self.sr()));
2214 self.feed(in_out, fl.in_("in"))?;
2215 self.knob(key, "frate", *rate, ParamMap::Unit, false, fl.in_("rate"))?;
2216 self.knob(
2217 key,
2218 "fdepth",
2219 *depth,
2220 ParamMap::Unit,
2221 false,
2222 fl.in_("depth"),
2223 )?;
2224 // A `CvBipolar` port, exactly as on the phaser: negative
2225 // feedback deepens the notches, positive one sharpens the
2226 // peaks, and knob centre is neither.
2227 self.knob(
2228 key,
2229 "ffb",
2230 *feedback,
2231 ParamMap::FeedbackBipolar,
2232 true,
2233 fl.in_("feedback"),
2234 )?;
2235 self.constant(FLANGER_MIX, fl.id(), "mix")?;
2236 self.constant(FLANGER_SPREAD, fl.id(), "spread")?;
2237 self.wire_mod(
2238 modulation,
2239 key,
2240 *mod_depth,
2241 fl.in_("depth"),
2242 Some(in_out),
2243 DepthScale::Normalized,
2244 )?;
2245 // Ports 11/12 are the spread pair; port 10 is bit-identical to
2246 // `left` and throws the decorrelation away.
2247 Ok(Sig::stereo(fl.out("left"), fl.out("right")))
2248 }
2249 AudioNode::Tremolo {
2250 rate,
2251 depth,
2252 shape,
2253 mod_depth,
2254 input,
2255 modulation,
2256 ..
2257 } => {
2258 let in_out = self.build(input, &format!("{key}/0"))?;
2259 let tr = self
2260 .patch
2261 .add(format!("{key}:tremolo"), Tremolo::new(self.sr()));
2262 self.feed(in_out, tr.in_("in"))?;
2263 self.knob(key, "trate", *rate, ParamMap::Unit, false, tr.in_("rate"))?;
2264 self.knob(
2265 key,
2266 "tdepth",
2267 *depth,
2268 ParamMap::Unit,
2269 false,
2270 tr.in_("depth"),
2271 )?;
2272 // Sine at 0, triangle at 1 — the difference between a breathing
2273 // amplitude and a stepped one at the same rate.
2274 self.knob(
2275 key,
2276 "tshape",
2277 *shape,
2278 ParamMap::Unit,
2279 false,
2280 tr.in_("shape"),
2281 )?;
2282 self.wire_mod(
2283 modulation,
2284 key,
2285 *mod_depth,
2286 tr.in_("depth"),
2287 Some(in_out),
2288 DepthScale::Normalized,
2289 )?;
2290 Ok(Sig::mono(tr.out("out")))
2291 }
2292 AudioNode::Vibrato {
2293 rate,
2294 depth,
2295 mix,
2296 mod_depth,
2297 input,
2298 modulation,
2299 ..
2300 } => {
2301 let in_out = self.build(input, &format!("{key}/0"))?;
2302 let vb = self
2303 .patch
2304 .add(format!("{key}:vibrato"), Vibrato::new(self.sr()));
2305 self.feed(in_out, vb.in_("in"))?;
2306 self.knob(key, "vrate", *rate, ParamMap::Unit, false, vb.in_("rate"))?;
2307 self.knob(
2308 key,
2309 "vdepth",
2310 *depth,
2311 ParamMap::Unit,
2312 false,
2313 vb.in_("depth"),
2314 )?;
2315 // Kept as a knob rather than pinned wet, because the whole
2316 // travel is musical — it is just that the interesting half is
2317 // the top. Below ~0.7 the dry copy beats against the shifted
2318 // one and the module becomes a chorus, which is a different
2319 // module in this palette.
2320 self.knob(key, "vmix", *mix, ParamMap::Unit, false, vb.in_("mix"))?;
2321 self.wire_mod(
2322 modulation,
2323 key,
2324 *mod_depth,
2325 vb.in_("depth"),
2326 Some(in_out),
2327 DepthScale::Normalized,
2328 )?;
2329 Ok(Sig::mono(vb.out("out")))
2330 }
2331 AudioNode::Eq {
2332 low,
2333 mid,
2334 high,
2335 mod_depth,
2336 input,
2337 modulation,
2338 ..
2339 } => {
2340 let in_out = self.build(input, &format!("{key}/0"))?;
2341 let eq = self
2342 .patch
2343 .add(format!("{key}:eq"), ParametricEq::new(self.sr()));
2344 self.feed(in_out, eq.in_("in"))?;
2345 // All three bands are bipolar ±5 V ports read as `cv/5 · 12`
2346 // dB, so knob centre has to be 0 dB — a tone control whose
2347 // home position colours the sound is a tone control nobody can
2348 // reason about.
2349 self.knob(
2350 key,
2351 "low",
2352 *low,
2353 ParamMap::GainBipolar,
2354 true,
2355 eq.in_("low_gain"),
2356 )?;
2357 self.knob(
2358 key,
2359 "mid",
2360 *mid,
2361 ParamMap::GainBipolar,
2362 true,
2363 eq.in_("mid_gain"),
2364 )?;
2365 self.knob(
2366 key,
2367 "high",
2368 *high,
2369 ParamMap::GainBipolar,
2370 true,
2371 eq.in_("high_gain"),
2372 )?;
2373 self.constant(EQ_LOW_FREQ, eq.id(), "low_freq")?;
2374 self.constant(EQ_MID_FREQ, eq.id(), "mid_freq")?;
2375 self.constant(EQ_MID_Q, eq.id(), "mid_q")?;
2376 self.constant(EQ_HIGH_FREQ, eq.id(), "high_freq")?;
2377 // The mid band is the modulated one: it is the only band with
2378 // a centre rather than a corner, so a wobble there is heard as
2379 // the patch moving forward and back rather than as a fade.
2380 self.wire_mod(
2381 modulation,
2382 key,
2383 *mod_depth,
2384 eq.in_("mid_gain"),
2385 Some(in_out),
2386 DepthScale::Gain,
2387 )?;
2388 Ok(Sig::mono(eq.out("out")))
2389 }
2390 AudioNode::Granular {
2391 position,
2392 size,
2393 density,
2394 mod_depth,
2395 input,
2396 modulation,
2397 ..
2398 } => {
2399 let in_out = self.build(input, &format!("{key}/0"))?;
2400 // Allocates a fixed 96 000-sample buffer (≈768 KB) at
2401 // construction, on the same order as `Reverb`'s comb bank and
2402 // on the same thread — compile time, never the audio thread.
2403 let gr = self
2404 .patch
2405 .add(format!("{key}:granular"), Granular::new(self.sr()));
2406 self.feed(in_out, gr.in_("in"))?;
2407 self.knob(
2408 key,
2409 "gpos",
2410 *position,
2411 ParamMap::Unit,
2412 false,
2413 gr.in_("position"),
2414 )?;
2415 self.knob(key, "gsize", *size, ParamMap::Unit, false, gr.in_("size"))?;
2416 self.knob(
2417 key,
2418 "gdens",
2419 *density,
2420 ParamMap::Unit,
2421 false,
2422 gr.in_("density"),
2423 )?;
2424 self.constant(GRANULAR_PITCH, gr.id(), "pitch")?;
2425 self.constant(GRANULAR_SPRAY, gr.id(), "spray")?;
2426 self.constant(GRANULAR_FREEZE, gr.id(), "freeze")?;
2427 // Position is the slot: sweeping where in the buffer the
2428 // grains are read from is the gesture the module exists for,
2429 // and it is the one that reads as motion rather than as a
2430 // different setting.
2431 self.wire_mod(
2432 modulation,
2433 key,
2434 *mod_depth,
2435 gr.in_("position"),
2436 Some(in_out),
2437 DepthScale::Normalized,
2438 )?;
2439 Ok(Sig::mono(gr.out("out")))
2440 }
2441 AudioNode::RingMod { mix, a, b, .. } => {
2442 let a_out = self.build(a, &format!("{key}/0"))?;
2443 let b_out = self.build(b, &format!("{key}/1"))?;
2444 let rm = self.patch.add(format!("{key}:ring"), RingModulator::new());
2445 self.feed(a_out, rm.in_("carrier"))?;
2446 self.feed(b_out, rm.in_("modulator"))?;
2447 // Ring modulation replaces the fundamental with sum and
2448 // difference tones, so at full wet the patch loses its own
2449 // pitch. Crossfading against the dry *carrier* — not against
2450 // silence, and not against the modulator — is what makes the
2451 // knob a "how metallic" control rather than a "how atonal"
2452 // one, and is why `a` is the carrier.
2453 let xf = self.patch.add(format!("{key}:rgmix"), Crossfader::new());
2454 self.feed(a_out, xf.in_("a"))?;
2455 self.patch.connect(rm.out("out"), xf.in_("b"))?;
2456 self.knob(key, "rgmix", *mix, ParamMap::XfadePos, true, xf.in_("pos"))?;
2457 Ok(Sig::mono(xf.out("out")))
2458 }
2459 AudioNode::Shift {
2460 semis,
2461 window,
2462 mix,
2463 mod_depth,
2464 input,
2465 modulation,
2466 ..
2467 } => {
2468 let in_out = self.build(input, &format!("{key}/0"))?;
2469 let ps = self
2470 .patch
2471 .add(format!("{key}:shift"), PitchShifter::new(self.sr()));
2472 self.feed(in_out, ps.in_("in"))?;
2473 // `#semis` and the mod cable sum on one port, as on the
2474 // wavefolder threshold — which is why each is given half of
2475 // quiver's ±24-semitone range rather than all of it.
2476 self.knob(
2477 key,
2478 "semis",
2479 *semis,
2480 ParamMap::Semitones,
2481 true,
2482 ps.in_("shift"),
2483 )?;
2484 self.knob(
2485 key,
2486 "window",
2487 *window,
2488 ParamMap::Unit,
2489 false,
2490 ps.in_("window"),
2491 )?;
2492 self.knob(key, "smix", *mix, ParamMap::Unit, false, ps.in_("mix"))?;
2493 self.wire_mod(
2494 modulation,
2495 key,
2496 *mod_depth,
2497 ps.in_("shift"),
2498 Some(in_out),
2499 DepthScale::Shift,
2500 )?;
2501 Ok(Sig::mono(ps.out("out")))
2502 }
2503 AudioNode::Comp {
2504 threshold,
2505 ratio,
2506 makeup,
2507 mod_depth,
2508 input,
2509 sidechain,
2510 modulation,
2511 ..
2512 } => {
2513 let in_out = self.build(input, &format!("{key}/0"))?;
2514 let key_out = self.build(sidechain, &format!("{key}/1"))?;
2515 let cp = self
2516 .patch
2517 .add(format!("{key}:comp"), Compressor::new(self.sr()));
2518 self.feed(in_out, cp.in_("in"))?;
2519 // quiver normals an unpatched sidechain to the main input, so
2520 // the `/1` branch is what makes this a *sidechain* compressor
2521 // rather than a plain one — and it is the only thing the
2522 // branch does: port 6 reaches the detector and never the
2523 // output.
2524 self.feed(key_out, cp.in_("sidechain"))?;
2525 self.knob(
2526 key,
2527 "thresh",
2528 *threshold,
2529 ParamMap::DetectorThreshold,
2530 false,
2531 cp.in_("threshold"),
2532 )?;
2533 self.knob(key, "ratio", *ratio, ParamMap::Unit, false, cp.in_("ratio"))?;
2534 self.knob(
2535 key,
2536 "makeup",
2537 *makeup,
2538 ParamMap::Unit,
2539 false,
2540 cp.in_("makeup"),
2541 )?;
2542 self.constant(COMP_ATTACK, cp.id(), "attack")?;
2543 self.constant(COMP_RELEASE, cp.id(), "release")?;
2544 // Threshold is the slot: moving it is what turns a static gain
2545 // trim into an audible pump. The port is a plain 0..1 CV, but
2546 // it is *not* a `Normalized` destination — see
2547 // `DepthScale::Detector`.
2548 self.wire_mod(
2549 modulation,
2550 key,
2551 *mod_depth,
2552 cp.in_("threshold"),
2553 Some(in_out),
2554 DepthScale::Detector,
2555 )?;
2556 Ok(Sig::mono(cp.out("out")))
2557 }
2558 AudioNode::Duck {
2559 amount,
2560 threshold,
2561 release,
2562 mod_depth,
2563 input,
2564 key: key_input,
2565 modulation,
2566 ..
2567 } => {
2568 let in_out = self.build(input, &format!("{key}/0"))?;
2569 let key_out = self.build(key_input, &format!("{key}/1"))?;
2570 let dk = self
2571 .patch
2572 .add(format!("{key}:duck"), Ducker::new(self.sr()));
2573 self.feed(in_out, dk.in_("in"))?;
2574 self.feed(key_out, dk.in_("key"))?;
2575 // Both of these are `ModulatedParam` knob+CV ports, not plain
2576 // CVs — the knob arrives as an offset from quiver's own base.
2577 // See `map::duck_amount`.
2578 self.knob(
2579 key,
2580 "amount",
2581 *amount,
2582 ParamMap::DuckAmount,
2583 true,
2584 dk.in_("amount"),
2585 )?;
2586 self.knob(
2587 key,
2588 "dthresh",
2589 *threshold,
2590 ParamMap::DuckThreshold,
2591 true,
2592 dk.in_("threshold"),
2593 )?;
2594 self.knob(
2595 key,
2596 "drel",
2597 *release,
2598 ParamMap::Unit,
2599 false,
2600 dk.in_("release"),
2601 )?;
2602 self.constant(DUCK_ATTACK, dk.id(), "attack")?;
2603 self.wire_mod(
2604 modulation,
2605 key,
2606 *mod_depth,
2607 dk.in_("amount"),
2608 Some(in_out),
2609 DepthScale::ParamCv,
2610 )?;
2611 Ok(Sig::mono(dk.out("out")))
2612 }
2613 AudioNode::Gate {
2614 threshold,
2615 range,
2616 release,
2617 mod_depth,
2618 input,
2619 sidechain,
2620 modulation,
2621 ..
2622 } => {
2623 let in_out = self.build(input, &format!("{key}/0"))?;
2624 let key_out = self.build(sidechain, &format!("{key}/1"))?;
2625 let ng = self
2626 .patch
2627 .add(format!("{key}:gate"), NoiseGate::new(self.sr()));
2628 self.feed(in_out, ng.in_("in"))?;
2629 // As on the compressor: unpatched, port 5 normals to the main
2630 // input and the module is an ordinary gate. The branch is what
2631 // makes it keyed.
2632 self.feed(key_out, ng.in_("sidechain"))?;
2633 self.knob(
2634 key,
2635 "gthresh",
2636 *threshold,
2637 ParamMap::DetectorThreshold,
2638 false,
2639 ng.in_("threshold"),
2640 )?;
2641 self.knob(key, "range", *range, ParamMap::Unit, false, ng.in_("range"))?;
2642 self.knob(
2643 key,
2644 "grel",
2645 *release,
2646 ParamMap::Unit,
2647 false,
2648 ng.in_("release"),
2649 )?;
2650 self.constant(GATE_ATTACK, ng.id(), "attack")?;
2651 self.wire_mod(
2652 modulation,
2653 key,
2654 *mod_depth,
2655 ng.in_("threshold"),
2656 Some(in_out),
2657 DepthScale::Detector,
2658 )?;
2659 Ok(Sig::mono(ng.out("out")))
2660 }
2661 AudioNode::Vocoder {
2662 bands,
2663 attack,
2664 release,
2665 mod_depth,
2666 carrier,
2667 modulator,
2668 modulation,
2669 ..
2670 } => {
2671 let carrier_out = self.build(carrier, &format!("{key}/0"))?;
2672 let mod_out = self.build(modulator, &format!("{key}/1"))?;
2673 let vc = self
2674 .patch
2675 .add(format!("{key}:vocoder"), Vocoder::new(self.sr()));
2676 self.feed(carrier_out, vc.in_("carrier"))?;
2677 self.feed(mod_out, vc.in_("modulator"))?;
2678 self.knob(key, "bands", *bands, ParamMap::Unit, false, vc.in_("bands"))?;
2679 self.knob(
2680 key,
2681 "vatt",
2682 *attack,
2683 ParamMap::Unit,
2684 false,
2685 vc.in_("attack"),
2686 )?;
2687 self.knob(
2688 key,
2689 "vrel",
2690 *release,
2691 ParamMap::Unit,
2692 false,
2693 vc.in_("release"),
2694 )?;
2695 // Band count is the slot. quiver quantizes it (`round(4 +
2696 // 12·cv)`), so this is the one mod destination in the grammar
2697 // that steps rather than sweeps — which is the honest thing
2698 // for it to do: resolution is what a vocoder's band count
2699 // *is*, and sweeping it is heard as the vowel going from
2700 // legible to smeared and back. The ballistics were the
2701 // alternative, and they are a decay time, not a timbre.
2702 self.wire_mod(
2703 modulation,
2704 key,
2705 *mod_depth,
2706 vc.in_("bands"),
2707 // The carrier is what the module processes, so it is the
2708 // tap — the same `/0`-is-the-signal rule the child order
2709 // follows.
2710 Some(carrier_out),
2711 DepthScale::Normalized,
2712 )?;
2713 Ok(Sig::mono(vc.out("out")))
2714 }
2715 }
2716 }
2717}
2718
2719/// Does this subtree contain a nonlinearity that can rectify, i.e. produce a
2720/// standing DC offset?
2721///
2722/// Two things in the palette can.
2723///
2724/// [`DiodeLadderFilter`]'s `diode_sat` is deliberately asymmetric
2725/// (`tanh(1.2x)` up, `tanh(0.8x)` down) and is applied at six points in the
2726/// ladder core.
2727///
2728/// [`Distortion`] in [`DriveMode::Tube`] is asymmetric *by definition* — it is
2729/// `1 − e^{−x}` above zero against `tanh(x)` below, which is the whole reason
2730/// the mode exists — so it emits DC at every drive setting above zero. Its two
2731/// siblings do not: soft clip is `tanh`, hard clip is a symmetric clamp, and
2732/// an odd nonlinearity cannot create DC from a zero-mean input. Skipping the
2733/// blocker on tube drive would put a per-note thump into every one of that
2734/// patch's feature vectors — and unlike a listener, the extractor cannot
2735/// discount it.
2736///
2737/// Everything else is linear or exactly odd-symmetric: `saturation::fold` is
2738/// `±2t − y`, the SVF's state clipper is `L·tanh(x/L)`, the bitcrusher's
2739/// quantizer is mid-tread (rounding, so unbiased), the ring modulator is a
2740/// product of two zero-mean signals, the limiter clamps symmetrically, and
2741/// every source is zero-mean — `KarplusStrong` explicitly zero-means its
2742/// excitation and leaks its loop for exactly this reason.
2743///
2744/// [`FormantOsc`] is the one that looks like an exception and is not. Its
2745/// glottal excitation is strictly **non-negative** (a half-sine open phase, a
2746/// quarter-cosine close, then zero), so it carries a large DC term — but it is
2747/// never heard directly: it reaches the output only through five parallel
2748/// 2-pole resonators whose numerator is `b0·(1 − z⁻²)`, which has an exact
2749/// zero at DC. The offset is annihilated in the filter bank, not by the voice
2750/// tail.
2751///
2752/// The 2A processors are all linear or amplitude-scaling: the flanger, vibrato
2753/// and granulator are (time-varying) delay reads, the EQ is a biquad cascade,
2754/// and the tremolo multiplies by a positive envelope — a gain, which cannot
2755/// create an offset a zero-mean input did not already have.
2756///
2757/// The 2B binaries are where this function stops being a plain recursion into
2758/// every child, and both directions matter:
2759///
2760/// - [`AudioNode::Comp`], [`AudioNode::Duck`] and [`AudioNode::Gate`] are
2761/// gains, so they pass their input's offset through — but their `/1` branch
2762/// reaches only the **detector** (quiver's ports 5/6/1 feed the envelope
2763/// follower and nothing else), so a ladder in the sidechain cannot put DC on
2764/// the output and must not buy a blocker.
2765/// - [`AudioNode::Vocoder`] is the opposite: both branches are consumed, and
2766/// *neither* can emit DC. Every band on both the analysis and the synthesis
2767/// side is a Chamberlin SVF bandpass, which has an exact zero at DC — at a
2768/// steady input the loop settles with `band = 0` — so the carrier's offset
2769/// is annihilated in the filter bank and the modulator's never reaches the
2770/// output at all. Same shape of argument as [`FormantOsc`] above, and the
2771/// same conclusion: no blocker.
2772fn makes_dc(node: &AudioNode) -> bool {
2773 match node {
2774 AudioNode::Comp { input, .. }
2775 | AudioNode::Duck { input, .. }
2776 | AudioNode::Gate { input, .. } => makes_dc(input),
2777 AudioNode::Vocoder { .. } => false,
2778 AudioNode::Vco { .. }
2779 | AudioNode::Supersaw { .. }
2780 | AudioNode::Noise { .. }
2781 | AudioNode::Wavetable { .. }
2782 | AudioNode::Pluck { .. }
2783 | AudioNode::Formant { .. }
2784 // A constant zero has no offset to block.
2785 | AudioNode::Silence { .. } => false,
2786 AudioNode::Mix { a, b, .. } | AudioNode::RingMod { a, b, .. } => makes_dc(a) || makes_dc(b),
2787 AudioNode::Filter { kind, input, .. } => {
2788 matches!(kind, FilterKind::Ladder) || makes_dc(input)
2789 }
2790 AudioNode::Distortion { mode, input, .. } => {
2791 matches!(mode, DriveMode::Tube) || makes_dc(input)
2792 }
2793 AudioNode::Fold { input, .. }
2794 | AudioNode::Delay { input, .. }
2795 | AudioNode::Chorus { input, .. }
2796 | AudioNode::Reverb { input, .. }
2797 | AudioNode::Bitcrush { input, .. }
2798 | AudioNode::Phaser { input, .. }
2799 | AudioNode::Flanger { input, .. }
2800 | AudioNode::Tremolo { input, .. }
2801 | AudioNode::Vibrato { input, .. }
2802 | AudioNode::Eq { input, .. }
2803 | AudioNode::Granular { input, .. }
2804 // A windowed buffer read plus a dry/wet blend: linear, so an offset
2805 // arrives unchanged rather than being created.
2806 | AudioNode::Shift { input, .. } => makes_dc(input),
2807 }
2808}
2809
2810/// Compile a patch term into a playable voice at the given sample rate.
2811pub fn compile(tree: &PatchTree, sample_rate: f64) -> Result<CompiledVoice, PatchError> {
2812 let mut patch = Patch::new(sample_rate);
2813 patch.set_validation_mode(ValidationMode::Warn);
2814
2815 let pitch = Arc::new(AtomicF64::new(0.0));
2816 let gate = Arc::new(AtomicF64::new(0.0));
2817 let pitch_in = patch.add("io:pitch", ExternalInput::voct(Arc::clone(&pitch)));
2818 let gate_in = patch.add("io:gate", ExternalInput::gate(Arc::clone(&gate)));
2819
2820 let mut c = Compiler {
2821 patch,
2822 pitch_out: pitch_in.out("out"),
2823 gate_out: gate_in.out("out"),
2824 params: HashMap::new(),
2825 taps: Vec::new(),
2826 };
2827
2828 // The evolved tree.
2829 let audio_out = c.build(&tree.root, "node")?;
2830
2831 // Mandatory voice stage: amp ADSR → VCA → limiter → stereo out.
2832 let adsr = c.patch.add("voice:adsr", Adsr::new(sample_rate));
2833 c.patch.connect(c.gate_out, adsr.in_("gate"))?;
2834 c.knob(
2835 "amp",
2836 "attack",
2837 tree.amp.attack,
2838 ParamMap::Unit,
2839 false,
2840 adsr.in_("attack"),
2841 )?;
2842 c.knob(
2843 "amp",
2844 "decay",
2845 tree.amp.decay,
2846 ParamMap::Unit,
2847 false,
2848 adsr.in_("decay"),
2849 )?;
2850 c.knob(
2851 "amp",
2852 "sustain",
2853 tree.amp.sustain,
2854 ParamMap::Unit,
2855 false,
2856 adsr.in_("sustain"),
2857 )?;
2858 c.knob(
2859 "amp",
2860 "release",
2861 tree.amp.release,
2862 ParamMap::Unit,
2863 false,
2864 adsr.in_("release"),
2865 )?;
2866 // Exponential contour. quiver's `shape` is a gate, not a curve amount: at
2867 // its 0 V default the whole instrument ran linear envelopes, and a linear
2868 // decay sounds like a fader being pulled, not like a note dying.
2869 c.constant(GATE_TRUE, adsr.id(), "shape")?;
2870
2871 let env = adsr.out("env");
2872 // Only pay for the blocker where DC can actually arise. It is an `Svf`,
2873 // and `Svf::tick` evaluates three transcendentals per sample — measured at
2874 // 0.057 s of render per patch, ~18% of a typical voice — so putting one on
2875 // every patch taxes the 91% that have no rectifying nonlinearity at all.
2876 let block_dc = makes_dc(&tree.root) || std::env::var("AUR_DCB_ALWAYS").is_ok();
2877 let left = c.voice_tail("", audio_out.left, env, block_dc)?;
2878 let right = match audio_out.right {
2879 Some(r) => Some(c.voice_tail("R", r, env, block_dc)?),
2880 None => None,
2881 };
2882
2883 let out = c.patch.add("voice:out", StereoOutput::new());
2884 c.patch.connect(left, out.in_("left"))?;
2885 // A mono tree leaves `right` unpatched — StereoOutput normals it to left,
2886 // and any cable at all would break that normal.
2887 if let Some(r) = right {
2888 c.patch.connect(r, out.in_("right"))?;
2889 }
2890
2891 let params = std::mem::take(&mut c.params);
2892 let recorded = std::mem::take(&mut c.taps);
2893 let mut patch = c.patch;
2894 patch.set_output(out.id());
2895 patch.compile()?;
2896 let warnings = patch.warnings().to_vec();
2897
2898 // Resolve each tap's `NodeId` back to the name it was added under. Done in
2899 // one pass here rather than by threading names through the builder: every
2900 // `Patch::add` call site would otherwise have to remember to record one,
2901 // and `Patch::nodes` already knows the answer for all of them.
2902 let names: HashMap<NodeId, &str> = patch.nodes().map(|(id, name, _)| (id, name)).collect();
2903 let taps = recorded
2904 .into_iter()
2905 .filter_map(|(key, port)| {
2906 names
2907 .get(&port.node)
2908 .map(|name| (key, (name.to_string(), port.port)))
2909 })
2910 .collect();
2911
2912 Ok(CompiledVoice {
2913 patch,
2914 pitch,
2915 gate,
2916 params,
2917 warnings,
2918 taps,
2919 })
2920}
2921
2922#[cfg(test)]
2923mod tests {
2924 use super::*;
2925 use crate::term::{
2926 AmpEnv, DriveMode, FilterKind, ModNode, NoiseColor, TableShape, Uid, Waveform,
2927 };
2928
2929 const SR: f64 = 44_100.0;
2930
2931 fn sustained(root: AudioNode) -> PatchTree {
2932 PatchTree {
2933 amp: AmpEnv {
2934 attack: 0.1,
2935 decay: 0.3,
2936 sustain: 1.0,
2937 release: 0.3,
2938 },
2939 root,
2940 }
2941 }
2942
2943 fn saw() -> AudioNode {
2944 AudioNode::Vco {
2945 uid: Uid::NEW,
2946 wave: Waveform::Saw,
2947 octave: 0,
2948 detune: 0.5,
2949 mod_depth: 0.0,
2950 modulation: ModNode::None,
2951 }
2952 }
2953
2954 /// Overwrite one of the compiler's baked constants — a `set_param_by_id`
2955 /// default on the named node's port — in an already compiled voice (the
2956 /// next tick recompiles and bakes it in). Every wiring decision in this
2957 /// module that is *not* a knob is such a constant, so this renders the
2958 /// exact counterfactual — the identical graph with one pinned value
2959 /// neutralized.
2960 fn set_constant(v: &mut CompiledVoice, node: &str, port: &str, value: f64) {
2961 let id = v
2962 .patch
2963 .get_node_id_by_name(node)
2964 .unwrap_or_else(|| panic!("no node `{node}`"));
2965 assert!(
2966 v.patch.set_param_by_id(id, port, value),
2967 "no control port `{port}` on `{node}`"
2968 );
2969 }
2970
2971 fn hold(v: &mut CompiledVoice, voct: f64, n: usize) -> Vec<(f64, f64)> {
2972 v.pitch.set(voct);
2973 v.gate.set(5.0);
2974 (0..n).map(|_| v.patch.tick()).collect()
2975 }
2976
2977 fn rms(buf: &[(f64, f64)]) -> f64 {
2978 (buf.iter().map(|(l, _)| l * l).sum::<f64>() / buf.len() as f64).sqrt()
2979 }
2980
2981 /// Every term node gets a tap, each tap names a port that resolves, and
2982 /// the taps read *different* signals from each other.
2983 ///
2984 /// The last clause is the one with teeth. A map that pointed every key at
2985 /// the root's output would satisfy "resolves and is nonzero" while being
2986 /// useless — the flow animation would show one number on every wire. So
2987 /// the fixture crossfades a saw against silence-adjacent noise and asserts
2988 /// the two branches read apart from each other, which only holds if each
2989 /// key resolved to the node that actually ends *its* chain.
2990 #[test]
2991 fn taps_name_a_live_port_on_every_term_node() {
2992 let tree = sustained(AudioNode::Mix {
2993 uid: Uid::NEW,
2994 balance: 0.5,
2995 a: Box::new(saw()),
2996 b: Box::new(AudioNode::Noise {
2997 uid: Uid::NEW,
2998 color: NoiseColor::White,
2999 }),
3000 });
3001 let mut v = compile(&tree, SR).expect("compiles");
3002
3003 for key in ["node", "node/0", "node/1"] {
3004 assert!(v.taps.contains_key(key), "no tap recorded for `{key}`");
3005 }
3006 assert_eq!(v.taps.len(), 3, "one tap per term node, no more");
3007
3008 // Read each tap while the voice sounds. Reading through the same
3009 // `get_output_value` path the observer uses keeps the test honest
3010 // about what a subscription would actually see.
3011 let read = |v: &CompiledVoice, key: &str| -> f64 {
3012 let (name, port) = v.taps.get(key).expect("tap");
3013 let id = v
3014 .patch
3015 .get_node_id_by_name(name)
3016 .unwrap_or_else(|| panic!("tap `{key}` names `{name}`, which is not in the patch"));
3017 v.patch.get_output_value(id, *port).expect("port resolves")
3018 };
3019
3020 let mut saw_trace = Vec::new();
3021 let mut noise_trace = Vec::new();
3022 v.pitch.set(0.0);
3023 v.gate.set(5.0);
3024 for _ in 0..2048 {
3025 v.patch.tick();
3026 saw_trace.push(read(&v, "node/0"));
3027 noise_trace.push(read(&v, "node/1"));
3028 }
3029
3030 let energy = |t: &[f64]| (t.iter().map(|s| s * s).sum::<f64>() / t.len() as f64).sqrt();
3031 assert!(energy(&saw_trace) > 0.0, "the saw branch reads silent");
3032 assert!(energy(&noise_trace) > 0.0, "the noise branch reads silent");
3033 // A saw is periodic and noise is not, so per-sample equality across a
3034 // 2048-sample window would take a coincidence that cannot happen.
3035 assert!(
3036 saw_trace
3037 .iter()
3038 .zip(&noise_trace)
3039 .any(|(a, b)| (a - b).abs() > 1e-9),
3040 "both branches read the same signal — the taps are not per-node"
3041 );
3042 }
3043
3044 /// Filter keytracking is wired and follows the keyboard. White noise is a
3045 /// pitch-independent source, so *any* change of level with pitch through a
3046 /// fixed-cutoff lowpass is the keytrack and nothing else — and with
3047 /// `keytrack_amt` neutralized the level must stop moving entirely.
3048 #[test]
3049 fn filter_tracks_the_keyboard() {
3050 let tree = sustained(AudioNode::Filter {
3051 uid: Uid::NEW,
3052 kind: FilterKind::SvfLp,
3053 cutoff: 0.3,
3054 resonance: 0.0,
3055 mod_depth: 0.0,
3056 input: Box::new(AudioNode::Noise {
3057 uid: Uid::NEW,
3058 color: NoiseColor::White,
3059 }),
3060 modulation: ModNode::None,
3061 });
3062 let level = |amt: Option<f64>, voct: f64| {
3063 let mut v = compile(&tree, SR).expect("compiles");
3064 if let Some(a) = amt {
3065 set_constant(&mut v, "node:svf", "keytrack_amt", a);
3066 }
3067 // Every leg hears the *same* noise. quiver's noise draws from a
3068 // thread-local RNG seeded from the system clock, so without this
3069 // each measurement gets a different realisation — and the patch
3070 // ends in a limiter, whose gain reduction tracks peak statistics
3071 // rather than RMS, so the difference between two realisations is
3072 // far larger than sampling error. Measured over 120 unseeded runs
3073 // the flat-control ratio spread from 0.0000 to 0.1332 against a
3074 // 0.1 tolerance: a 1.7%-per-run CI failure that says nothing about
3075 // keytracking. Seeded, both legs differ only by the thing under
3076 // test, which is also why the tolerance below can be tight.
3077 quiver::rng::seed(0x5EED_1E55);
3078 let out = hold(&mut v, voct, 88_200);
3079 rms(&out[44_100..])
3080 };
3081 let (low, mid, high) = (level(None, -2.0), level(None, 0.0), level(None, 2.0));
3082 assert!(
3083 low < mid && mid < high && high > low * 1.8,
3084 "cutoff does not follow pitch: C2 {low:.4}, C4 {mid:.4}, C6 {high:.4}"
3085 );
3086 // Counterfactual: amount 0 is quiver's default, i.e. the old behaviour.
3087 let (flat_low, flat_high) = (level(Some(0.0), -2.0), level(Some(0.0), 2.0));
3088 assert!(
3089 (flat_high / flat_low - 1.0).abs() < 0.01,
3090 "control is not flat, so the test proves nothing: \
3091 C2 {flat_low:.4}, C6 {flat_high:.4}"
3092 );
3093 }
3094
3095 /// The DC blocker removes the ladder's saturation offset. `diode_sat` is
3096 /// deliberately asymmetric and audio reaches it at nominal ±5 V, so without
3097 /// the blocker the amp envelope multiplies a standing offset into a thump
3098 /// on every note.
3099 #[test]
3100 fn dc_blocker_removes_the_ladder_offset() {
3101 let tree = sustained(AudioNode::Filter {
3102 uid: Uid::NEW,
3103 kind: FilterKind::Ladder,
3104 cutoff: 0.35,
3105 resonance: 0.3,
3106 mod_depth: 0.0,
3107 input: Box::new(saw()),
3108 modulation: ModNode::None,
3109 });
3110 let mut v = compile(&tree, SR).expect("compiles");
3111 let n = (SR * 5.0) as usize;
3112 let out = hold(&mut v, -2.0, n);
3113 let tail = &out[n / 2..];
3114 let dc = tail.iter().map(|(l, _)| l).sum::<f64>() / tail.len() as f64;
3115 let level = rms(tail);
3116 assert!(level > 0.1, "patch was silent, nothing to measure");
3117 assert!(
3118 dc.abs() / level < 2.0e-3,
3119 "standing DC offset {dc:.6} against {level:.4} rms"
3120 );
3121 }
3122
3123 /// Reverb and chorus keep both tanks all the way to the stereo output, and
3124 /// a mono tree still normals right to left rather than going silent.
3125 #[test]
3126 fn stereo_tanks_reach_the_output() {
3127 let mut wide = compile(
3128 &sustained(AudioNode::Reverb {
3129 uid: Uid::NEW,
3130 size: 0.7,
3131 damp: 0.4,
3132 mix: 0.6,
3133 mod_depth: 0.0,
3134 modulation: ModNode::None,
3135 input: Box::new(saw()),
3136 }),
3137 SR,
3138 )
3139 .expect("compiles");
3140 let out = hold(&mut wide, 0.0, 44_100);
3141 let tail = &out[22_050..];
3142 let width: f64 = tail.iter().map(|(l, r)| (l - r).abs()).sum::<f64>() / tail.len() as f64;
3143 assert!(
3144 width > 1.0e-2,
3145 "reverb collapsed to mono (width {width:.5})"
3146 );
3147
3148 let mut narrow = compile(&sustained(saw()), SR).expect("compiles");
3149 let out = hold(&mut narrow, 0.0, 4_410);
3150 assert!(
3151 out.iter().all(|(l, r)| l == r) && rms(&out) > 1.0e-3,
3152 "mono tree lost its right channel"
3153 );
3154 }
3155
3156 /// The wavefolder's `#thresh` knob is live *while the fold is modulated*.
3157 /// `Wavefolder::new` only sets port 1's default, and quiver ignores a
3158 /// default the moment any cable lands on the port — so the knob used to go
3159 /// silently dead exactly when a mod source was attached.
3160 #[test]
3161 fn fold_threshold_stays_live_under_modulation() {
3162 let tree = sustained(AudioNode::Fold {
3163 uid: Uid::NEW,
3164 threshold: 0.5,
3165 mod_depth: 0.6,
3166 input: Box::new(saw()),
3167 modulation: ModNode::Lfo {
3168 uid: Uid::NEW,
3169 wave: Waveform::Sine,
3170 rate: 0.4,
3171 },
3172 });
3173 let at = |thresh: f64| {
3174 let mut v = compile(&tree, SR).expect("compiles");
3175 v.params
3176 .get("node#thresh")
3177 .expect("fold threshold has no live handle")
3178 .set_normalized(thresh);
3179 let out = hold(&mut v, 0.0, 44_100);
3180 rms(&out[22_050..])
3181 };
3182 let (hard, soft) = (at(0.0), at(1.0));
3183 assert!(
3184 (hard - soft).abs() / soft.max(1.0e-9) > 0.05,
3185 "fold threshold knob is inaudible: {hard:.4} vs {soft:.4}"
3186 );
3187 // The mod depth advertised by `describe.rs` is a live handle too, so
3188 // dragging it no longer forces a whole-patch recompile.
3189 assert!(
3190 tree_params(&tree).contains(&"node#mdepth".to_string()),
3191 "mod depth has no live handle"
3192 );
3193 }
3194
3195 /// Zero crossings per window, over `windows` equal slices of `buf`.
3196 ///
3197 /// A sine's crossing count is a direct read of its instantaneous
3198 /// frequency and is blind to amplitude, so this survives the amp envelope
3199 /// and the limiter sitting between the oscillator and the measurement.
3200 fn crossings_per_window(buf: &[(f64, f64)], windows: usize) -> Vec<usize> {
3201 let w = buf.len() / windows;
3202 (0..windows)
3203 .map(|i| {
3204 buf[i * w..(i + 1) * w]
3205 .windows(2)
3206 .filter(|p| (p[0].0 < 0.0) != (p[1].0 < 0.0))
3207 .count()
3208 })
3209 .collect()
3210 }
3211
3212 /// Pitch modulation is **in octaves**, and the taper's full depth is
3213 /// exactly ±0.5 of one.
3214 ///
3215 /// This is the wave-2A capability nothing else in the grammar offers, and
3216 /// the arithmetic behind it is a three-step chain that is easy to get
3217 /// wrong by a factor of five: the [`Attenuverter`]'s gain is `level / 5`,
3218 /// its ±5 V source therefore arrives at `±level` **volts**, and the
3219 /// [`Offset`] it lands on is V/Oct — so the level *is* the octave depth.
3220 /// At `mod_depth` 1.0 that is ±0.5 octave, i.e. the fastest moment of the
3221 /// sweep is a full **2×** the slowest. Asserting the ratio rather than
3222 /// "something moved" is what makes this a test of the mapping instead of
3223 /// a test that a cable exists.
3224 ///
3225 /// The LFO runs one full cycle across the render, so the measurement does
3226 /// not depend on where its phase starts.
3227 #[test]
3228 fn pitch_modulation_spans_exactly_one_octave_at_full_depth() {
3229 let vco = |mod_depth: f64| AudioNode::Vco {
3230 uid: Uid::NEW,
3231 wave: Waveform::Sine,
3232 octave: 0,
3233 detune: 0.5,
3234 mod_depth,
3235 modulation: ModNode::Lfo {
3236 uid: Uid::NEW,
3237 wave: Waveform::Sine,
3238 // 0.01·3000^x Hz ⇒ 0.5 Hz, one cycle in the 2 s rendered.
3239 rate: 0.4886,
3240 },
3241 };
3242 // Held two octaves up: a 100 ms window then spans ~420 crossings, so
3243 // the ±1 quantization of counting them is 0.2% rather than the 4% it
3244 // would be at C4 — which is the difference between "the depth-0 leg is
3245 // inert" being a measurement and being a hope.
3246 let span = |mod_depth: f64| {
3247 let mut v = compile(&sustained(vco(mod_depth)), SR).expect("compiles");
3248 let out = hold(&mut v, 2.0, 88_200);
3249 let counts = crossings_per_window(&out, 20);
3250 let hi = *counts.iter().max().expect("windows") as f64;
3251 let lo = *counts.iter().min().expect("windows") as f64;
3252 hi / lo.max(1.0)
3253 };
3254
3255 let full = span(1.0);
3256 assert!(
3257 (1.8..2.2).contains(&full),
3258 "full pitch depth spans {full:.3}× in frequency, not the 2.0× that \
3259 ±0.5 octave means — the attenuverter/V-Oct arithmetic is off"
3260 );
3261 // A tenth of the knob is the vibrato corner: ±0.05 octave ≈ ±60 cents,
3262 // so the span is 2^0.1 ≈ 1.072. Linear taper, so this follows from the
3263 // number above — and would not if the taper were square-law.
3264 let tenth = span(0.1);
3265 assert!(
3266 (1.03..1.12).contains(&tenth),
3267 "pitch depth 0.1 spans {tenth:.4}×, not the ~1.072× a linear taper gives"
3268 );
3269 // And the cable is inert at zero depth rather than merely quiet.
3270 let none = span(0.0);
3271 assert!(
3272 none < 1.02,
3273 "an empty pitch depth still moved the pitch by {none:.4}×"
3274 );
3275 }
3276
3277 /// The EQ's modulation slot lands on a **volt-scaled** port, and is taken
3278 /// to that port's own scale rather than the normalized one.
3279 ///
3280 /// quiver reads `ParametricEq`'s bands as `cv/5 · 12` dB. The taper every
3281 /// other slot in this grammar uses is sized for a 0..1 CV — half of full
3282 /// scale, i.e. ±0.5 V — which on this port is **±1.2 dB at full depth**,
3283 /// about the level JND. That is a mod slot that does nothing across its
3284 /// entire travel, and it reviews as correct because the cable is there.
3285 /// `DepthScale::Gain` reaches the port's own ±5 V, so full depth is a
3286 /// ±12 dB pump.
3287 ///
3288 /// The two tapers differ by exactly 10×, which is what makes this
3289 /// measurable rather than arguable: **`mod_depth` 0.1 under the gain taper
3290 /// is precisely what `mod_depth` 1.0 would have been under the normalized
3291 /// one**, so the same render measures both designs. A sine parked on the
3292 /// bell's centre (≈1.26 kHz, i.e. 2.27 octaves above C4) makes the band's
3293 /// gain the whole signal's gain; the voice limiter clips the boost half,
3294 /// so what the swing reports is the cut half reaching its full −12 dB.
3295 #[test]
3296 fn eq_modulation_reaches_the_bands_own_volt_scale() {
3297 let eq = |mod_depth: f64| AudioNode::Eq {
3298 uid: Uid::NEW,
3299 low: 0.5,
3300 mid: 0.5,
3301 high: 0.5,
3302 mod_depth,
3303 input: Box::new(AudioNode::Vco {
3304 uid: Uid::NEW,
3305 wave: Waveform::Sine,
3306 octave: 0,
3307 detune: 0.5,
3308 mod_depth: 0.0,
3309 modulation: ModNode::None,
3310 }),
3311 modulation: ModNode::Lfo {
3312 uid: Uid::NEW,
3313 wave: Waveform::Sine,
3314 rate: 0.4886, // 0.5 Hz — one cycle in the 2 s rendered
3315 },
3316 };
3317 let swing = |mod_depth: f64| {
3318 let mut v = compile(&sustained(eq(mod_depth)), SR).expect("compiles");
3319 let out = hold(&mut v, 2.27, 88_200);
3320 let w = out.len() / 20;
3321 let levels: Vec<f64> = (0..20).map(|i| rms(&out[i * w..(i + 1) * w])).collect();
3322 let hi = levels.iter().cloned().fold(0.0_f64, f64::max);
3323 let lo = levels.iter().cloned().fold(f64::MAX, f64::min);
3324 hi / lo.max(1.0e-12)
3325 };
3326
3327 // −12 dB is 3.98×; anything near it means the cable reached the port.
3328 let full = swing(1.0);
3329 assert!(
3330 full > 3.5,
3331 "eq modulation swings the level only {full:.3}× at full depth, not \
3332 the ~4× that ±12 dB on the mid band means"
3333 );
3334 // The counterfactual: the normalized taper's entire travel, which is
3335 // a hair over the level JND and 4% of the swing above.
3336 let as_normalized = swing(0.1);
3337 assert!(
3338 as_normalized < 1.25,
3339 "the normalized taper reaches {as_normalized:.3}× here — if that is \
3340 no longer ~1.15× the 10× ratio between the two tapers has moved"
3341 );
3342 // And the cable is inert at zero depth rather than merely quiet.
3343 let none = swing(0.0);
3344 assert!(
3345 none < 1.05,
3346 "an empty eq mod depth still moved the level by {none:.3}×"
3347 );
3348 }
3349
3350 /// Magnitude of `buf` at `hz`, normalized by length — a one-bin DFT.
3351 ///
3352 /// The pitch shifter's output is a windowed sum of two resampled grains,
3353 /// so counting zero crossings measures the grain boundaries as much as the
3354 /// pitch. Correlating against the tone being looked for does not.
3355 fn tone_mag(buf: &[(f64, f64)], hz: f64, sr: f64) -> f64 {
3356 let (mut re, mut im) = (0.0, 0.0);
3357 for (n, (l, _)) in buf.iter().enumerate() {
3358 let w = std::f64::consts::TAU * hz * n as f64 / sr;
3359 re += l * w.cos();
3360 im += l * w.sin();
3361 }
3362 (re * re + im * im).sqrt() / buf.len() as f64
3363 }
3364
3365 /// The semitone offset (over `range`) whose tone is strongest in `buf`,
3366 /// relative to `base_hz`.
3367 fn dominant_semitone(buf: &[(f64, f64)], base_hz: f64, range: i32) -> i32 {
3368 (-range..=range)
3369 .max_by(|a, b| {
3370 let m = |k: &i32| tone_mag(buf, base_hz * 2f64.powf(*k as f64 / 12.0), SR);
3371 m(a).total_cmp(&m(b))
3372 })
3373 .expect("non-empty range")
3374 }
3375
3376 fn sine_src() -> AudioNode {
3377 AudioNode::Vco {
3378 uid: Uid::NEW,
3379 wave: Waveform::Sine,
3380 octave: 0,
3381 detune: 0.5,
3382 mod_depth: 0.0,
3383 modulation: ModNode::None,
3384 }
3385 }
3386
3387 /// A plucked string, the default key/sidechain branch — and the quietest
3388 /// source in the palette, which is the whole reason the threshold knobs
3389 /// are geometric.
3390 fn pluck_key() -> AudioNode {
3391 AudioNode::Pluck {
3392 uid: Uid::NEW,
3393 octave: -1,
3394 damping: 0.4,
3395 brightness: 0.7,
3396 mod_depth: 0.0,
3397 modulation: ModNode::None,
3398 }
3399 }
3400
3401 /// Per-window RMS over `n` equal slices, with quiver's noise RNG seeded.
3402 ///
3403 /// The seed is not optional here: every one of these patches is keyed from
3404 /// a Karplus-Strong string, whose excitation is drawn from a clock-seeded
3405 /// thread-local RNG — so an unseeded run measures a different string each
3406 /// time, and a gate's close *time* moves by hundreds of milliseconds
3407 /// between realisations.
3408 fn window_rms(tree: &PatchTree, voct: f64, n: usize) -> Vec<f64> {
3409 quiver::rng::seed(0x2B_5EED);
3410 let mut v = compile(tree, SR).expect("compiles");
3411 let out = hold(&mut v, voct, 44_100);
3412 let w = out.len() / n;
3413 (0..n).map(|i| rms(&out[i * w..(i + 1) * w])).collect()
3414 }
3415
3416 /// The pitch shifter's `#semis` knob is in **semitones**, on quiver's own
3417 /// scale, with unison at knob centre.
3418 ///
3419 /// quiver reads the port as `cv/5 · 24` semitones and hard-clamps at ±24
3420 /// (`PitchShifter`, nonlinear.rs), so this is the third member of the
3421 /// family of errors wave 2A kept making: a control that reviews as correct
3422 /// because the cable exists and is off by a factor. Passing the raw 0..1
3423 /// knob would have given 0..+4.8 semitones with **no downward shift at
3424 /// all** — a "pitch shift" that can only go up, and only by a third.
3425 ///
3426 /// Measured as a one-bin DFT rather than by counting zero crossings,
3427 /// because the output is a windowed sum of two resampled grains: the grain
3428 /// boundaries cross zero too.
3429 #[test]
3430 fn pitch_shift_lands_on_quivers_semitone_scale() {
3431 // C4 held two octaves up, so a 1 s window resolves the interval and
3432 // the grain-rate sidebands sit further from the fundamental.
3433 let base = 261.625_565 * 4.0;
3434 let at = |semis: f64| {
3435 let tree = sustained(AudioNode::Shift {
3436 uid: Uid::NEW,
3437 semis,
3438 window: 0.5,
3439 mix: 1.0, // fully wet: the dry copy would win every bin
3440 mod_depth: 0.0,
3441 input: Box::new(sine_src()),
3442 modulation: ModNode::None,
3443 });
3444 let mut v = compile(&tree, SR).expect("compiles");
3445 let out = hold(&mut v, 2.0, 88_200);
3446 dominant_semitone(&out[44_100..], base, 14)
3447 };
3448 for (knob, want) in [(0.0, -12), (0.5, 0), (1.0, 12)] {
3449 let got = at(knob);
3450 assert!(
3451 (got - want).abs() <= 1,
3452 "shift knob {knob} transposes {got:+} semitones, not {want:+} — \
3453 the ±12-at-the-ends, unison-at-centre map is off"
3454 );
3455 }
3456 }
3457
3458 /// ...and its modulation slot lands on that same semitone scale rather
3459 /// than on the normalized one every other slot in the grammar uses.
3460 ///
3461 /// The two tapers differ by exactly 5× (`SHIFT_PEAK_V` 2.5 V against
3462 /// `PEAK_NORMALIZED` 0.5), which is what makes this measurable rather than
3463 /// arguable: **`mod_depth` 0.2 under the shift taper is precisely what
3464 /// `mod_depth` 1.0 would have been under the normalized one**, so the same
3465 /// render measures both designs.
3466 #[test]
3467 fn pitch_shift_modulation_reaches_the_ports_own_semitone_scale() {
3468 let base = 261.625_565 * 4.0;
3469 let span = |mod_depth: f64| {
3470 let tree = sustained(AudioNode::Shift {
3471 uid: Uid::NEW,
3472 semis: 0.5,
3473 window: 0.5,
3474 mix: 1.0,
3475 mod_depth,
3476 input: Box::new(sine_src()),
3477 modulation: ModNode::Lfo {
3478 uid: Uid::NEW,
3479 wave: Waveform::Triangle,
3480 rate: 0.4886, // ≈0.5 Hz — one cycle in the 2 s rendered
3481 },
3482 });
3483 let mut v = compile(&tree, SR).expect("compiles");
3484 let out = hold(&mut v, 2.0, 88_200);
3485 let w = out.len() / 16;
3486 let ks: Vec<i32> = (0..16)
3487 .map(|i| dominant_semitone(&out[i * w..(i + 1) * w], base, 14))
3488 .collect();
3489 ks.iter().max().expect("windows") - ks.iter().min().expect("windows")
3490 };
3491
3492 let full = span(1.0);
3493 assert!(
3494 full >= 18,
3495 "full shift depth sweeps only {full} semitones, not the ~24 that \
3496 ±12 means — the attenuverter arithmetic is off"
3497 );
3498 // The counterfactual: the normalized taper's *entire* travel.
3499 let as_normalized = span(0.2);
3500 assert!(
3501 as_normalized <= 8,
3502 "the normalized taper sweeps {as_normalized} semitones here — if \
3503 that is no longer ~5 the 5× ratio between the two tapers has moved"
3504 );
3505 assert_eq!(span(0.0), 0, "an empty shift depth still moved the pitch");
3506 }
3507
3508 /// The three dynamics thresholds are **geometric over 0.05–5 V**, and they
3509 /// have to be, because this instrument's sources are not on one level.
3510 ///
3511 /// quiver reads all three as a plain `cv · 5` volts against a smoothed
3512 /// `|x|` detector, so passing the raw knob through is the obvious thing —
3513 /// and it produces a gate that never opens. Measured mean `|x|` on a held
3514 /// note: sine vco 3.18 V, plucked string 0.14 V. The pluck is what a gate
3515 /// or a ducker is usually keyed from, and under the linear map its whole
3516 /// useful range sat below knob position 0.1.
3517 ///
3518 /// The arithmetic first, then the behaviour it buys: with the default key
3519 /// branch the gate must both **open** on the pluck's attack and **shut**
3520 /// again as the string decays, inside one held note.
3521 #[test]
3522 fn the_dynamics_threshold_knob_spans_the_levels_the_palette_produces() {
3523 let volts = |x: f64| map::detector_threshold(x) * 5.0;
3524 assert!((volts(0.0) - 0.05).abs() < 1e-9, "bottom of the knob moved");
3525 assert!((volts(1.0) - 5.0).abs() < 1e-9, "top of the knob moved");
3526 // Geometric: the midpoint is the geometric mean, not the arithmetic
3527 // one (which would be 2.5 V and put every non-oscillator off the dial).
3528 assert!((volts(0.5) - 0.5).abs() < 1e-3, "the knob is not geometric");
3529 assert!(
3530 volts(0.35) < 0.3,
3531 "knob 0.35 asks for {:.3} V — under the linear map it asked for \
3532 1.75 V, which no key in the palette ever reaches",
3533 volts(0.35)
3534 );
3535
3536 // The behaviour. `range` 0.7 means a shut gate passes 0.3 of the
3537 // signal, so open and shut differ by ~10 dB and are unmistakable.
3538 let gated = sustained(AudioNode::Gate {
3539 uid: Uid::NEW,
3540 threshold: 0.45,
3541 range: 0.7,
3542 release: 0.3,
3543 mod_depth: 0.0,
3544 input: Box::new(sine_src()),
3545 sidechain: Box::new(pluck_key()),
3546 modulation: ModNode::None,
3547 });
3548 let levels = window_rms(&gated, 0.0, 20);
3549 let (hi, lo) = (
3550 levels.iter().cloned().fold(0.0_f64, f64::max),
3551 levels.iter().cloned().fold(f64::MAX, f64::min),
3552 );
3553 assert!(
3554 hi / lo.max(1e-12) > 2.5,
3555 "the gate never changes state across a held note: {levels:?}"
3556 );
3557 // ...and in that order: open on the transient, shut on the decay.
3558 assert!(
3559 levels[1] > 2.0 * levels[19],
3560 "the gate did not open on the attack and shut on the decay: {levels:?}"
3561 );
3562 }
3563
3564 /// The ducker's two knobs are **offsets from quiver's own knob base**, not
3565 /// plain CVs — and getting that wrong is a control that is at full depth
3566 /// across its entire travel.
3567 ///
3568 /// `Ducker` reads `amount` and `threshold` through a `ModulatedParam`
3569 /// (`base + cv/5`, dynamics.rs) whose base is set in `Ducker::new` and is
3570 /// reachable only from Rust, not from a port. `amount`'s base is **1.0**,
3571 /// so passing the raw 0..1 knob would have run the parameter from 1.0 to
3572 /// 1.2 and clamped: full ducking at every knob position, including zero.
3573 #[test]
3574 fn the_ducker_knob_offsets_quivers_own_base() {
3575 let ducked = |amount: f64| {
3576 let tree = sustained(AudioNode::Duck {
3577 uid: Uid::NEW,
3578 amount,
3579 threshold: 0.4,
3580 release: 0.35,
3581 mod_depth: 0.0,
3582 input: Box::new(sine_src()),
3583 key: Box::new(pluck_key()),
3584 modulation: ModNode::None,
3585 });
3586 let levels = window_rms(&tree, 0.0, 10);
3587 // The key decays, so the deepest duck is at the start.
3588 levels[0]
3589 };
3590 let (open, deep) = (ducked(0.0), ducked(1.0));
3591 assert!(
3592 deep < open * 0.5,
3593 "full duck depth only reaches {deep:.3} against {open:.3} unducked"
3594 );
3595 // The end that the raw-knob bug would have destroyed: at zero the
3596 // module must be a wire.
3597 let dry = window_rms(&sustained(sine_src()), 0.0, 10)[0];
3598 assert!(
3599 (open - dry).abs() / dry < 0.02,
3600 "a ducker at amount 0 is not a wire: {open:.3} against {dry:.3}"
3601 );
3602 // Monotone in between, so the knob is a depth and not a switch.
3603 let mid = ducked(0.5);
3604 assert!(
3605 deep < mid && mid < open,
3606 "duck depth is not monotone: {deep:.3} {mid:.3} {open:.3}"
3607 );
3608 }
3609
3610 /// The ducker's modulation slot lands on a `ModulatedParam` port, which
3611 /// costs **ten times** the volts a normalized port does for the same
3612 /// musical depth.
3613 ///
3614 /// `PEAK_PARAM_CV` is 2.5 V against `PEAK_NORMALIZED`'s 0.5, so — as with
3615 /// the eq — `mod_depth` 0.2 here is exactly what `mod_depth` 1.0 would
3616 /// have been under the normalized taper, and one pair of renders measures
3617 /// both designs.
3618 #[test]
3619 fn duck_modulation_reaches_the_param_cv_scale() {
3620 let swing = |mod_depth: f64| {
3621 let tree = sustained(AudioNode::Duck {
3622 uid: Uid::NEW,
3623 // Mid depth, so the cable has room to move it both ways.
3624 amount: 0.5,
3625 threshold: 0.2,
3626 release: 0.35,
3627 mod_depth,
3628 input: Box::new(sine_src()),
3629 key: Box::new(pluck_key()),
3630 modulation: ModNode::Lfo {
3631 uid: Uid::NEW,
3632 wave: Waveform::Sine,
3633 rate: 0.5595, // ≈0.9 Hz — a full cycle inside the render
3634 },
3635 });
3636 let levels = window_rms(&tree, 0.0, 20);
3637 let hi = levels.iter().cloned().fold(0.0_f64, f64::max);
3638 let lo = levels.iter().cloned().fold(f64::MAX, f64::min);
3639 hi / lo.max(1e-12)
3640 };
3641 let full = swing(1.0);
3642 assert!(
3643 full > 2.0,
3644 "duck modulation swings the level only {full:.3}× at full depth"
3645 );
3646 let as_normalized = swing(0.2);
3647 assert!(
3648 as_normalized < full * 0.6,
3649 "the normalized taper reaches {as_normalized:.3}× against the \
3650 gain taper's {full:.3}× — the 5× ratio between them has moved"
3651 );
3652 }
3653
3654 /// A vocoder emits no DC, so `makes_dc` is right to refuse it a blocker.
3655 ///
3656 /// The argument is that every band on both sides is a Chamberlin SVF
3657 /// *bandpass*, which has an exact zero at DC — so the carrier's offset is
3658 /// annihilated in the filter bank and the modulator's never reaches the
3659 /// output at all. That is a claim about quiver's arithmetic, and this
3660 /// measures it on the module rather than trusting it: a ladder in the
3661 /// carrier is the palette's own DC generator, and the ladder alone would
3662 /// fail the vetting gate's `|mean|/rms` test without a blocker.
3663 #[test]
3664 fn a_vocoder_annihilates_its_carriers_dc() {
3665 let tree = sustained(AudioNode::Vocoder {
3666 uid: Uid::NEW,
3667 bands: 0.6,
3668 attack: 0.25,
3669 release: 0.3,
3670 mod_depth: 0.0,
3671 carrier: Box::new(AudioNode::Filter {
3672 uid: Uid::NEW,
3673 kind: FilterKind::Ladder,
3674 cutoff: 0.6,
3675 resonance: 0.4,
3676 mod_depth: 0.0,
3677 modulation: ModNode::None,
3678 input: Box::new(saw()),
3679 }),
3680 modulator: Box::new(AudioNode::Formant {
3681 uid: Uid::NEW,
3682 vowel: 0.3,
3683 shift: 0.5,
3684 octave: 0,
3685 mod_depth: 0.0,
3686 modulation: ModNode::None,
3687 }),
3688 modulation: ModNode::None,
3689 });
3690 assert!(
3691 !makes_dc(&tree.root),
3692 "a vocoder must not buy the voice a DC blocker"
3693 );
3694 quiver::rng::seed(0x2B_5EED);
3695 let mut v = compile(&tree, SR).expect("compiles");
3696 let out = hold(&mut v, 0.0, 44_100);
3697 let tail = &out[22_050..];
3698 let dc = tail.iter().map(|(l, _)| l).sum::<f64>() / tail.len() as f64;
3699 let level = rms(tail);
3700 assert!(level > 1e-3, "the vocoder was silent, nothing to measure");
3701 assert!(
3702 dc.abs() / level < 2.0e-3,
3703 "standing DC offset {dc:.6} against {level:.4} rms — the bandpass \
3704 zero this skips the blocker for is not where it was thought to be"
3705 );
3706 }
3707
3708 fn tree_params(tree: &PatchTree) -> Vec<String> {
3709 compile(tree, SR)
3710 .expect("compiles")
3711 .params
3712 .keys()
3713 .cloned()
3714 .collect()
3715 }
3716
3717 /// Amp envelope and VCA run exponential, not linear. Measured as the
3718 /// convexity of the decay: a linear contour through a linear VCA is a
3719 /// straight line to the sustain floor, so it sits at exactly half its
3720 /// starting level halfway through the decay.
3721 #[test]
3722 fn amp_contour_is_exponential() {
3723 let tree = PatchTree {
3724 amp: AmpEnv {
3725 attack: 0.0,
3726 decay: 0.7, // ≈630 ms
3727 sustain: 0.0,
3728 release: 0.3,
3729 },
3730 root: saw(),
3731 };
3732 let half_life = |exp: bool| {
3733 let mut v = compile(&tree, SR).expect("compiles");
3734 if !exp {
3735 // The gate is baked on both `Adsr.shape` and `Vca.response`.
3736 set_constant(&mut v, "voice:adsr", "shape", GATE_FALSE);
3737 set_constant(&mut v, "voice:vca", "response", GATE_FALSE);
3738 }
3739 let out = hold(&mut v, 0.0, (SR * 0.7) as usize);
3740 // Peak amplitude in each 10 ms window, as an envelope follower.
3741 let win = (SR * 0.01) as usize;
3742 let env: Vec<f64> = out
3743 .chunks(win)
3744 .map(|c| c.iter().fold(0.0f64, |m, (l, _)| m.max(l.abs())))
3745 .collect();
3746 let start = env[2];
3747 env.iter()
3748 .position(|&e| e < start * 0.5)
3749 .unwrap_or(env.len()) as f64
3750 * 0.01
3751 };
3752 let (exp, lin) = (half_life(true), half_life(false));
3753 assert!(
3754 exp < lin * 0.8,
3755 "decay is not exponential: half-life {exp:.2}s exp vs {lin:.2}s linear"
3756 );
3757 }
3758
3759 /// Tube drive rectifies, and `makes_dc` is right to buy a blocker for it.
3760 ///
3761 /// The premise first, measured on quiver's module rather than asserted: a
3762 /// zero-mean sine through the asymmetric curve comes out with a standing
3763 /// offset, while the two symmetric curves leave it at zero (the ~0.0015
3764 /// floor below is the window's own partial cycle, not a signal).
3765 ///
3766 /// The offset is largest at *low* drive — 7.7% of RMS at drive 0.05,
3767 /// falling to 1.0% at drive 1.0 — because `1 − e^{−x}` and `tanh(x)` both
3768 /// saturate to ±1, so heavy drive is nearly symmetric and it is the gentle
3769 /// settings, the ones a patch is most likely to use, that rectify. −22 dB
3770 /// of DC multiplied by the amp envelope is an audible per-note thump, and
3771 /// one whose spectrum reaches far above the offset itself.
3772 #[test]
3773 fn dc_blocker_removes_the_tube_distortion_offset() {
3774 let raw_offset = |mode_cv: f64| {
3775 let mut p = Patch::new(SR);
3776 p.set_validation_mode(ValidationMode::Warn);
3777 let osc = p.add("osc", Vco::new(SR));
3778 let d = p.add("d", Distortion::new(SR));
3779 p.connect(osc.out("sin"), d.in_("in")).expect("wires");
3780 // Tone wide open, so nothing but the shaper is being measured.
3781 for (port, v) in [
3782 ("drive", 0.1),
3783 ("tone", 1.0),
3784 ("mode", mode_cv),
3785 ("mix", 1.0),
3786 ] {
3787 assert!(p.set_param_by_id(d.id(), port, v), "no port {port}");
3788 }
3789 let out = p.add("out", StereoOutput::new());
3790 p.connect(d.out("out"), out.in_("left")).expect("wires");
3791 p.set_output(out.id());
3792 p.compile().expect("compiles");
3793 let buf: Vec<f64> = (0..(SR as usize)).map(|_| p.tick().0).collect();
3794 let tail = &buf[SR as usize / 2..];
3795 let mean = tail.iter().sum::<f64>() / tail.len() as f64;
3796 let rms = (tail.iter().map(|x| x * x).sum::<f64>() / tail.len() as f64).sqrt();
3797 mean.abs() / rms.max(1.0e-12)
3798 };
3799 let (soft, hard, tube) = (
3800 raw_offset(map::drive_mode_cv(0)),
3801 raw_offset(map::drive_mode_cv(1)),
3802 raw_offset(map::drive_mode_cv(2)),
3803 );
3804 assert!(
3805 tube > 0.05 && soft < 5.0e-3 && hard < 5.0e-3,
3806 "the asymmetry premise is wrong: soft {soft:.5}, hard {hard:.5}, tube {tube:.5}"
3807 );
3808
3809 // ...and the compiled voice has none of it left. Sustain is well
3810 // under the limiter: clipping an asymmetric waveform is itself a
3811 // rectifier, downstream of the blocker, and it would be measured here
3812 // as a failure of a stage that cannot see it.
3813 let tree = PatchTree {
3814 amp: AmpEnv {
3815 attack: 0.05,
3816 decay: 0.3,
3817 sustain: 0.35,
3818 release: 0.3,
3819 },
3820 root: AudioNode::Distortion {
3821 uid: Uid::NEW,
3822 drive: 0.15,
3823 tone: 0.7,
3824 mode: DriveMode::Tube,
3825 mod_depth: 0.0,
3826 input: Box::new(saw()),
3827 modulation: ModNode::None,
3828 },
3829 };
3830 assert!(makes_dc(&tree.root), "tube drive must buy a blocker");
3831 let mut v = compile(&tree, SR).expect("compiles");
3832 let n = (SR * 3.0) as usize;
3833 let out = hold(&mut v, -1.0, n);
3834 let tail = &out[n / 2..];
3835 let dc = tail.iter().map(|(l, _)| l).sum::<f64>() / tail.len() as f64;
3836 let level = rms(tail);
3837 assert!(level > 0.1, "patch was silent, nothing to measure");
3838 assert!(
3839 dc.abs() / level < 2.0e-3,
3840 "standing DC offset {dc:.6} against {level:.4} rms"
3841 );
3842 // The symmetric modes pay nothing for it.
3843 for mode in [DriveMode::Soft, DriveMode::Hard] {
3844 let clean = sustained(AudioNode::Distortion {
3845 uid: Uid::NEW,
3846 drive: 0.15,
3847 tone: 0.7,
3848 mode,
3849 mod_depth: 0.0,
3850 input: Box::new(saw()),
3851 modulation: ModNode::None,
3852 });
3853 assert!(!makes_dc(&clean.root), "{mode:?} must not buy a blocker");
3854 }
3855 }
3856
3857 /// The envelope follower rides the owning module's *own* input, and
3858 /// degrades to silence rather than to a panic where there is no input.
3859 #[test]
3860 fn the_follower_reads_the_signal_below_it() {
3861 // A lowpass whose cutoff is opened by the level of what it is
3862 // filtering. Playing louder is not available, so the counterfactual is
3863 // the same tree with the depth knob at zero: identical graph, one
3864 // attenuverter neutralized.
3865 let tree = |depth: f64| {
3866 sustained(AudioNode::Filter {
3867 uid: Uid::NEW,
3868 kind: FilterKind::SvfLp,
3869 cutoff: 0.25,
3870 resonance: 0.0,
3871 mod_depth: depth,
3872 input: Box::new(saw()),
3873 modulation: ModNode::Follow {
3874 uid: Uid::NEW,
3875 sens: 0.8,
3876 release: 0.3,
3877 },
3878 })
3879 };
3880 let level = |depth: f64| {
3881 let mut v = compile(&tree(depth), SR).expect("compiles");
3882 let out = hold(&mut v, 0.0, 44_100);
3883 rms(&out[22_050..])
3884 };
3885 let (off, on) = (level(0.0), level(0.9));
3886 assert!(
3887 on > off * 1.05,
3888 "the follower is inaudible: {off:.4} closed vs {on:.4} open"
3889 );
3890
3891 // A source's slot has nothing to tap. That must compile and stay
3892 // silent on the cable, because both the prior and the panel can put a
3893 // follower there.
3894 let lone = sustained(AudioNode::Wavetable {
3895 uid: Uid::NEW,
3896 table: crate::term::TableShape::Saw,
3897 octave: 0,
3898 morph: 0.4,
3899 mod_depth: 0.8,
3900 modulation: ModNode::Follow {
3901 uid: Uid::NEW,
3902 sens: 0.8,
3903 release: 0.3,
3904 },
3905 });
3906 let mut v = compile(&lone, SR).expect("a follower on a source must still compile");
3907 assert!(
3908 rms(&hold(&mut v, 0.0, 22_050)) > 1.0e-3,
3909 "the wavetable went silent"
3910 );
3911 }
3912
3913 /// `table_cv` has to land each table on an *exact* integer position in
3914 /// quiver's stack, because the port is a crossfade, not a selector: quiver
3915 /// takes `idx = floor(cv·7)` and blends table `idx` into `idx+1` by
3916 /// `frac + morph`. Any non-zero `frac` both mis-names the table on the
3917 /// plate and eats the top of the morph knob's travel, and neither symptom
3918 /// is visible in a diff — this is the guard that makes it visible.
3919 #[test]
3920 fn every_wavetable_shape_lands_on_its_own_table() {
3921 for i in 0..TableShape::ALL.len() {
3922 let pos = map::table_cv(i as f64) * 7.0;
3923 let frac = pos - pos.floor();
3924 assert!(
3925 frac < 1e-12 || (1.0 - frac) < 1e-12,
3926 "table {i} lands at {pos} — fraction {frac} blends it into its neighbour"
3927 );
3928 // …and inside the stack, so no shape is unreachable.
3929 assert!(
3930 (0.0..=7.0).contains(&pos),
3931 "table {i} maps outside the stack"
3932 );
3933 }
3934 // Distinct tables, in order: the plate's index IS the table you hear.
3935 let cvs: Vec<f64> = (0..TableShape::ALL.len())
3936 .map(|i| map::table_cv(i as f64))
3937 .collect();
3938 assert!(
3939 cvs.windows(2).all(|w| w[1] > w[0]),
3940 "table CVs are not monotonic"
3941 );
3942 }
3943
3944 /// ...and it is not a bass cut. A DC blocker that audits as "thin" has
3945 /// traded one defect for a worse one, so the passband is pinned where the
3946 /// instrument actually plays.
3947 #[test]
3948 fn dc_blocker_keeps_the_bass() {
3949 let tree = PatchTree {
3950 amp: AmpEnv {
3951 attack: 0.1,
3952 decay: 0.3,
3953 sustain: 0.3, // well under the limiter, so gains are readable
3954 release: 0.3,
3955 },
3956 // A ladder, so the patch actually receives a blocker; a sine
3957 // through it stays a sine, so output level reads as filter gain.
3958 root: AudioNode::Filter {
3959 uid: Uid::NEW,
3960 kind: FilterKind::Ladder,
3961 cutoff: 1.0,
3962 resonance: 0.0,
3963 mod_depth: 0.0,
3964 input: Box::new(AudioNode::Vco {
3965 uid: Uid::NEW,
3966 wave: Waveform::Sine,
3967 octave: 0,
3968 detune: 0.5,
3969 mod_depth: 0.0,
3970 modulation: ModNode::None,
3971 }),
3972 modulation: ModNode::None,
3973 },
3974 };
3975 let at = |voct: f64| {
3976 let mut v = compile(&tree, SR).expect("compiles");
3977 let out = hold(&mut v, voct, 44_100);
3978 rms(&out[22_050..])
3979 };
3980 let reference = at(0.0); // C4
3981 // C2 (65 Hz) within 1 dB, C3 within 0.5 dB.
3982 assert!(at(-2.0) > reference * 0.89, "C2 lost more than 1 dB");
3983 assert!(at(-1.0) > reference * 0.945, "C3 lost more than 0.5 dB");
3984 }
3985
3986 // ---------------------------------------------------------------------
3987 // Wave 2C: modulation as a sort.
3988 //
3989 // Every test below measures the *scale* of a control rather than the
3990 // presence of a cable, because four palette waves running the presence
3991 // check let four dead controls through.
3992 // ---------------------------------------------------------------------
3993
3994 /// A sine oscillator whose **pitch** is driven by `m`.
3995 ///
3996 /// Pitch is the measuring destination for the whole of this wave: it is
3997 /// the only mod target whose value can be read straight out of the
3998 /// rendered audio, by counting zero crossings, without a spectral estimate
3999 /// in the way.
4000 fn pitch_modulated(m: ModNode, mod_depth: f64) -> PatchTree {
4001 sustained(AudioNode::Vco {
4002 uid: Uid::NEW,
4003 wave: Waveform::Sine,
4004 octave: 0,
4005 detune: 0.5,
4006 mod_depth,
4007 modulation: m,
4008 })
4009 }
4010
4011 /// A slow triangle LFO: 0.125 Hz on quiver's `0.01·3000^cv` map, so one
4012 /// cycle takes 8 s and a 4 s render sweeps up and back down once.
4013 const SLOW_LFO_RATE: f64 = 0.3155;
4014
4015 /// Semitone offsets of each window relative to the lowest, read off the
4016 /// zero-crossing count. At C6 a 100 ms window holds ~209 crossings, so the
4017 /// ±1 count quantization is 0.08 of a semitone — fine enough to say
4018 /// whether a pitch landed on the 12-TET grid.
4019 fn semitone_track(out: &[(f64, f64)], windows: usize) -> Vec<f64> {
4020 let counts = crossings_per_window(out, windows);
4021 let lo = *counts.iter().min().expect("windows") as f64;
4022 counts
4023 .iter()
4024 .map(|c| 12.0 * (*c as f64 / lo.max(1.0)).log2())
4025 .collect()
4026 }
4027
4028 /// Transitions of more than a semitone between adjacent windows — how a
4029 /// gate arriving on a pitch cable reads.
4030 fn gate_edges(out: &[(f64, f64)], windows: usize) -> usize {
4031 let t = semitone_track(out, windows);
4032 t.windows(2).filter(|w| (w[0] - w[1]).abs() > 1.0).count()
4033 }
4034
4035 /// The quantizer snaps a modulator onto the **12-TET grid**, and the grid
4036 /// is sized so a fully-modulated pitch cable lands on whole semitones.
4037 ///
4038 /// This is the one module in the wave whose musical claim is arithmetic
4039 /// rather than taste, and it is arithmetic in three stages that multiply.
4040 /// `ScaleQuantizer` snaps in V/Oct on a fixed 1/12 V grid, so handed a
4041 /// modulator at its native ±5 V it emits 121 steps across ±60 semitones —
4042 /// a "quantizer" whose output is finer than the ear and, after the mod
4043 /// cable's own 0.1 gain, finer than a tenth of a semitone.
4044 /// [`QUANTIZE_IN_LEVEL`] scales the input into a ±6 semitone window and
4045 /// its inverse scales the output back out, so the *grid* is resized and
4046 /// the cable's gain is not.
4047 ///
4048 /// Both halves are asserted: that the modulation still spans its full
4049 /// ±0.5 octave (the round trip is unity, not an attenuation), and that
4050 /// what it visits on the way is a staircase on the semitone grid rather
4051 /// than a ramp.
4052 #[test]
4053 fn the_quantizer_lands_a_pitch_cable_on_whole_semitones() {
4054 let lfo = || ModNode::Lfo {
4055 uid: Uid::NEW,
4056 wave: Waveform::Triangle,
4057 rate: SLOW_LFO_RATE,
4058 };
4059 let track = |m: ModNode| {
4060 let mut v = compile(&pitch_modulated(m, 1.0), SR).expect("compiles");
4061 // Two octaves up, for the crossing-count resolution the grid
4062 // check needs.
4063 let out = hold(&mut v, 2.0, (SR * 4.0) as usize);
4064 semitone_track(&out, 40)
4065 };
4066 let quantized = track(ModNode::Op {
4067 uid: Uid::NEW,
4068 kind: ModOp::Quantize,
4069 p0: 0.0, // root C
4070 p1: 0.0, // chromatic — every semitone is reachable
4071 input: Box::new(lfo()),
4072 });
4073 let plain = track(lfo());
4074
4075 // 1. The round trip is transparent: a triangle sweeping the full
4076 // ±0.5 octave still spans an octave after being quantized.
4077 let span = |t: &[f64]| t.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
4078 let (qs, ps) = (span(&quantized), span(&plain));
4079 assert!(
4080 ps > 8.0,
4081 "the control sweep is too small to measure: {ps:.2}"
4082 );
4083 assert!(
4084 (qs - ps).abs() < 1.5,
4085 "quantizing changed the modulation's range: {qs:.2} vs {ps:.2} \
4086 semitones — the input and output levels do not cancel"
4087 );
4088
4089 // 2. It is a staircase. Adjacent windows land on the *same* pitch far
4090 // more often than a continuous sweep ever does…
4091 let plateaus = |t: &[f64]| {
4092 t.windows(2).filter(|w| (w[0] - w[1]).abs() < 0.05).count() as f64
4093 / (t.len() - 1) as f64
4094 };
4095 let (qp, pp) = (plateaus(&quantized), plateaus(&plain));
4096 assert!(
4097 qp > 0.3 && qp > pp + 0.15,
4098 "quantized pitch is not stepped: {:.0}% of windows held, against \
4099 {:.0}% for the unquantized control",
4100 100.0 * qp,
4101 100.0 * pp
4102 );
4103 // …and every pitch it holds is on the grid, which is the claim about
4104 // the grid's *size* rather than about its existence.
4105 let off_grid = |t: &[f64]| {
4106 let mut d: Vec<f64> = t.iter().map(|s| (s - s.round()).abs()).collect();
4107 d.sort_by(f64::total_cmp);
4108 d[d.len() / 2]
4109 };
4110 let (qg, pg) = (off_grid(&quantized), off_grid(&plain));
4111 assert!(
4112 qg < 0.15 && qg < pg * 0.7,
4113 "quantized pitch sits {qg:.3} semitones off the grid (control \
4114 {pg:.3}) — QUANTIZE_IN_LEVEL is not sizing the grid to the \
4115 destination"
4116 );
4117 }
4118
4119 /// A euclidean pattern's clock spans the tempo the port actually offers.
4120 ///
4121 /// `Clock`'s `bpm` port is `CvUnipolar`, which in quiver is **0–10 V and
4122 /// not 0–1**: `cv_to_bpm` is `20·15^(cv/10)`, so passing the raw knob
4123 /// through would have given 20 BPM at one end of the control and 21.4 at
4124 /// the other — a rate knob with a 7% range. `ParamMap::ClockRate` spans
4125 /// the port, and the ratio asserted here is most of the 15× the map can
4126 /// produce.
4127 #[test]
4128 fn the_euclid_clock_spans_the_ports_own_tempo_range() {
4129 let edges = |rate: f64| {
4130 let m = ModNode::Euclid {
4131 uid: Uid::NEW,
4132 rate,
4133 steps: 0.3,
4134 pulses: 0.6,
4135 };
4136 let mut v = compile(&pitch_modulated(m, 1.0), SR).expect("compiles");
4137 let out = hold(&mut v, 2.0, (SR * 8.0) as usize);
4138 gate_edges(&out, 400)
4139 };
4140 let (slow, fast) = (edges(0.0), edges(1.0));
4141 assert!(slow > 0, "the slowest clock never fired at all");
4142 assert!(
4143 fast as f64 / slow as f64 > 5.0,
4144 "the euclid rate knob spans only {:.1}× ({slow} to {fast} edges in \
4145 8 s) — the bpm port is 0–10 V, not 0–1",
4146 fast as f64 / slow as f64
4147 );
4148 }
4149
4150 /// Neither end of the euclid's `pulses` knob is a dead cable, at either
4151 /// end of its `steps` knob.
4152 ///
4153 /// quiver takes `pulses = (cv · steps) as usize`, so a raw knob emits
4154 /// nothing at all below `1/steps` — about one uniform draw in seven — and
4155 /// a solid gate at exactly 1.0. `ParamMap::EuclidSteps` gives up the two
4156 /// shortest patterns so that one CV floor can serve every step count, and
4157 /// this checks all four corners.
4158 #[test]
4159 fn every_corner_of_the_euclid_knobs_still_makes_a_rhythm() {
4160 let edges = |steps: f64, pulses: f64| {
4161 let m = ModNode::Euclid {
4162 uid: Uid::NEW,
4163 rate: 1.0,
4164 steps,
4165 pulses,
4166 };
4167 let mut v = compile(&pitch_modulated(m, 1.0), SR).expect("compiles");
4168 let out = hold(&mut v, 2.0, (SR * 8.0) as usize);
4169 gate_edges(&out, 400)
4170 };
4171 for (steps, pulses) in [(0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0)] {
4172 assert!(
4173 edges(steps, pulses) > 0,
4174 "euclid at steps {steps} / pulses {pulses} emits a constant"
4175 );
4176 }
4177 }
4178
4179 /// The switch hears **both** of its branches.
4180 ///
4181 /// quiver's `VcSwitch` needs a third input to choose with and `Pair` has
4182 /// only two to give. The contract proposed the voice gate; that is a
4183 /// control that reviews as correct and does nothing, because the gate is
4184 /// high for the whole of every note and low only between notes when the
4185 /// VCA is shut — so `b` would win every sample anybody hears and `a` would
4186 /// be a module on the rack that is never once audible. Wiring `b` as its
4187 /// own control makes the module "punch `b` in over `a`", and the way to
4188 /// prove `a` is alive is to change only `a`.
4189 #[test]
4190 fn the_switch_is_not_stuck_on_one_branch() {
4191 let render = |a_rate: f64| {
4192 let m = ModNode::Pair {
4193 uid: Uid::NEW,
4194 kind: PairOp::Switch,
4195 a: Box::new(ModNode::Lfo {
4196 uid: Uid::NEW,
4197 wave: Waveform::Triangle,
4198 rate: a_rate,
4199 }),
4200 b: Box::new(ModNode::Euclid {
4201 uid: Uid::NEW,
4202 rate: 0.7,
4203 steps: 0.4,
4204 pulses: 0.5,
4205 }),
4206 };
4207 let mut v = compile(&pitch_modulated(m, 1.0), SR).expect("compiles");
4208 let out = hold(&mut v, 2.0, (SR * 2.0) as usize);
4209 crossings_per_window(&out, 40)
4210 };
4211 let (slow, fast) = (render(SLOW_LFO_RATE), render(0.6));
4212 let moved = slow
4213 .iter()
4214 .zip(&fast)
4215 .filter(|(a, b)| (**a as i64 - **b as i64).abs() > 4)
4216 .count();
4217 assert!(
4218 moved * 4 > slow.len(),
4219 "changing only the switch's `a` branch moved {moved} of {} windows \
4220 — that branch is never selected",
4221 slow.len()
4222 );
4223 }
4224
4225 /// The slew limiter's useful glide times are on the plate rather than
4226 /// crammed into its first quarter, and its top is a freeze rather than a
4227 /// third of one.
4228 ///
4229 /// quiver's own map is `0.001 + cv²·10` seconds — already square-law — so
4230 /// a raw knob puts every glide under 1.5 s below position 0.39 and spends
4231 /// the remaining three fifths of its travel holding the modulator still.
4232 /// `ParamMap::SlewTime` is `0.4·x`, which is measured here at three
4233 /// points: a quarter turn already smooths, and the top of the knob has
4234 /// nearly stopped the modulator.
4235 #[test]
4236 fn the_slew_knob_spends_its_travel_on_audible_glide_times() {
4237 // Movement per window: how far the pitch jumps between adjacent
4238 // 50 ms windows. A stepped source jumps; a slewed one ramps.
4239 // The *largest* jump between adjacent 50 ms windows, which is the
4240 // step height a slew limiter exists to soften. A mean would be the
4241 // wrong statistic: slewing spreads one big jump over several windows,
4242 // so it moves the mean up while moving the maximum down.
4243 let jump = |m: ModNode| {
4244 let mut v = compile(&pitch_modulated(m, 1.0), SR).expect("compiles");
4245 let out = hold(&mut v, 2.0, (SR * 4.0) as usize);
4246 // 25 ms windows: fine enough that a 50 ms glide — a quarter turn
4247 // of the knob — spreads its step across more than one of them.
4248 let t = semitone_track(&out, 160);
4249 t.windows(2)
4250 .map(|w| (w[0] - w[1]).abs())
4251 .fold(0.0f64, f64::max)
4252 };
4253 let stepped = || ModNode::Euclid {
4254 uid: Uid::NEW,
4255 rate: 0.75,
4256 steps: 0.3,
4257 pulses: 0.5,
4258 };
4259 let slewed = |t: f64| ModNode::Op {
4260 uid: Uid::NEW,
4261 kind: ModOp::Slew,
4262 p0: t,
4263 p1: t,
4264 input: Box::new(stepped()),
4265 };
4266 let bare = jump(stepped());
4267 assert!(bare > 3.0, "the control source barely moves: {bare:.3}");
4268 let quarter = jump(slewed(0.25));
4269 assert!(
4270 quarter < bare * 0.75,
4271 "a quarter turn of slew changed the step height from {bare:.3} to \
4272 {quarter:.3} — the useful glide times are not on the plate"
4273 );
4274 let full = jump(slewed(1.0));
4275 assert!(
4276 full < quarter * 0.4,
4277 "full slew ({full:.3}) is not much slower than a quarter turn \
4278 ({quarter:.3})"
4279 );
4280 }
4281
4282 /// The rectifier picks an output **port**, and the three it offers are
4283 /// three different signals.
4284 ///
4285 /// quiver's `Rectifier` has no `mode` input at all — it publishes `full`,
4286 /// `half_pos` and `half_neg` simultaneously — so `rmode` chooses a cable
4287 /// at compile time rather than writing a CV, which is also why it is the
4288 /// one 2C knob with no live handle.
4289 #[test]
4290 fn the_three_rectifier_modes_are_three_different_signals() {
4291 let track = |mode: f64| {
4292 let m = ModNode::Op {
4293 uid: Uid::NEW,
4294 kind: ModOp::Rectify,
4295 p0: mode,
4296 p1: 0.0,
4297 input: Box::new(ModNode::Lfo {
4298 uid: Uid::NEW,
4299 wave: Waveform::Triangle,
4300 rate: SLOW_LFO_RATE,
4301 }),
4302 };
4303 let mut v = compile(&pitch_modulated(m, 1.0), SR).expect("compiles");
4304 let out = hold(&mut v, 2.0, (SR * 4.0) as usize);
4305 crossings_per_window(&out, 40)
4306 };
4307 let differs = |a: &[usize], b: &[usize]| {
4308 a.iter()
4309 .zip(b)
4310 .filter(|(x, y)| (**x as i64 - **y as i64).abs() > 4)
4311 .count()
4312 };
4313 // Cell centres of a three-way split: full / positive / negative.
4314 let (full, pos, neg) = (track(0.1), track(0.5), track(0.9));
4315 assert!(
4316 differs(&full, &pos) * 5 > full.len(),
4317 "full-wave and positive-half rectification render the same"
4318 );
4319 assert!(
4320 differs(&pos, &neg) * 5 > pos.len(),
4321 "the two half-wave modes render the same"
4322 );
4323 // A full-wave rectified triangle is a triangle at twice the rate and
4324 // never goes below the base note, so the *lowest* pitch it visits is
4325 // the unmodulated one — which is what "folded into one polarity"
4326 // means and what distinguishes it from the bare LFO.
4327 let bare = {
4328 let mut v = compile(
4329 &pitch_modulated(
4330 ModNode::Lfo {
4331 uid: Uid::NEW,
4332 wave: Waveform::Triangle,
4333 rate: SLOW_LFO_RATE,
4334 },
4335 1.0,
4336 ),
4337 SR,
4338 )
4339 .expect("compiles");
4340 let out = hold(&mut v, 2.0, (SR * 4.0) as usize);
4341 crossings_per_window(&out, 40)
4342 };
4343 assert!(
4344 differs(&full, &bare) * 5 > full.len(),
4345 "rectifying a bipolar LFO changed nothing"
4346 );
4347 }
4348
4349 /// A modulation chain compiles as a chain: two processors over a leaf all
4350 /// reach the destination, and every knob in it is a real trace address.
4351 ///
4352 /// This is the shape the whole wave exists for — `s&h rand → quantize →
4353 /// slew` — and the thing that would break silently is the recursion
4354 /// dropping a level and wiring the leaf straight to the attenuverter.
4355 #[test]
4356 fn a_two_deep_mod_chain_reaches_the_destination_through_every_stage() {
4357 let chain = ModNode::Op {
4358 uid: Uid::NEW,
4359 kind: ModOp::Slew,
4360 p0: 0.3,
4361 p1: 0.3,
4362 input: Box::new(ModNode::Op {
4363 uid: Uid::NEW,
4364 kind: ModOp::Quantize,
4365 p0: 0.0,
4366 p1: 2.5 / 7.0, // minor
4367 input: Box::new(ModNode::Rand {
4368 uid: Uid::NEW,
4369 rate: 0.6,
4370 glide: 0.0,
4371 }),
4372 }),
4373 };
4374 // Two processors over a leaf — the deepest term the default prior can
4375 // draw, so this is the shape the search actually has to survive and
4376 // not a hand-built extreme.
4377 assert_eq!(
4378 chain.depth(),
4379 1 + crate::PatchGrammarPrior::default().max_mod_depth
4380 );
4381 let tree = pitch_modulated(chain, 1.0);
4382 let v = compile(&tree, SR).expect("compiles");
4383 // Each stage's knobs live under its own key, one level deeper than
4384 // its parent's — `node/m` for the slew, `node/m/0` for the quantizer,
4385 // `node/m/0/0` for the S&H.
4386 for addr in [
4387 "node#mdepth",
4388 "node/m#rise",
4389 "node/m#fall",
4390 "node/m/0#qroot",
4391 "node/m/0#qscale",
4392 "node/m/0/0#rate",
4393 "node/m/0/0#glide",
4394 ] {
4395 assert!(
4396 v.params.contains_key(addr),
4397 "chain stage `{addr}` has no live handle"
4398 );
4399 }
4400 // And the top stage is what reaches the oscillator: a recursion that
4401 // dropped a level would wire the leaf straight to the attenuverter
4402 // and leave the hard steps on the pitch.
4403 //
4404 // Measured on the same two-deep shape with a **euclidean** leaf
4405 // rather than the S&H one above, because the comparison has to be
4406 // controlled: two patches containing a noise generator are two
4407 // different random signals, and the difference between them would
4408 // measure the noise rather than the slew.
4409 let jump = |m: ModNode| {
4410 let mut v = compile(&pitch_modulated(m, 1.0), SR).expect("compiles");
4411 let out = hold(&mut v, 2.0, (SR * 4.0) as usize);
4412 let t = semitone_track(&out, 160);
4413 t.windows(2)
4414 .map(|w| (w[0] - w[1]).abs())
4415 .fold(0.0f64, f64::max)
4416 };
4417 let inner = || ModNode::Op {
4418 uid: Uid::NEW,
4419 kind: ModOp::Quantize,
4420 p0: 0.0,
4421 p1: 2.5 / 7.0, // minor
4422 input: Box::new(ModNode::Euclid {
4423 uid: Uid::NEW,
4424 rate: 0.75,
4425 steps: 0.3,
4426 pulses: 0.5,
4427 }),
4428 };
4429 let raw = jump(inner());
4430 let slewed = jump(ModNode::Op {
4431 uid: Uid::NEW,
4432 kind: ModOp::Slew,
4433 p0: 1.0,
4434 p1: 1.0,
4435 input: Box::new(inner()),
4436 });
4437 assert!(raw > 3.0, "the two-stage control barely moves: {raw:.3}");
4438 assert!(
4439 slewed < raw * 0.5,
4440 "the slew stage did not reach the pitch: {slewed:.3} against \
4441 {raw:.3} without it"
4442 );
4443 }
4444
4445 /// FNV-1a over the *bits* of every sample a voice renders: 3000 frames
4446 /// with the gate high, then 1096 with it low, at a pitch of `7/12`.
4447 ///
4448 /// The pitch is a tempered fifth on purpose. It is not a binary fraction,
4449 /// so `pitch + (oct + detune)` and `(pitch + oct) + detune` disagree in
4450 /// the last bit — any change that reassociates the pitch sum shows up here
4451 /// rather than in someone's ears.
4452 fn render_fingerprint(mut v: CompiledVoice) -> u64 {
4453 v.pitch.set(7.0 / 12.0);
4454 v.gate.set(5.0);
4455 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
4456 for i in 0..4096u32 {
4457 if i == 3000 {
4458 v.gate.set(0.0);
4459 }
4460 let (l, r) = v.patch.tick();
4461 for x in [l, r] {
4462 h ^= x.to_bits();
4463 h = h.wrapping_mul(0x0000_0100_0000_01b3);
4464 }
4465 }
4466 h
4467 }
4468
4469 /// Put a compiled voice back the way the compiler built it *before*
4470 /// `table` and `oct` were live: pull the two new [`ExternalInput`] cables
4471 /// out and write their values back where they used to be baked.
4472 ///
4473 /// This is what makes the regression test a real counterfactual rather
4474 /// than a captured constant. A hard-coded golden vector would pin the
4475 /// render to whichever machine captured it — quiver's transcendentals are
4476 /// a pure-Rust libm, but nothing in this repo guarantees that for every
4477 /// dependency on every target, and a flaky CI golden teaches people to
4478 /// re-baseline it, which is precisely the thing it exists to prevent.
4479 /// Rendering both graphs in the same process compares the arithmetic
4480 /// itself.
4481 ///
4482 /// (The stronger check was also run once, by hand, across the two real
4483 /// builds: 16 prior draws plus this corpus, hashed before and after the
4484 /// change, identical — which additionally covers what this cannot, that
4485 /// two extra nodes per source do not reorder the execution of the
4486 /// `NoiseGenerator`s that draw from quiver's shared RNG.)
4487 ///
4488 /// Returns how many cables it removed, so a test cannot quietly pass by
4489 /// comparing a patch with nothing in it.
4490 fn unlive_table_and_oct(v: &mut CompiledVoice) -> usize {
4491 let names: Vec<String> = v.patch.nodes().map(|(_, n, _)| n.to_string()).collect();
4492 let mut pulled = 0;
4493 for name in names {
4494 if let Some(key) = name.strip_suffix(":table!") {
4495 let src = v.patch.get_handle_by_name(&name).expect("just listed");
4496 let wt = v
4497 .patch
4498 .get_handle_by_name(&format!("{key}:wavetable"))
4499 .expect("a `table!` knob belongs to a wavetable");
4500 // The atomic holds exactly what `constant` used to pin.
4501 let baked = v.params[&format!("{key}#table")].value.get();
4502 v.patch
4503 .disconnect_ports(src.out("out"), wt.in_("table"))
4504 .expect("the cable this stage added");
4505 assert!(v.patch.set_param_by_id(wt.id(), "table", baked));
4506 pulled += 1;
4507 } else if let Some(key) = name.strip_suffix(":oct!") {
4508 let src = v.patch.get_handle_by_name(&name).expect("just listed");
4509 let off = v
4510 .patch
4511 .get_handle_by_name(&format!("{key}:pitch"))
4512 .expect("an `oct!` knob belongs to a pitch offset");
4513 // Nothing to bake back: the trim is zero at compile time and
4514 // the octave never left the `Offset`. That assertion *is* the
4515 // bit-exactness argument, so make it out loud.
4516 assert_eq!(
4517 v.params[&format!("{key}#oct")].value.get(),
4518 0.0,
4519 "{key}: a freshly compiled octave trim must be exactly zero"
4520 );
4521 v.patch
4522 .disconnect_ports(src.out("out"), off.in_("in"))
4523 .expect("the cable this stage added");
4524 pulled += 1;
4525 }
4526 }
4527 v.patch.compile().expect("recompiles without the cables");
4528 pulled
4529 }
4530
4531 /// Every source that has an octave, at a non-zero octave, with something
4532 /// on its modulation slot — the exact shape the pitch sum is fragile in,
4533 /// since a mod cable joins the same gather. Plus one wavetable per table
4534 /// shape, `table` being the other constant that became a cable.
4535 fn pitch_and_table_corpus() -> Vec<PatchTree> {
4536 let lfo = || ModNode::Lfo {
4537 uid: Uid::NEW,
4538 wave: Waveform::Triangle,
4539 rate: 0.4,
4540 };
4541 let mut out = vec![
4542 sustained(AudioNode::Vco {
4543 uid: Uid::NEW,
4544 wave: Waveform::Saw,
4545 octave: -2,
4546 detune: 0.31,
4547 mod_depth: 0.6,
4548 modulation: lfo(),
4549 }),
4550 sustained(AudioNode::Vco {
4551 uid: Uid::NEW,
4552 wave: Waveform::Square,
4553 octave: 2,
4554 detune: 0.83,
4555 mod_depth: 0.0,
4556 modulation: ModNode::None,
4557 }),
4558 sustained(AudioNode::Supersaw {
4559 uid: Uid::NEW,
4560 octave: -1,
4561 detune: 0.4,
4562 mix: 0.6,
4563 mod_depth: 0.45,
4564 modulation: lfo(),
4565 }),
4566 sustained(AudioNode::Pluck {
4567 uid: Uid::NEW,
4568 octave: 1,
4569 damping: 0.6,
4570 brightness: 0.4,
4571 mod_depth: 0.3,
4572 modulation: lfo(),
4573 }),
4574 sustained(AudioNode::Formant {
4575 uid: Uid::NEW,
4576 vowel: 0.3,
4577 shift: 0.5,
4578 octave: -1,
4579 mod_depth: 0.2,
4580 modulation: lfo(),
4581 }),
4582 ];
4583 for (i, table) in TableShape::ALL.iter().enumerate() {
4584 out.push(sustained(AudioNode::Wavetable {
4585 uid: Uid::NEW,
4586 table: *table,
4587 octave: (i as i8 % 5) - 2,
4588 morph: 0.37,
4589 mod_depth: if i % 2 == 0 { 0.0 } else { 0.55 },
4590 modulation: if i % 2 == 0 { ModNode::None } else { lfo() },
4591 }));
4592 }
4593 out
4594 }
4595
4596 /// **The standing rule, in CI.** Making `table` and `oct` live must not
4597 /// move one sample of any patch that already exists — a rendered-audio
4598 /// change invalidates the bank's featurisation and the taste posterior,
4599 /// and needs a measured evolution revalidation, not a green `make check`.
4600 ///
4601 /// So the claim is checked rather than asserted: every patch is rendered
4602 /// twice in the same process, once through the live cables and once
4603 /// through [`unlive_table_and_oct`], and every bit of every sample must
4604 /// agree. Sixteen prior draws for breadth, then a corpus built to hit
4605 /// every octave-bearing source and every wavetable shape.
4606 #[test]
4607 fn table_and_oct_going_live_moved_no_sample() {
4608 use crate::prior::PatchGrammarPrior;
4609 use rand::rngs::StdRng;
4610 use rand::SeedableRng;
4611
4612 let prior = PatchGrammarPrior::default();
4613 let mut rng = StdRng::seed_from_u64(0x9E37_79B9_7F4A_7C15);
4614 let trees: Vec<PatchTree> = (0..16)
4615 .map(|_| prior.sample_with_rng(&mut rng))
4616 .chain(pitch_and_table_corpus())
4617 .collect();
4618
4619 let mut pulled_total = 0;
4620 for (i, tree) in trees.iter().enumerate() {
4621 // Noise draws from quiver's thread-local RNG, so both renders have
4622 // to start from the same state or the comparison means nothing.
4623 quiver::rng::seed(0x60_1DE5);
4624 let live = render_fingerprint(compile(tree, SR).expect("compiles"));
4625
4626 quiver::rng::seed(0x60_1DE5);
4627 let mut baked = compile(tree, SR).expect("compiles");
4628 pulled_total += unlive_table_and_oct(&mut baked);
4629 let baked = render_fingerprint(baked);
4630
4631 assert_eq!(
4632 format!("{live:#018x}"),
4633 format!("{baked:#018x}"),
4634 "patch {i} renders differently with `table`/`oct` live:\n{}",
4635 tree.to_sexpr()
4636 );
4637 }
4638 assert!(
4639 pulled_total >= trees.len(),
4640 "only {pulled_total} live cables across {} patches — the corpus is \
4641 not exercising the sites this test is about",
4642 trees.len()
4643 );
4644 }
4645}