Tzopilotl
Docs
GitHub

Writing SynthDefs

12. Polyphony: voicer, noteParam, gate

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.

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);
Voice bodies are ordinary subgraphs: they can contain delays, filters, random init-rate values (different per voice), and controls declared outside the voicer are shared across voices. See examples/instrument_synthdefs.x for a set of ten voicer-based instruments (FM bell, pluck, modal bell, kick, snare, …).
↑ Back to top