tono_core/dsl/node.rs
1//! The synthesis-graph [`Node`] enum — every source, combinator, and
2//! processor in the DSL — with its per-family serde defaults co-located.
3
4use super::{
5 Adsr, Bus, DriveShape, KitStyle, Mode, NoiseColor, SeqNote, SeqWave, SuperWave, TempoPoint,
6 Track, Value, WavetableKind, default_gain,
7};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11// Serde `default = "..."` requires free functions. Values with non-obvious
12// origins: q 0.707 is Butterworth (maximally flat).
13fn default_duty() -> Value {
14 Value::Const(0.5)
15}
16// Wavetable morph starts at the first (darkest) sub-wave.
17fn default_wavetable_position() -> Value {
18 Value::Const(0.0)
19}
20fn default_q() -> f32 {
21 0.707
22}
23fn default_steps_per_beat() -> u32 {
24 4
25}
26// Shared by chorus / flanger / phaser ("mod fx").
27fn default_mod_depth() -> f32 {
28 0.5
29}
30fn default_mod_mix() -> f32 {
31 0.5
32}
33fn default_chorus_rate() -> f32 {
34 1.5
35}
36// Classic amp/tremolo rate.
37fn default_tremolo_rate() -> f32 {
38 6.0
39}
40fn default_flanger_rate() -> f32 {
41 0.25
42}
43fn default_flanger_feedback() -> f32 {
44 0.5
45}
46fn default_phaser_rate() -> f32 {
47 0.4
48}
49fn default_phaser_feedback() -> f32 {
50 0.3
51}
52fn default_comp_attack() -> f32 {
53 0.005
54}
55fn default_comp_release() -> f32 {
56 0.08
57}
58fn default_voices() -> u32 {
59 7
60}
61fn default_detune() -> f32 {
62 15.0
63}
64// Seq instrument defaults: ratio 1 + decaying index ≈ an FM piano strike;
65// pluck decay 0.996 rings ~1 s in the mid register.
66fn default_seq_fm_ratio() -> f32 {
67 1.0
68}
69fn default_seq_fm_index() -> f32 {
70 5.0
71}
72fn default_seq_fm_strike() -> f32 {
73 0.2
74}
75fn default_pluck_decay() -> f32 {
76 0.996
77}
78// Guitar tone stages on the `pluck` voice. All default to identity (0.0), so
79// omitting them renders byte-identically and draws no extra RNG.
80fn default_pluck_body() -> f32 {
81 0.0
82}
83fn default_pluck_pick() -> f32 {
84 0.0
85}
86fn default_pluck_tone() -> f32 {
87 0.0
88}
89// Piano tone knobs (engine-3 additive `piano` voice). Every default reproduces
90// the concert-grand kernel bit-for-bit (x*1.0==x, x/1.0==x, 0.125==1.0/8.0 in
91// f32), so a doc that omits them renders byte-identically. Variants set others.
92fn default_piano_hammer() -> f32 {
93 1.0
94}
95fn default_piano_strike() -> f32 {
96 0.125
97}
98fn default_piano_inharm() -> f32 {
99 1.0
100}
101fn default_piano_detune() -> f32 {
102 1.0
103}
104fn default_piano_decay() -> f32 {
105 1.0
106}
107// Bass tone knobs (the `bass` voice). Every default is the current voice's
108// hard-coded constant, so omitting them renders byte-identically.
109fn default_bass_cutoff() -> f32 {
110 250.0
111}
112fn default_bass_env() -> f32 {
113 700.0
114}
115fn default_bass_env_vel() -> f32 {
116 1100.0
117}
118fn default_bass_decay() -> f32 {
119 0.15
120}
121fn default_bass_click() -> f32 {
122 0.0
123}
124fn default_bass_body() -> f32 {
125 0.7
126}
127fn default_bass_sub() -> f32 {
128 0.45
129}
130fn default_bass_sub_ratio() -> f32 {
131 1.0
132}
133fn default_bass_drive() -> f32 {
134 0.0
135}
136fn default_bass_body_decay() -> f32 {
137 2.0
138}
139fn default_duck_amount() -> f32 {
140 0.8
141}
142fn default_duck_attack() -> f32 {
143 0.005
144}
145fn default_duck_release() -> f32 {
146 0.25
147}
148fn default_modal_mix() -> f32 {
149 1.0
150}
151fn default_impact_hardness() -> f32 {
152 0.5
153}
154fn default_dust_decay() -> f32 {
155 0.02
156}
157fn default_convolve_decay() -> f32 {
158 1.5
159}
160fn default_convolve_damp() -> f32 {
161 0.3
162}
163fn default_convolve_mix() -> f32 {
164 0.35
165}
166fn default_granular_grain_ms() -> f32 {
167 80.0
168}
169fn default_granular_density() -> f32 {
170 25.0
171}
172fn default_granular_pitch() -> f32 {
173 1.0
174}
175fn default_granular_spread() -> f32 {
176 0.3
177}
178fn default_granular_mix() -> f32 {
179 0.5
180}
181
182/// A node in the synthesis graph. Every node evaluates to a mono signal.
183#[non_exhaustive]
184#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
185#[serde(tag = "type", rename_all = "lowercase")]
186pub enum Node {
187 // --- Sources (output audio in [-1, 1]) ---
188 /// Square/pulse wave with variable duty cycle. `duty` may be a modulator
189 /// (e.g. an `lfo`) for PWM — the classic moving chiptune lead.
190 Square {
191 /// Frequency in Hz.
192 freq: Value,
193 /// Fraction of the period spent "high", 0..1 (0.5 = symmetric square).
194 #[serde(default = "default_duty")]
195 duty: Value,
196 },
197 /// Triangle wave.
198 Triangle {
199 /// Frequency in Hz.
200 freq: Value,
201 },
202 /// Sawtooth wave.
203 Sawtooth {
204 /// Frequency in Hz.
205 freq: Value,
206 },
207 /// Sine wave.
208 Sine {
209 /// Frequency in Hz.
210 freq: Value,
211 },
212 /// Noise source (percussion / explosion / texture). White by default;
213 /// `pink` is warmer (−3 dB/oct, good for wind/rumble), `brown` darker still.
214 Noise {
215 /// Spectral colour of the noise.
216 #[serde(default)]
217 color: NoiseColor,
218 },
219 /// Two-operator FM: a carrier at `freq` is phase-modulated by an operator
220 /// at `freq * ratio` with modulation `index`. Bells, e-piano, metallic
221 /// basses. Slide `index` down for a struck/plucked attack.
222 Fm {
223 /// Carrier frequency in Hz.
224 freq: Value,
225 /// Modulator frequency as a multiple of the carrier (e.g. 2.0, 3.5).
226 ratio: f32,
227 /// Modulation index (depth). Higher ⇒ brighter / more sidebands.
228 index: Value,
229 },
230 /// Unison "super" oscillator: `voices` detuned copies of a band-limited
231 /// saw/square, summed for a fat, wide lead or pad (the supersaw). Detune is
232 /// spread symmetrically across `detune_cents`.
233 Super {
234 /// Oscillator shape for every voice.
235 #[serde(default)]
236 wave: SuperWave,
237 /// Centre frequency in Hz (modulatable).
238 freq: Value,
239 /// Number of detuned voices (1..=16).
240 #[serde(default = "default_voices")]
241 voices: u32,
242 /// Total detune spread in cents across all voices.
243 #[serde(default = "default_detune")]
244 detune_cents: f32,
245 },
246 /// Morphing wavetable oscillator: `position` (0..1) sweeps across an
247 /// ordered set of built-in, single-cycle tables (see [`WavetableKind`]),
248 /// crossfading between adjacent sub-waves — the classic wavetable sweep.
249 /// Modulate `position` (an `lfo` or `env`) for the signature moving timbre:
250 /// a vocal morph on `formant`, a brightness swell on `harmonics`. Tables
251 /// are generated deterministically at build time (additive, band-limited to
252 /// 32 partials — darker sub-waves use fewer), so no table files are needed;
253 /// very high pitches at bright positions can fold.
254 Wavetable {
255 /// Which table set to morph across.
256 #[serde(default)]
257 wave: WavetableKind,
258 /// Frequency in Hz (modulatable).
259 freq: Value,
260 /// Morph position across the sub-waves, 0..1 (modulatable).
261 #[serde(default = "default_wavetable_position")]
262 position: Value,
263 },
264 /// A note sequencer: plays `notes` on a tempo grid, each with its own pitch,
265 /// length, and a shared per-note ADSR. This is how you write real melodies,
266 /// basslines, and drum patterns (rests = gaps between notes).
267 Seq {
268 /// Tempo in beats per minute.
269 bpm: f32,
270 /// Tempo changes on the beat grid at exact rational positions,
271 /// applied segment-wise (ADR 0002): a note's start and end convert to
272 /// seconds through the segments in f64 and land on frames with halves
273 /// rounded away from zero. Empty = the constant-tempo `bpm` behavior
274 /// — the only behavior documents had before this field existed, so
275 /// they render byte-identically. The first point must sit at beat 0.
276 /// Not supported on the `sampler` wave (validation rejects it).
277 #[serde(default, skip_serializing_if = "Vec::is_empty")]
278 tempo_map: Vec<TempoPoint>,
279 /// Grid resolution: steps per beat (4 = sixteenth notes).
280 #[serde(default = "default_steps_per_beat")]
281 steps_per_beat: u32,
282 /// Instrument used for every note.
283 wave: SeqWave,
284 /// Duty cycle when `wave` is `square` (may be modulated for PWM).
285 #[serde(default = "default_duty")]
286 duty: Value,
287 /// FM voice knobs (`wave: "fm"`), flattened onto the node.
288 #[serde(flatten)]
289 fm: FmKnobs,
290 /// Plucked-string knobs (`wave: "pluck"`), flattened onto the node.
291 #[serde(flatten)]
292 pluck: PluckKnobs,
293 /// Piano tone knobs (`wave: "piano"`, engine ≥ 3), flattened onto the node.
294 #[serde(flatten)]
295 piano: PianoKnobs,
296 /// Drum-kit voicing when `wave` is `kit`. Omitted ⇒ `classic` (the
297 /// original kit, byte-identical); `acoustic`/`electronic`/`808` are
298 /// alternate synthesized kits.
299 #[serde(default)]
300 kit: KitStyle,
301 /// Bass tone knobs (`wave: "bass"`), flattened onto the node.
302 #[serde(flatten)]
303 bass: BassKnobs,
304 /// SoundFont sampler settings (`wave: "sampler"`), flattened onto the node.
305 #[serde(flatten)]
306 sf2: Sf2Knobs,
307 /// Swing, 0..1: every off-beat grid step is delayed by this fraction
308 /// of a step (0 = straight, ~0.55 = classic shuffle). Off-beats are
309 /// odd steps, so set `steps_per_beat` to the swung subdivision.
310 #[serde(default)]
311 swing: f32,
312 /// Humanize, 0..1: deterministic per-note timing and velocity jitter
313 /// (from the doc's seed) so repeats stop sounding machine-perfect.
314 /// 0.1–0.25 is a tasteful player; 1 is sloppy.
315 #[serde(default)]
316 humanize: f32,
317 /// Per-note amplitude envelope.
318 env: Adsr,
319 /// The notes to play.
320 notes: Vec<SeqNote>,
321 },
322
323 /// Impact exciter: a short excitation burst that models the contact force
324 /// of a strike — a single raised-cosine force pulse whose width is set by
325 /// `hardness` (hard = brief, bright click; soft = wider, duller thud),
326 /// scaled by `velocity`. On its own it is a faint tick; its job is to
327 /// *excite* a resonant body — put it before a `modal` bank (or any
328 /// resonant filter) in a `chain`: `chain[ impact, modal ]` is a struck
329 /// object. The harder the strike, the more high modes it lights up.
330 Impact {
331 /// Strike hardness, 0..1 (1 = hardest / brightest / shortest contact).
332 #[serde(default = "default_impact_hardness")]
333 hardness: f32,
334 /// Strike velocity / level, 0..1.
335 #[serde(default = "default_gain")]
336 velocity: f32,
337 },
338 /// Sparse stochastic impulses — a Poisson click train. `density` events per
339 /// second fire at random times with random ± amplitude, each decaying over
340 /// `decay` seconds (0 = bare single-sample impulses). The grain generator
341 /// behind crackle textures: fire, rain, geiger ticks, sparks, debris. Feed
342 /// it through a `bandpass`/`highpass` (for tone) or a `modal` (for pitched
343 /// debris). Its randomness draws from the layer's deterministic stream, so
344 /// like `noise` it is edit-stable within its own mixer layer.
345 Dust {
346 /// Mean events per second.
347 density: f32,
348 /// Per-grain decay time in seconds (0 = single-sample impulses).
349 #[serde(default = "default_dust_decay")]
350 decay: f32,
351 },
352
353 // --- Envelope (outputs a 0..1 control signal) ---
354 /// ADSR amplitude envelope with an sfxr-style `punch` transient.
355 Env {
356 /// Envelope shape.
357 #[serde(flatten)]
358 adsr: Adsr,
359 },
360
361 // --- Combinators ---
362 /// The mixing console — only valid as the document root. Each track is a
363 /// mono graph placed on the stereo stage with its own pan and gain
364 /// (sampler tracks keep their native stereo); `master` is a processor
365 /// chain applied to the stereo bus (compressor glue, reverb — the reverb
366 /// runs with decorrelated left/right tails). This is how multi-
367 /// instrument music gets a real stereo image instead of a mono sum.
368 Tracks {
369 /// The mixer channels.
370 tracks: Vec<Track>,
371 /// Processors applied to the stereo master bus, in order.
372 #[serde(default)]
373 master: Vec<Node>,
374 /// Mix buses: named submixes with their own insert chains, returned
375 /// onto the master bus (see [`Bus`](crate::dsl::Bus)). Empty ⇒ the
376 /// flat mixer documents have always had, byte-identical.
377 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 buses: Vec<Bus>,
379 },
380 /// Sum (layer) all inputs.
381 Mix {
382 /// Branches to add together.
383 inputs: Vec<Node>,
384 },
385 /// Multiply all inputs (typically `source × envelope`).
386 Mul {
387 /// Branches to multiply together.
388 inputs: Vec<Node>,
389 },
390 /// Serial pipe: stage 0 is a source, each later stage processes the prior output.
391 Chain {
392 /// Ordered processing stages.
393 stages: Vec<Node>,
394 },
395
396 // --- Processors (transform the preceding signal in a chain) ---
397 /// Resonant low-pass filter.
398 Lowpass {
399 /// Cutoff frequency in Hz.
400 cutoff: Value,
401 /// Resonance / quality factor.
402 #[serde(default = "default_q")]
403 q: f32,
404 },
405 /// Resonant high-pass filter.
406 Highpass {
407 /// Cutoff frequency in Hz.
408 cutoff: Value,
409 /// Resonance / quality factor.
410 #[serde(default = "default_q")]
411 q: f32,
412 },
413 /// Band-pass filter.
414 Bandpass {
415 /// Center frequency in Hz.
416 cutoff: Value,
417 /// Resonance / quality factor.
418 #[serde(default = "default_q")]
419 q: f32,
420 },
421 /// Notch (band-reject) filter: removes a narrow band around `cutoff` (hum /
422 /// resonance removal).
423 Notch {
424 /// Center frequency in Hz.
425 cutoff: Value,
426 /// Quality factor (higher ⇒ narrower notch).
427 #[serde(default = "default_q")]
428 q: f32,
429 },
430 /// Peaking EQ: boost or cut a band around `cutoff` by `gain_db` (surgical
431 /// tone shaping — act on the brightness/centroid the analyzer reports).
432 Peak {
433 /// Center frequency in Hz.
434 cutoff: Value,
435 /// Quality factor (bandwidth).
436 #[serde(default = "default_q")]
437 q: f32,
438 /// Gain in dB (positive boosts, negative cuts).
439 #[serde(default)]
440 gain_db: f32,
441 },
442 /// Low shelf: boost/cut everything below `cutoff` by `gain_db` (add weight or
443 /// thin out lows).
444 Lowshelf {
445 /// Shelf corner frequency in Hz.
446 cutoff: Value,
447 /// Gain in dB.
448 #[serde(default)]
449 gain_db: f32,
450 },
451 /// High shelf: boost/cut everything above `cutoff` by `gain_db` (air / de-ess).
452 Highshelf {
453 /// Shelf corner frequency in Hz.
454 cutoff: Value,
455 /// Gain in dB.
456 #[serde(default)]
457 gain_db: f32,
458 },
459 /// Scale the signal by a (possibly modulated) factor.
460 Gain {
461 /// Multiplier.
462 amount: Value,
463 },
464 /// Quantize amplitude to `bits` of resolution for crunch.
465 Bitcrush {
466 /// Bit depth, 1..16.
467 bits: u8,
468 },
469 /// Sample-rate reduction by an integer `factor` for lo-fi grit.
470 Downsample {
471 /// Hold each sample for this many samples.
472 factor: u32,
473 },
474 /// Feedback delay (echo / comb).
475 Delay {
476 /// Delay time in seconds.
477 secs: f32,
478 /// Feedback amount, 0..1.
479 #[serde(default)]
480 feedback: f32,
481 },
482 /// Schroeder-style reverb.
483 Reverb {
484 /// Room size, 0..1 (larger ⇒ longer tail).
485 #[serde(default)]
486 room: f32,
487 /// Dry/wet mix, 0..1.
488 #[serde(default)]
489 mix: f32,
490 },
491 /// Modal resonator bank: a set of damped sinusoidal partials (`modes`)
492 /// excited by the incoming signal — a struck/resonant object's *body*.
493 /// Bells, glass, metal bars, wood, ceramic, coins, and the resonant ping
494 /// of UI/impact sounds, none of which the oscillators can voice cleanly.
495 /// Each mode is one 2-pole resonator (a constant-peak-gain bandpass), so a
496 /// bank is N parallel resonators — cheap, stable, deterministic. Use it as
497 /// a chain stage after an excitation: `chain[ impact, modal ]`. The
498 /// excitation's brightness lights the modes; the modes' frequencies and
499 /// decays define the timbre. Author modes explicitly — the cookbook's
500 /// struck-bodies guidance (harmonic vs off-harmonic ratios, short vs long
501 /// decays) is the map from material to mode list.
502 Modal {
503 /// The resonant partials (1..=64). Each is a damped sine.
504 modes: Vec<Mode>,
505 /// Wet/dry mix, 0..1 (1 = pure resonance; lower keeps some of the raw
506 /// excitation transient for extra attack click).
507 #[serde(default = "default_modal_mix")]
508 mix: f32,
509 },
510 /// Waveshaper for saturation / distortion. `amount` is pre-gain; `shape`
511 /// chooses the curve (warm `tanh`, aggressive `hard` clip, or `fold`back).
512 Drive {
513 /// Drive amount (pre-gain into the shaper).
514 amount: Value,
515 /// Distortion curve.
516 #[serde(default)]
517 shape: DriveShape,
518 /// Antiderivative anti-aliasing. The waveshaper's harmonics fold back
519 /// as inharmonic alias dirt at the base rate; ADAA suppresses that for
520 /// a clean, hi-fi distortion. Honoured only when the document's
521 /// `engine` is ≥ 1 (so legacy documents stay bit-exact); within an
522 /// engine-1 document it is on by default — set `false` to hear the raw
523 /// aliasing curve. Omitted ⇒ follow the engine.
524 #[serde(default, skip_serializing_if = "Option::is_none")]
525 aa: Option<bool>,
526 },
527 /// Ring modulation: multiply the signal by a sine carrier at `freq`. Metallic,
528 /// clangorous, robotic textures.
529 RingMod {
530 /// Carrier frequency in Hz.
531 freq: Value,
532 },
533 /// Tremolo: per-sample amplitude modulation — the gain swings between
534 /// `1 - depth` and 1 at `rate` Hz. Unlike a modulated `gain` (which does
535 /// not stream), the gain is a closed-form function of the absolute sample
536 /// index, so tremolo streams natively and byte-identically to the offline
537 /// render.
538 Tremolo {
539 /// LFO rate in Hz.
540 #[serde(default = "default_tremolo_rate")]
541 rate: f32,
542 /// Modulation depth, 0..1 (0 = transparent passthrough, 1 = full cut).
543 #[serde(default = "default_mod_depth")]
544 depth: f32,
545 },
546 /// Flanger: a very short modulated delay with feedback — the classic jet
547 /// sweep / metallic whoosh. Stronger and more resonant than chorus.
548 Flanger {
549 /// LFO rate in Hz.
550 #[serde(default = "default_flanger_rate")]
551 rate: f32,
552 /// Modulation depth, 0..1.
553 #[serde(default = "default_mod_depth")]
554 depth: f32,
555 /// Feedback amount, 0..1 (more ⇒ more resonant).
556 #[serde(default = "default_flanger_feedback")]
557 feedback: f32,
558 /// Dry/wet mix, 0..1.
559 #[serde(default = "default_mod_mix")]
560 mix: f32,
561 },
562 /// Phaser: swept all-pass notches — a hollow, swooshing movement for pads,
563 /// lasers, and sci-fi textures.
564 Phaser {
565 /// LFO sweep rate in Hz.
566 #[serde(default = "default_phaser_rate")]
567 rate: f32,
568 /// Sweep depth, 0..1.
569 #[serde(default = "default_mod_depth")]
570 depth: f32,
571 /// Feedback amount, 0..1.
572 #[serde(default = "default_phaser_feedback")]
573 feedback: f32,
574 /// Dry/wet mix, 0..1.
575 #[serde(default = "default_mod_mix")]
576 mix: f32,
577 },
578 /// Chorus: a short modulated delay mixed with the dry signal for thickening
579 /// and width.
580 Chorus {
581 /// LFO rate in Hz.
582 #[serde(default = "default_chorus_rate")]
583 rate: f32,
584 /// Modulation depth, 0..1.
585 #[serde(default = "default_mod_depth")]
586 depth: f32,
587 /// Dry/wet mix, 0..1.
588 #[serde(default = "default_mod_mix")]
589 mix: f32,
590 },
591 /// Sidechain duck: gain-reduces the chained signal whenever `trigger` is
592 /// loud — the pumping that glues a bass or pad to the kick. The trigger
593 /// is rendered silently (it only steers the gain); chain the audible
594 /// kick separately in the mix.
595 Duck {
596 /// The signal whose loudness drives the ducking (e.g. the kick seq).
597 trigger: Box<Node>,
598 /// Duck depth, 0..1 (1 = fully silent at the trigger's peak).
599 #[serde(default = "default_duck_amount")]
600 amount: f32,
601 /// Gain-reduction attack in seconds.
602 #[serde(default = "default_duck_attack")]
603 attack: f32,
604 /// Recovery time in seconds (the "pump" length).
605 #[serde(default = "default_duck_release")]
606 release: f32,
607 },
608 /// Dynamic-range compressor: tames peaks above `threshold` (dBFS) by `ratio`,
609 /// with `attack`/`release` ballistics, then applies `makeup` gain (dB). The
610 /// glue behind loud, punchy game audio.
611 Compress {
612 /// Threshold in dBFS (e.g. -18).
613 threshold: f32,
614 /// Compression ratio (e.g. 4 = 4:1).
615 ratio: f32,
616 /// Attack time in seconds.
617 #[serde(default = "default_comp_attack")]
618 attack: f32,
619 /// Release time in seconds.
620 #[serde(default = "default_comp_release")]
621 release: f32,
622 /// Make-up gain in dB.
623 #[serde(default)]
624 makeup: f32,
625 },
626 /// Convolution reverb with a synthesized impulse response — the "put it in
627 /// a real space" effect, with zero assets: instead of loading an IR file,
628 /// the node *generates* its own IR deterministically from its parameters
629 /// and its structural position in the graph (the same node at the same
630 /// graph position always builds the same IR — edit-stable like the
631 /// structurally-seeded RNG sources of engine 2 and later). The IR is a
632 /// white-noise burst under an exponential
633 /// decay envelope reaching −60 dB at `decay` seconds (RT60-style), capped
634 /// at `size` seconds, darkened over time by a one-pole lowpass whose
635 /// cutoff falls per `damp`, and shifted right by `predelay` (the gap
636 /// between the dry hit and the room's answer — larger rooms answer later).
637 /// Convolution is FFT-based (single-shot, the whole block at once) and
638 /// normalized to unit IR energy, so the wet level stays put as `decay` /
639 /// `size` change. Like `reverb`, the tail folds into the document: the
640 /// output is truncated to the document length rather than extended. `mix`
641 /// crossfades dry/wet (0 = transparent passthrough). This node is
642 /// **offline only**: convolution needs the whole input buffer, so the
643 /// streaming renderer refuses it with a named reason (bounce it offline
644 /// and keep the streamed graph causal).
645 Convolve {
646 /// RT60-ish decay time in seconds (tail reaches −60 dB at this point).
647 #[serde(default = "default_convolve_decay")]
648 decay: f32,
649 /// IR length cap in seconds (0 = `decay`, i.e. no extra truncation).
650 #[serde(default)]
651 size: f32,
652 /// Silence before the IR starts, in seconds (pre-delay).
653 #[serde(default)]
654 predelay: f32,
655 /// High-frequency damping, 0..1: how fast the tail darkens (0 = the
656 /// burst stays white, 1 = it closes to a rumble by the end).
657 #[serde(default = "default_convolve_damp")]
658 damp: f32,
659 /// Dry/wet mix, 0..1.
660 #[serde(default = "default_convolve_mix")]
661 mix: f32,
662 },
663 /// Granular texture: chops the incoming signal into overlapping
664 /// Hann-windowed grains of `grain_ms` milliseconds at `density` grains per
665 /// second, replays each at `pitch` (playback ratio — 2 = octave up, 0.5 =
666 /// octave down) with deterministic onset/source jitter and detune of depth
667 /// `spread` (drawn from the node's structurally-seeded stream in a fixed
668 /// order, so the texture is stable under edits), sums them (normalized by
669 /// the window overlap so loudness tracks the dry signal), and crossfades
670 /// with the dry signal per `mix`. Frozen pads and shimmer from any source
671 /// material: chain it after a note or a noise burst and let `spread`
672 /// smear it into a cloud. `mix` 0 = transparent passthrough. **Offline
673 /// only** — grains read the whole input out of order, so the streaming
674 /// renderer refuses it with a named reason (bounce it offline and keep the
675 /// streamed graph causal).
676 Granular {
677 /// Grain length in milliseconds (5..=500).
678 #[serde(default = "default_granular_grain_ms")]
679 grain_ms: f32,
680 /// Grains per second (overlap = grain_ms × density / 1000).
681 #[serde(default = "default_granular_density")]
682 density: f32,
683 /// Grain playback ratio (1 = source pitch, 2 = octave up).
684 #[serde(default = "default_granular_pitch")]
685 pitch: f32,
686 /// Randomization depth, 0..1: onset jitter and per-grain detune.
687 #[serde(default = "default_granular_spread")]
688 spread: f32,
689 /// Dry/wet mix, 0..1.
690 #[serde(default = "default_granular_mix")]
691 mix: f32,
692 },
693}
694
695impl Node {
696 /// True if this node only makes sense as a non-first stage of a `chain`
697 /// (it transforms an incoming signal rather than generating one).
698 pub fn is_processor(&self) -> bool {
699 matches!(
700 self,
701 Node::Lowpass { .. }
702 | Node::Highpass { .. }
703 | Node::Bandpass { .. }
704 | Node::Notch { .. }
705 | Node::Peak { .. }
706 | Node::Lowshelf { .. }
707 | Node::Highshelf { .. }
708 | Node::Gain { .. }
709 | Node::Bitcrush { .. }
710 | Node::Downsample { .. }
711 | Node::Delay { .. }
712 | Node::Reverb { .. }
713 | Node::Modal { .. }
714 | Node::Drive { .. }
715 | Node::RingMod { .. }
716 | Node::Tremolo { .. }
717 | Node::Chorus { .. }
718 | Node::Flanger { .. }
719 | Node::Phaser { .. }
720 | Node::Compress { .. }
721 | Node::Duck { .. }
722 | Node::Convolve { .. }
723 | Node::Granular { .. }
724 )
725 }
726
727 /// Every direct child of this node — the nested graphs of the combinator
728 /// variants: `mix`/`mul` inputs, `chain` stages, a `tracks`' layers then
729 /// its `master` chain, and a `duck`'s trigger. The ONE traversal
730 /// definition every walker shares, so a new variant can never be silently
731 /// skipped the way the hand-written walkers were (the `duck`-trigger
732 /// omissions in the vary and MIDI walkers were exactly that class). The
733 /// match is exhaustive *on purpose*: adding a variant without a children
734 /// decision is a compile error, never a silent skip.
735 pub fn children(&self) -> Children<'_> {
736 let kind = match self {
737 Node::Mix { inputs } | Node::Mul { inputs } => ChildrenKind::Slice(inputs.iter()),
738 Node::Chain { stages } => ChildrenKind::Slice(stages.iter()),
739 Node::Tracks {
740 tracks,
741 master,
742 buses,
743 } => ChildrenKind::Tracks {
744 tracks: tracks.iter(),
745 master: master.iter(),
746 buses: buses.iter(),
747 bus_fx: [].iter(),
748 },
749 Node::Duck { trigger, .. } => ChildrenKind::One(Some(trigger)),
750 Node::Square { .. }
751 | Node::Triangle { .. }
752 | Node::Sawtooth { .. }
753 | Node::Sine { .. }
754 | Node::Noise { .. }
755 | Node::Fm { .. }
756 | Node::Super { .. }
757 | Node::Wavetable { .. }
758 | Node::Seq { .. }
759 | Node::Impact { .. }
760 | Node::Dust { .. }
761 | Node::Env { .. }
762 | Node::Lowpass { .. }
763 | Node::Highpass { .. }
764 | Node::Bandpass { .. }
765 | Node::Notch { .. }
766 | Node::Peak { .. }
767 | Node::Lowshelf { .. }
768 | Node::Highshelf { .. }
769 | Node::Gain { .. }
770 | Node::Bitcrush { .. }
771 | Node::Downsample { .. }
772 | Node::Delay { .. }
773 | Node::Reverb { .. }
774 | Node::Modal { .. }
775 | Node::Drive { .. }
776 | Node::RingMod { .. }
777 | Node::Tremolo { .. }
778 | Node::Chorus { .. }
779 | Node::Flanger { .. }
780 | Node::Phaser { .. }
781 | Node::Compress { .. }
782 | Node::Convolve { .. }
783 | Node::Granular { .. } => ChildrenKind::None,
784 };
785 Children { kind }
786 }
787
788 /// Mutable form of [`children`](Self::children).
789 pub fn children_mut(&mut self) -> ChildrenMut<'_> {
790 let kind = match self {
791 Node::Mix { inputs } | Node::Mul { inputs } => {
792 ChildrenMutKind::Slice(inputs.iter_mut())
793 }
794 Node::Chain { stages } => ChildrenMutKind::Slice(stages.iter_mut()),
795 Node::Tracks {
796 tracks,
797 master,
798 buses,
799 } => ChildrenMutKind::Tracks {
800 tracks: tracks.iter_mut(),
801 master: master.iter_mut(),
802 buses: buses.iter_mut(),
803 bus_fx: [].iter_mut(),
804 },
805 Node::Duck { trigger, .. } => ChildrenMutKind::One(Some(trigger)),
806 Node::Square { .. }
807 | Node::Triangle { .. }
808 | Node::Sawtooth { .. }
809 | Node::Sine { .. }
810 | Node::Noise { .. }
811 | Node::Fm { .. }
812 | Node::Super { .. }
813 | Node::Wavetable { .. }
814 | Node::Seq { .. }
815 | Node::Impact { .. }
816 | Node::Dust { .. }
817 | Node::Env { .. }
818 | Node::Lowpass { .. }
819 | Node::Highpass { .. }
820 | Node::Bandpass { .. }
821 | Node::Notch { .. }
822 | Node::Peak { .. }
823 | Node::Lowshelf { .. }
824 | Node::Highshelf { .. }
825 | Node::Gain { .. }
826 | Node::Bitcrush { .. }
827 | Node::Downsample { .. }
828 | Node::Delay { .. }
829 | Node::Reverb { .. }
830 | Node::Modal { .. }
831 | Node::Drive { .. }
832 | Node::RingMod { .. }
833 | Node::Tremolo { .. }
834 | Node::Chorus { .. }
835 | Node::Flanger { .. }
836 | Node::Phaser { .. }
837 | Node::Compress { .. }
838 | Node::Convolve { .. }
839 | Node::Granular { .. } => ChildrenMutKind::None,
840 };
841 ChildrenMut { kind }
842 }
843
844 /// Apply `f` to this node and every descendant, depth-first (parents
845 /// before children, in document order).
846 pub fn walk(&self, f: &mut impl FnMut(&Node)) {
847 f(self);
848 for c in self.children() {
849 c.walk(f);
850 }
851 }
852
853 /// Mutable form of [`walk`](Self::walk).
854 pub fn walk_mut(&mut self, f: &mut impl FnMut(&mut Node)) {
855 f(self);
856 for c in self.children_mut() {
857 c.walk_mut(f);
858 }
859 }
860}
861
862/// The iterator returned by [`Node::children`].
863pub struct Children<'a> {
864 kind: ChildrenKind<'a>,
865}
866
867enum ChildrenKind<'a> {
868 None,
869 One(Option<&'a Node>),
870 Slice(std::slice::Iter<'a, Node>),
871 Tracks {
872 tracks: std::slice::Iter<'a, Track>,
873 master: std::slice::Iter<'a, Node>,
874 buses: std::slice::Iter<'a, Bus>,
875 bus_fx: std::slice::Iter<'a, Node>,
876 },
877}
878
879impl<'a> Iterator for Children<'a> {
880 type Item = &'a Node;
881 fn next(&mut self) -> Option<&'a Node> {
882 match &mut self.kind {
883 ChildrenKind::None => None,
884 ChildrenKind::One(x) => x.take(),
885 ChildrenKind::Slice(it) => it.next(),
886 ChildrenKind::Tracks {
887 tracks,
888 master,
889 buses,
890 bus_fx,
891 } => {
892 if let Some(t) = tracks.next() {
893 return Some(&t.node);
894 }
895 if let Some(m) = master.next() {
896 return Some(m);
897 }
898 loop {
899 if let Some(fx) = bus_fx.next() {
900 return Some(fx);
901 }
902 let b = buses.next()?;
903 *bus_fx = b.effects.iter();
904 }
905 }
906 }
907 }
908}
909
910/// The iterator returned by [`Node::children_mut`].
911pub struct ChildrenMut<'a> {
912 kind: ChildrenMutKind<'a>,
913}
914
915enum ChildrenMutKind<'a> {
916 None,
917 One(Option<&'a mut Node>),
918 Slice(std::slice::IterMut<'a, Node>),
919 Tracks {
920 tracks: std::slice::IterMut<'a, Track>,
921 master: std::slice::IterMut<'a, Node>,
922 buses: std::slice::IterMut<'a, Bus>,
923 bus_fx: std::slice::IterMut<'a, Node>,
924 },
925}
926
927impl<'a> Iterator for ChildrenMut<'a> {
928 type Item = &'a mut Node;
929 fn next(&mut self) -> Option<&'a mut Node> {
930 match &mut self.kind {
931 ChildrenMutKind::None => None,
932 ChildrenMutKind::One(x) => x.take(),
933 ChildrenMutKind::Slice(it) => it.next(),
934 ChildrenMutKind::Tracks {
935 tracks,
936 master,
937 buses,
938 bus_fx,
939 } => {
940 if let Some(t) = tracks.next() {
941 return Some(&mut t.node);
942 }
943 if let Some(m) = master.next() {
944 return Some(m);
945 }
946 loop {
947 if let Some(fx) = bus_fx.next() {
948 return Some(fx);
949 }
950 let b = buses.next()?;
951 *bus_fx = b.effects.iter_mut();
952 }
953 }
954 }
955 }
956}
957
958/// FM voice knobs of a `seq` node (`wave: "fm"`), flattened onto the node in
959/// JSON. Defaults ≈ an FM piano strike (ratio 1 + decaying index).
960#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
961pub struct FmKnobs {
962 /// Modulator frequency ratio (1 = e-piano/piano, 3.5 = bell, 14 = tine).
963 #[serde(default = "default_seq_fm_ratio")]
964 pub fm_ratio: f32,
965 /// Modulation index at the strike (brightness; also scaled by each note's
966 /// velocity, so louder notes ring brighter).
967 #[serde(default = "default_seq_fm_index")]
968 pub fm_index: f32,
969 /// Strike decay in seconds: how fast the index (brightness) fades after
970 /// each note's attack. Short = percussive e-piano, long = bell shimmer.
971 #[serde(default = "default_seq_fm_strike")]
972 pub fm_strike: f32,
973}
974
975/// Plucked-string knobs of a `seq` node (`wave: "pluck"`), flattened onto the
976/// node in JSON. The tone stages default to identity, so omitting them renders
977/// byte-identically and draws no extra RNG.
978#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
979pub struct PluckKnobs {
980 /// String feedback decay, 0.8..1 (higher rings longer; low notes
981 /// naturally ring longer than high ones).
982 #[serde(default = "default_pluck_decay")]
983 pub pluck_decay: f32,
984 /// Acoustic body-resonance depth, 0..1 — mixes in a fixed guitar-body
985 /// mode bank. 0 = solid-body (default).
986 #[serde(default = "default_pluck_body")]
987 pub pluck_body: f32,
988 /// Pick/attack transient level, 0..1. 0 = none.
989 #[serde(default = "default_pluck_pick")]
990 pub pluck_pick: f32,
991 /// String brightness/damping, −1..1. 0 = the current loop filter;
992 /// + brightens, − darkens.
993 #[serde(default = "default_pluck_tone")]
994 pub pluck_tone: f32,
995}
996
997/// Piano tone knobs of a `seq` node (`wave: "piano"`, engine ≥ 3), flattened
998/// onto the node in JSON. Every default reproduces the concert-grand kernel
999/// bit-for-bit, so a doc that omits them renders byte-identically.
1000#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
1001pub struct PianoKnobs {
1002 /// Hammer hardness: spectral brightness. 1 = concert grand; > 1
1003 /// harder/brighter, < 1 softer/darker.
1004 #[serde(default = "default_piano_hammer")]
1005 pub piano_hammer: f32,
1006 /// Hammer strike position (fraction along the string): the comb-notch
1007 /// that thins the spectrum. 0.125 = grand; toward the bridge is brighter.
1008 #[serde(default = "default_piano_strike")]
1009 pub piano_strike: f32,
1010 /// String-stiffness scale: stretches the partials sharp. 1 = grand;
1011 /// > 1 = short/stiff upright jangle.
1012 #[serde(default = "default_piano_inharm")]
1013 pub piano_inharm: f32,
1014 /// Unison detune width: the two-string beating. 1 ≈ ±1 cent (grand
1015 /// shimmer); ~12 = honky-tonk warble.
1016 #[serde(default = "default_piano_detune")]
1017 pub piano_detune: f32,
1018 /// Ring-time scale. 1 = grand; < 1 = shorter, damped; > 1 = longer.
1019 #[serde(default = "default_piano_decay")]
1020 pub piano_decay: f32,
1021}
1022
1023/// Bass tone knobs of a `seq` node (`wave: "bass"`), flattened onto the node
1024/// in JSON. Every default is the original voice's hard-coded constant, so
1025/// omitting them renders byte-identically.
1026#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
1027pub struct BassKnobs {
1028 /// Filter resting floor in Hz. Low = dark/round.
1029 #[serde(default = "default_bass_cutoff")]
1030 pub bass_cutoff: f32,
1031 /// Fixed cutoff-sweep depth above the floor, Hz.
1032 #[serde(default = "default_bass_env")]
1033 pub bass_env: f32,
1034 /// Velocity-scaled sweep depth, Hz (× note velocity).
1035 #[serde(default = "default_bass_env_vel")]
1036 pub bass_env_vel: f32,
1037 /// Filter-sweep time constant, seconds — how fast the cutoff closes.
1038 #[serde(default = "default_bass_decay")]
1039 pub bass_decay: f32,
1040 /// Pick-tick: an extra attack cutoff bump over ~8 ms, Hz. 0 = none.
1041 #[serde(default = "default_bass_click")]
1042 pub bass_click: f32,
1043 /// Filtered-saw body level.
1044 #[serde(default = "default_bass_body")]
1045 pub bass_body: f32,
1046 /// Sine-sub level.
1047 #[serde(default = "default_bass_sub")]
1048 pub bass_sub: f32,
1049 /// Sub frequency ratio to the note. 1 = reinforce; 0.5 = octave down.
1050 #[serde(default = "default_bass_sub_ratio")]
1051 pub bass_sub_ratio: f32,
1052 /// tanh saturation, 0..1. 0 = clean; > 0 = synth-bass grit.
1053 #[serde(default = "default_bass_drive")]
1054 pub bass_drive: f32,
1055 /// Note body decay, seconds (on top of the ADSR). Longer = sustained.
1056 #[serde(default = "default_bass_body_decay")]
1057 pub bass_body_decay: f32,
1058}
1059
1060/// SoundFont sampler settings of a `seq` node (`wave: "sampler"`), flattened
1061/// onto the node in JSON.
1062#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1063pub struct Sf2Knobs {
1064 /// Path to a SoundFont (.sf2) file.
1065 #[serde(default)]
1066 pub sf2: String,
1067 /// General MIDI program number (0..=127).
1068 #[serde(default)]
1069 pub sf2_preset: u32,
1070 /// SoundFont bank (0 = melodic, 128 = the percussion bank / GM drum map).
1071 #[serde(default)]
1072 pub sf2_bank: u32,
1073}