voicer(maxVoices, voiceFn) instantiates maxVoices copies of the
voice subgraph, each with independent state (delays, envelopes, random values) and per-note
parameters. The voicer's output carries the voices as channels; sum mixes them.
The engine handles voice allocation, stealing, and note lifetimes.
noteParam(name, spec) declares a per-note parameter
inside the voice body. When a note starts, the note's parameter values are latched into
that voice.gate() reads the implicit per-note gate: 1 while the
note is held, 0 after note-off. Drive an asr/adsr release from
it, or ignore it for one-shot percussive voices (use gate() tr as a
trigger and let a decay2 ring out).Trigger notes from Tzopilotl with the audio_engine bridge:
noteOn(nodeID, noteID, params) and noteOff(nodeID, noteID).
The params array matches the order in which noteParam is
declared in the voice body; gate is implicit and always last. The
music libraries' player and the app's piano roll drive voicers the same way.
import synthdef.*;
import synthc.compile.*;
import common_ugens.*;
fn organVoice() S {
let f = noteParam("freq", ControlSpec { lo: 20.0, hi: 12000.0, init: 440.0, warp: ControlWarp.exponential });
let a = noteParam("amp", ControlSpec { lo: 0.0, hi: 1.0, init: 0.5, warp: ControlWarp.linear });
let g = gate();
let env = adsr(g, 0.02, 0.1, 0.9, 0.25);
let harms = [1, 2, 3, 4, 5, 6, 8] vec;
let amps = [1.0, 0.7, 0.5, 0.4, 0.25, 0.2, 0.15] vec;
(f * harms) sinosc * amps |> sum * env * a * 0.18
}
fn organ() S = voicer(8, organVoice) sum outlet;
organ defSynthX("organ") await;
-- engine side: params are [freq, amp, gate] -- declaration order, gate last
newNode("organ", 100); connect(100, 0, 0, 0);
noteOn(100, 1, [261.6, 0.5, 1.0]);
noteOff(100, 1);
examples/instrument_synthdefs.x for a set of ten
voicer-based instruments (FM bell, pluck, modal bell, kick, snare, …).