Skode’s ordinary v, n, l, modulation, routing, and copy commands remain
the lowest-level voice interface. Voice groups and pools are an optional layer
above that interface. They make a multi-voice sound playable as a polyphonic or
monophonic instrument without hiding its physical voices.
A group describes a contiguous prototype block of physical voices. It has
one root voice. Pitch and gate commands enter through the root, then the
prototype’s existing G and H links decide which other members receive those
changes.
A pool reserves one or more equally sized blocks as playable instances of a group. Pool configuration copies the prototype synthesis settings into every instance and remaps dependencies that point to another prototype member. References outside the prototype remain absolute, which permits shared LFOs or other global modulation voices.
Groups and pools do not prevent direct vN editing. Pool ownership is
advisory: direct edits take effect immediately, and a later pool refresh or
allocation may replace the synthesis state of a free instance.
Serialize /pg, /pp, /pm, and refresh calls with the host’s other Skode
configuration work. They mutate shared voice layouts and are intended for the
control path, not for concurrent calls from several host threads. The compiled
pn, pr, and pb operations are the bounded performance path.
All limits are fixed and allocation-free during performance:
First construct a sound using ordinary voices. This two-voice example layers an octave above the root:
v0 w0 a0 t.01,.15,.7,.35 N0 G1 H1
v1 w0 a0 t.005,.1,.5,.25 N12
Define group 0 from voices 0 and 1:
/pg 0,0,2,0
The arguments are:
/pg group,source,width[,root-offset]
root-offset defaults to 0. It is relative to source, so the example root
is voice 0.
The group records the prototype layout, not a second private copy of every synth field. Pool construction and explicit refresh read the current prototype settings. Editing the prototype does not silently rewrite sounding instances.
Show one group or all groups:
?pg 0
?pg
Create four two-voice instances in voices 8 through 15:
/pp 0,0,8,4,0
The arguments are:
/pp pool,group,base,count[,steal-policy]
The numeric steal policies are:
| Value | Name | Behavior after checking for a free instance |
|---|---|---|
0 |
release-oldest | Oldest releasing instance, then oldest held instance. This is the default. |
1 |
oldest | Oldest allocated instance. |
2 |
round-robin | Next instance in pool order. |
3 |
quietest | Instance whose loudest member envelope currently has the lowest amplitude. |
4 |
no-steal | Reject note-on when every instance is occupied. |
Unknown policy values reject pool construction. Every policy always prefers a free instance.
The destination range must fit inside the runtime voice count. It may begin at the prototype source, allowing a one-instance group to use its original voices. Other partial overlaps with the prototype are rejected, as are overlapping destination ranges owned by two pools.
A pool index can be redefined; sounding allocations in the old definition are released first. A group can be restated with the same geometry, but changing its source, width, or root is rejected while a pool still refers to it. This prevents an existing allocator from silently acquiring the wrong physical voice layout. Rebuild those pools with another group index when a live layout migration is needed.
Show current allocations:
?pp 0
?pp
The output includes copy/pasteable /pp and /pm commands, numeric values,
readable policy/mode comments, physical root voices, allocation state, keys,
notes, and velocities.
Pool note commands use an integer key that identifies a note lifetime. The key is intentionally separate from pitch. This permits overlapping instances of the same MIDI note and makes a late release harmless after an instance has been stolen.
Keys must be exactly representable by the compiled float opcode format, so the
documented range is -16777216..16777216. Key -1 is reserved as the
pool-wide target for pitch bend.
pn pool,key,note,velocity[,cents]
pr pool,key[,release-velocity]
pb pool,key,semitones[,cents]
Examples:
pn 0,1001,60,.8
pn 0,1002,64,.7
pb 0,1001,2
pr 0,1001
pr 0,1002
pn applies n and then l to the allocated instance root. Its cloned G
and H links propagate pitch and gate exactly as they do under manual voice
control. Member N values continue to provide intervals and fine detuning.
pr applies l0. Releasing an unknown or stolen key is a successful no-op,
so delayed MIDI note-off messages cannot release a newer allocation.
The optional release-velocity argument is retained for MIDI-shaped call sites;
it is currently accepted but does not alter the release envelope.
pb changes pitch without retriggering. A key of -1 stores a pool-wide bend,
applies it to every held instance, and also affects later note-ons:
pb 0,-1,-2,25
That bends the pool down 175 cents. Per-note bend remains available with a normal key.
pn, pr, and pb are bounded numeric opcodes. They are valid in pattern
steps, defers, repeats, external compiled macros, and the timestamped event
queue. Group and pool configuration commands are immediate-only.
A monophonic pool uses its first group instance; any remaining instances stay idle until the pool returns to polyphonic mode. Configure it after pool construction:
/pp 1,0,20,1,0
/pm 1,1,0,1
The arguments are:
/pm pool,mode[,priority[,articulation]]
Modes:
| Value | Meaning |
|---|---|
0 |
Polyphonic allocation |
1 |
Monophonic held-note selection |
2 |
Reserved for a future arpeggiator; currently rejected |
Monophonic priorities:
| Value | Meaning |
|---|---|
0 |
Last pressed note |
1 |
Highest note |
2 |
Lowest note |
3 |
First pressed note |
Articulation:
| Value | Meaning |
|---|---|
0 |
Retrigger the envelope whenever the active note changes |
1 |
Legato: trigger only the first held note and release only after the final key is released |
Example last-note legato behavior:
pn 1,2001,60,.8
pn 1,2002,67,.6
pr 1,2002
pr 1,2001
The pool plays 60, changes to 67, returns to the still-held 60, then releases.
The held-note ledger retains every key’s pitch, cents, velocity, bend, and
press order. That fixed ledger is also the intended input set for future
arpeggiation mode 2.
Changing /pm releases current allocations and clears the held-note ledger.
This makes it safe to switch an existing multi-instance pool between poly and
mono without rebuilding it.
The control-plane MIDI router can drive a configured pool directly. /mp
accepts channel,pool[,bend-range]; channels are zero-based and ./- means
all channels.
For the four-instance, two-voice polyphonic pool 0 constructed above:
/mp 0 0 2
MIDI channel 1 now supplies note-on, note-off, and ±2-semitone pitch bend. Every note lifetime is keyed by channel plus MIDI note, so delayed note-off and equal notes on different accepted channels do not release the wrong instance.
For monophonic pool 1 from the previous section:
/mp 0 1 12
This retains the pool’s configured priority and articulation while allowing a
±12-semitone bend range. The router does not replace /pm; it only translates
MIDI performance messages into the pool’s existing note, release, and bend
operations.
The simpler /mv channel,voice[,bend-range] route drives one physical voice
without a pool. It is useful for a basic one-note-at-a-time instrument, but it
tracks only the latest active key and cannot return to an earlier held note.
Use monophonic pool mode for held-note priority, legato, and fallback behavior.
See the “MIDI mono and poly synths” and “MIDI drum maps” recipes in SKODE_USER_COMMAND_REFERENCE.md for complete voice setup examples and channel-routing variations.
After editing prototype voices, refresh every free instance in pools using the group:
/pg! 0
Refresh only one pool:
/pp! 0
Refresh never rewrites held or releasing instances. Those instances retain their current settings until they become free and a later refresh occurs.
Group cloning uses the existing synthesis-copy behavior, resets transient playback state, and remaps voice dependencies. It includes oscillator, wave, range/loop, amplitude, pan, envelopes, tuning, filters, smoothing, distortion, and enabled modulation settings.
It deliberately preserves the destination voice’s:
r)ds)vc)Track delay parameters are global track state and are never part of a group.
The transient state reset includes one-shot/loop progress, output sample,
ping-pong direction leg, finished flags, and sample-hold progress. Oscillator
and envelope performance state begins when the instance receives pn.
Use /vg to inspect all dependencies reachable from one voice:
/vg 0
/vg 0,0,4
/vg 0,1,4
The arguments are:
/vg voice[,format[,depth]]
Formats:
0: human-readable ASCII tree (default)1: stable machine-readable graph textDepth 0 means the configured voice count. Cycles are shown once and marked
(seen) in ASCII output.
Example ASCII:
voice v0
|- pitch -> v1
`- gate -> v1 (seen)
The machine format is a line protocol designed for ro-totem and browser apps:
skred-voice-graph 1
root 0
node 0
edge 0 1 0 pitch
edge 0 1 1 gate
end
Grammar:
header = "skred-voice-graph" SP version LF
root = "root" SP voice LF
node = "node" SP voice LF
edge = "edge" SP from SP to SP type SP label LF
end = "end" LF
Edge types are numeric and stable:
| Type | Label | Source setting |
|---|---|---|
0 |
pitch | G MIDI-note link |
1 |
gate | H velocity/gate link |
2 |
amp-mod | A source voice |
3 |
freq-mod | F source voice |
4 |
pan-mod | P source voice |
5 |
phase-mod | C source voice |
6 |
ring-mod | XM source voice |
Feature-gated edge kinds appear only when included in the build. Multiple edge types between the same nodes are retained.
A browser-side parser can stay deliberately small:
function parseVoiceGraph(text) {
const graph = { version: 0, root: -1, nodes: [], edges: [] };
for (const line of text.trim().split("\n")) {
const field = line.split(/\s+/);
if (field[0] === "skred-voice-graph") graph.version = Number(field[1]);
else if (field[0] === "root") graph.root = Number(field[1]);
else if (field[0] === "node") graph.nodes.push(Number(field[1]));
else if (field[0] === "edge") graph.edges.push({
from: Number(field[1]), to: Number(field[2]),
type: Number(field[3]), label: field[4]
});
}
if (graph.version !== 1) throw new Error("unsupported voice graph version");
return graph;
}
Consumers should key edges by (from,to,type), not just (from,to), because
pitch and gate commonly connect the same pair of voices.
Hosts can bypass the bounded Skode log and obtain the full graph directly:
const char *text = skred_voice_graph(0, 1, 0);
In WebAssembly:
const ptr = Module.ccall(
"skred_voice_graph", "number",
["number", "number", "number"],
[0, 1, 0]
);
const graph = Module.UTF8ToString(ptr);
The returned pointer uses an internal buffer and remains valid until the next graph request. Consumers should copy it before issuing another request.
# Prototype: root plus octave layer.
v0 w0 a0 t.01,.15,.7,.35 N0 G1 H1
v1 w0 a0 t.005,.1,.5,.25 N12
# Four instances occupy voices 8..15.
/pg 0,0,2,0
/pp 0,0,8,4,0
# Inspect the remapped first instance.
/vg 8
# Play and release a chord.
pn 0,1,60,.8
pn 0,2,64,.75
pn 0,3,67,.7
pr 0,1
pr 0,2
pr 0,3
See examples/poly-layer.sk and examples/poly-mono.sk for copyable examples.