The language

Endpoints

Inputs, outputs, side channels, endpoint arrays, and the five dimensions of a note.

Anyone wiring a patch to a host, a DAW or a keyboard.

An endpoint is a wire in or out of a processor. Reading an input gives you this frame's value from the host or another node. Writing an output gives the host or the next node a result. The name is yours to choose; the declaration states its direction, shape and role.

Audio and control streams use the same per-frame arithmetic. What makes a stream “pitch” or “level” is the contract between the processor and its host, not a different numeric type.

Playground
input  stream float pitch;
input  stream float vel;
output stream float<2> out;

Inputs are read by name; outputs are written with <-. Reading an output or writing an input is an error (POLE0301, POLE0302).

The first output is the audio#

Use output stream float out; for mono or output stream float<2> out; for stereo. A stereo value carries left in lane 0 and right in lane 1. Declaring two separate scalar outputs does not give the same interface: the second endpoint is a side channel.

Every output after the first is a side channel — a level for a meter, a frame for a spectrum, a tap for an effects send. The host reads them by name and never mixes them into the audio.

Playground
output stream float<2> out;      // the audio
output stream float    level;    // a meter reads this
output stream float    busTap;   // an effects send reads this

This was capped at two outputs outright until it became clear that a scope, a send bus and a signal probe are all the same thing — an endpoint that is not a channel — and that all three were therefore unwritable.

Endpoint arrays#

An input may be an array, which is what makes polyphony writable:

Playground
input stream float voicePitch[8];
input stream float voiceGate[8];

var mix = 0.0f;
for (wrap<8> v) { mix = mix + voicePitch[v] * voiceGate[v]; }
out <- mix * 0.05f;

Eight voices is one declaration and a loop, not sixteen declarations. Sizes are powers of two and the index is masked, exactly like state arrays.

Inputs are effectively unbounded — the cap is 4096 slots, sized from the largest real thing that has to fit rather than from a guess about what is reasonable.

A note has five dimensions#

When a patch is played live, inputs are fed in declaration order from the host's controls. A note is not one number:

indexmeaning
0pitch in Hz — and it holds through the release
1velocity, 0–1
2mod wheel (CC 1), 0–1
3gate — 1 while held, 0 once released
4pressure, 0–1 — aftertouch
5brightness, 0–1 — MPE timbre (CC 74)
6–13pitch per voice, 8 voices
14–21velocity per voice
22–29gate per voice
30–37pressure per voice
38–45brightness per voice

polec --note-layout prints these rather than anything mirroring them.

Why gate is separate from pitch

The gate used to be pitch — pitch > 0 meant a note was on — on the reasoning that it is how a modular synth would wire it. That is a 1970s control-voltage convention, and here it was wrong in a way you could hear: a note-off zeroed the pitch, so the oscillator stopped while the envelope was still releasing, and every release tail came out as DC instead of a fading tone.

Silencing a voice is the envelope's job. So pitch holds and gate falls.

Pressure and brightness are MPE's two expression axes. Without them a patch cannot respond to a controller anyone currently owns.

Monophonic and polyphonic#

A monophonic patch reads indices 0–5 and ignores the rest. A polyphonic one declares voicePitch[8] / voiceGate[8] and loops over them; the host allocates voices from MIDI with last-note stealing.

Offline, --controls=0.9,0.5 sets endpoints 1..N to fixed values for a render.

If you would rather receive real note events than read a CV convention, the language has those too — see events.