This document describes the Skode command language implemented by
skode.c, skode-event.c, and the synth and sequencer modules behind
them.
Skode is compact by design. A command name is usually one to four punctuation or letter characters followed by numeric arguments. Arguments may be separated by commas or whitespace, and commands may be placed next to each other:
v0 w0 f220 a0 l1
v1n60l1
Strings are written in square brackets and numeric data arrays in parentheses:
[v0 n45 l1] xa
[sk/drums-kick.ks] /ks
(0 0.5 1 0.5 0) /d300,44100,1
Skode is case-sensitive. For example, a controls amplitude while A
controls amplitude modulation.
The main entry point is:
int skode_consume(char *line, skode_t *ctx);
The execution path is:
skode_consume() passes text to the ands parser.skode_callback() receives parser events.skode_function().skode_compile_program().event_program_t and event_t
values.skode_execute_event() resolves register operands and calls
skode_execute_voice_opcode().amp_set(),
wave_set(), or envelope_set().This division matters: immediate commands may use strings, files, allocation, or parser state. Scheduled commands must be representable as bounded numeric opcodes and may run from the audio callback.
vN selects voice N in the current skode_t context.$N reads register N. There are ANDS_VAR_MAX (128) registers.=N,value writes a register. This command is schedulable.- is an explicit default value for commands that support it, notably
n- and N-.+delay command defers by a tempo-relative amount.~seconds command defers by seconds.SEQ_PROGRAM_OP_MAX (32) operations.SEQ_OPCODE_ARG_MAX (8) arguments.Register operands remain symbolic in compiled programs and are resolved when the event executes. This permits commands such as:
=0,3 v$0 n60 l1
The commands in this section can be compiled for queues, repeats, deferred execution, and sequence steps. Feature-gated commands are available only when their named build option is enabled.
| Command | Arguments | Opcode | Function or state changed | Feature |
|---|---|---|---|---|
v |
voice |
SKODE_OP_VOICE |
Changes the current voice inside the compiled program | base |
a |
dB |
SKODE_OP_AMP |
amp_set() |
base |
f |
Hz |
SKODE_OP_FREQ |
freq_set() |
base |
fb |
val |
SKODE_OP_FREQ_BEND |
freq_bend_set(); frequency bend normalized value (-1.0 to 1.0) |
base |
fbp |
range [, offset] |
SKODE_OP_FREQ_BEND_PARAM |
freq_bend_param_set(); frequency bend range (semitones) and optional offset |
base |
ab |
val |
SKODE_OP_AMP_BEND |
amp_bend_set(); amplitude bend normalized value (-1.0 to 1.0) |
base |
abp |
range [, offset] |
SKODE_OP_AMP_BEND_PARAM |
amp_bend_param_set(); amplitude bend range (dB) and optional offset |
base |
n |
note [, cents] |
SKODE_OP_MIDI_NOTE |
skode_midi_note() then freq_midi() |
base |
p |
pan |
SKODE_OP_PAN |
pan_set() |
base |
l |
velocity |
SKODE_OP_VELOCITY |
skode_envelope_velocity() and linked voices |
base |
___l |
velocity |
SKODE_OP_ENVELOPE_VELOCITY |
envelope_velocity() without link propagation |
internal |
A |
[voice, depth [, offset]] |
SKODE_OP_AMP_MOD |
amp_mod_set(); fewer than two args disable AM |
AM |
b |
[direction] |
SKODE_OP_WAVE_DIRECTION |
wave_dir(); no argument toggles |
base |
B |
[loop] |
SKODE_OP_WAVE_LOOP |
wave_loop(); no argument toggles |
base |
BC |
count |
SKODE_OP_WAVE_LOOP_COUNT |
Sets bounded one-shot loop repeats; 0 means unlimited |
base |
WL |
wave,start,end |
immediate | wave_loop_points_set() |
base |
VS |
[start,end] |
SKODE_OP_WAVE_RANGE_SET |
voice_wave_range_set() or voice_wave_range_reset() |
base |
VL |
[start,end] |
SKODE_OP_WAVE_LOOP_SET |
voice_loop_points_set() or voice_loop_points_reset() |
base |
pn |
pool,key,note,velocity[,cents] |
SKODE_OP_POLY_NOTE |
Allocates/updates a group instance and applies pitch plus gate | base |
pr |
pool,key[,release-velocity] |
SKODE_OP_POLY_RELEASE |
Releases a keyed pool allocation | base |
pb |
pool,key,semitones[,cents] |
SKODE_OP_POLY_BEND |
Applies keyed or pool-wide pitch bend | base |
c |
[mode [, amount]] |
SKODE_OP_PHASE_DISTORTION |
Sets signed base PD amount; zero args disable PD | PD |
C |
[voice, depth] |
SKODE_OP_PHASE_MOD |
cmod_set(); fewer than two args disable modulation |
PD |
ct |
attack, decay, sustain, release |
SKODE_OP_PHASE_ENVELOPE |
Configures the PD envelope for its next trigger | PD |
cd |
depth |
SKODE_OP_PHASE_ENVELOPE_DEPTH |
Sets the signed PD-envelope depth | PD |
ft |
attack, decay, sustain, release |
SKODE_OP_FILTER_ENVELOPE |
Configures the filter envelope for its next trigger | FILT, FADSR |
fd |
depth |
SKODE_OP_FILTER_ENVELOPE_DEPTH |
Sets sv.filter_env_depth |
FILT, FADSR |
F |
[voice, depth [, offset]] |
SKODE_OP_FREQ_MOD |
freq_mod_set(); zero or one arg disables FM |
FM |
FF |
mode |
SKODE_OP_FREQ_MOD_MODE |
0 relative FM, 1 absolute hertz, 2 lookup-phase modulation |
FM |
FB |
amount |
SKODE_OP_FREQ_FEEDBACK |
Sets FF2 two-sample operator feedback in radians, 0..7 |
FM |
g |
time |
SKODE_OP_GLISSANDO |
Sets sv.glissando_enable and sv.glissando_time |
GLISS |
G |
voice [, voice ...] |
SKODE_OP_LINK_MIDI |
Sets up to four sv.link_midi_* voices |
base |
h |
ratio [mode] |
SKODE_OP_SAMPLE_HOLD |
Sets sv.sample_hold_ratio and sv.sample_hold_mode |
SAH |
H |
voice [, voice ...] |
SKODE_OP_LINK_VELOCITY |
Sets up to four sv.link_velo_* voices |
base |
L |
seconds |
SKODE_OP_TRIGGER_DELAY |
Sets sv.link_trig and sv.link_trig_samp |
base |
J |
mode [character] |
SKODE_OP_FILTER_MODE |
Sets filter mode and calls mmf_set_params() |
FILT |
K |
Hz |
SKODE_OP_FILTER_FREQ |
mmf_set_freq() |
FILT |
k |
mode |
SKODE_OP_ENVELOPE_MODE |
Sets sv.amp_envelope_mode |
ADSR |
m |
state |
SKODE_OP_MUTE |
wave_mute(); suppresses master output while preserving an r1..r4 dry stem route |
base |
N |
semitones [, cents] |
SKODE_OP_MIDI_DETUNE |
Sets sv.midi_transpose and sv.midi_cents |
base |
P |
[voice, depth [, offset]] |
SKODE_OP_PAN_MOD |
pan_mod_set(); fewer than two args disable modulation |
PANMOD |
q |
bit-depth [curve] |
SKODE_OP_QUANTIZE |
wave_quant() |
CRUSH |
Q |
resonance |
SKODE_OP_FILTER_RESONANCE |
mmf_set_res() |
FILT |
r |
track |
SKODE_OP_RECORD_TRACK |
synth_record_track_set() |
TRACKS, RECORD, or SCOPE |
rt |
[name] track |
immediate | synth_track_name_set() |
TRACKS, RECORD, or SCOPE |
rv |
track,dB |
immediate | synth_track_volume_set() |
TRACKS, RECORD, or SCOPE |
?r |
none | immediate | show track names, volumes, and assigned voices | TRACKS, RECORD, or SCOPE |
s |
amount |
SKODE_OP_SMOOTHER |
Enables or disables amplitude smoothing | SMOOTHER |
S |
voice |
SKODE_OP_VOICE_RESET |
wave_reset() |
base |
t |
attack, decay, sustain, release |
SKODE_OP_ENVELOPE |
Configures the amplitude envelope for its next trigger | ADSR |
T |
none | SKODE_OP_TRIGGER |
Calls envelope_velocity(..., 1) on the voice and velocity links |
base |
w |
wave [, interpolate [, mode]] |
SKODE_OP_WAVE |
wave_set() and optional voice flags; mode 0 is cycle and 1 is one-shot |
base |
> |
destination-voice |
SKODE_OP_VOICE_COPY |
voice_copy(current, destination) |
base |
/ |
none | SKODE_OP_WAVE_DEFAULT |
wave_default() |
base |
= |
register, value |
SKODE_OP_VARIABLE_SET |
Writes global_var[register] |
base |
XM |
voice [, amount] |
SKODE_OP_RING_MOD |
Sets sv.ring_osc and sv.ring_amount |
XM |
pn, pr, and pb operate on pools configured by the immediate /pg,
/pp, and /pm commands. Their integer note key identifies a lifetime rather
than a pitch. They are ordinary bounded opcodes, so patterns and queued events
can use them without parsing text in the audio callback. See
POLYPHONY.md for allocation and monophonic semantics.
n accepts a MIDI note number and optional cents offset. n- reuses the
voice’s last MIDI note. skode_midi_note() also propagates the note to voices
configured by G.
l applies velocity immediately or schedules it using the voice’s trigger
delay. It also propagates velocity to voices configured by H.
b, B, BC, and l have separate playback responsibilities. b selects
direction mode: b0 forward, b1 backward, and b2 ping-pong. B
configures wrapping, and BC configures a bounded number of one-shot
traversals. Zero is unlimited; a positive count means repeats after the initial
loop traversal, so BC1 permits one repeat and two traversals. In ping-pong
mode, each trip from one boundary to the other counts as one traversal.
Positive l or T snapshots the BC bound for the new note. B remains an
immediate runtime switch. l0 emits the release lifecycle event and asks any
active one-shot loop, bounded or unbounded, to leave the loop at the next
boundary in the current playback direction.
One-shot loop entry always includes the pre-loop segment. Forward playback
starts at physical sample 0, plays to loop_end, then wraps to loop_start.
Backward playback starts at the physical last sample, plays to loop_start,
then wraps to loop_end. Ping-pong playback starts forward, reverses at
loop_end, reverses again at loop_start, and repeats. l0 and BC
exhaustion leave the loop at the next boundary reached by the current leg and
play the remaining physical tail in that same direction.
B0 clears active loop runtime state. B1 immediately starts a fresh active
loop snapshot from the current BC configuration, so stale counted-loop
remaining state does not carry into a later unbounded loop.
WL wave,start,end sets the loop boundaries stored on the wave itself.
start is inclusive, end is exclusive, and end may equal the wave sample
count. Voices already using that wave receive the updated loop points unless
they have a voice-level override.
VS start,end overrides the selected voice’s playable sample range after its
w assignment. VS with no arguments resets the selected voice to the full
current wave range. If narrowing the range would leave inherited loop points
outside the playable region, the voice loop falls back to the new VS range.
VL start,end overrides loop points on the selected voice after its w
assignment. VL with no arguments clears the override and reapplies the
current wave’s WL defaults when they fit inside the active VS range;
otherwise it falls back to the active VS range. Voice display emits
VSstart,end and VLstart,end only for voices with active overrides, so saved
voice lines stay compact but pasteable. VS and VL do not enable looping;
the voice still needs B1 or BC count for those points to affect l1
playback.
Amplitude envelope mode k1 enables timed one-shot ASR. For non-looping
one-shots and bounded BC one-shot loops, positive l schedules the release
phase to finish at the natural playback end. The t attack, sustain, and
release values shape the result; decay remains available but is usually not
useful for this mode. Unbounded BC0/B1 loops keep normal held ADSR behavior
until l0, then exit the loop in the current direction and play the remaining
wave tail.
At trigger time, osc_trigger() initializes loop_active, loop_bounded, and
loop_remaining. osc_next() consumes wraps in either direction, including
multiple boundaries crossed by one phase increment. Natural bounded-loop
exhaustion raises loop_ended; the render loop consumes that event to publish
one VOICE_RELEASE control-plane event while playback continues through the
one-shot tail. An explicit l0 publishes its release event immediately, then
exits the loop at the next boundary without publishing a duplicate boundary
release event.
c phase-distortion modes are:
| Mode | Shape |
|---|---|
0 |
off |
1 |
saw to pulse |
2 |
folded sine |
3 |
triangle |
4 |
double sine |
5 |
saw to triangle |
6 |
resonant 1 |
7 |
resonant 2 |
The public PD amount range is -1..1, with 0 as the exact undistorted
phase for every mode. The render path computes c amount + C modulation +
ct envelope * cd depth, then clamps the result to -1..1. Negative amounts
apply the complementary direction of each shape. ct 0 0 1 0 disables the PD
envelope; cd may be positive or negative.
w changes the selected wavetable. The optional second and third arguments
set interpolation and one-shot playback flags.
S takes an explicit voice number rather than using the selected voice. In
the current implementation, an out-of-range voice invokes voice_init() and
resets every voice; S100 therefore acts as reset-all with the default voice
limit. This sentinel behavior is retained for compatibility but is not a
dedicated reset-all command.
EXEC id [args...] Direct execution/compilation of opcode id (0-255).
Re-uses the argument arity and execution path of the underlying opcode.
These commands run on the control thread but compile their string contents into the opcode representation above.
| Command | Input | Behavior | Main functions |
|---|---|---|---|
+delay ... |
deferred commands | Tempo-relative defer | skode_defer(), skode_queue_program() |
~seconds ... |
deferred commands | Seconds-relative defer | skode_defer(), skode_queue_program() |
[commands] R count,seconds[,tag] |
string | Repeat using seconds | skode_compile_program(), skode_queue_program() |
[commands] RR count,steps[,tag] |
string | Repeat using tempo-relative step duration | skode_compile_program(), skode_queue_program() |
eR macro,count,seconds[,tag] |
external string | Compile a macro snapshot and repeat it using seconds | skode_extra_copy(), skode_compile_program(), skode_queue_program() |
eRR macro,count,beats[,tag] |
external string | Compile a macro snapshot and repeat it using tempo-relative beat duration | same as above |
[commands] DO? value[,tag] |
string | Queue once when value > 0 |
skode_compile_program(), skode_queue_program() |
R! tag |
numeric | Remove queued events with a tag | seq_kill_by_tag() |
R!! |
none | Remove all queued events | seq_kill_all() |
[commands] e! |
string | Compile and queue the parser string now | skode_compile_program(), skode_queue_program() |
e! index |
external string | Expand a literal external buffer while compiling, then queue it | same as above |
?o |
none | Show queued opcode events | opcode_queue_show() |
?o pattern[,step] |
numeric | Show compiled pattern programs | opcode_pattern_show() |
Commands containing strings, arrays, file operations, or other immediate-only
operations are rejected by skode_compile_program(). There is no fallback
that sends command text into the audio callback.
Literal external macro references such as e!12 are the exception: the
compiler reads buffer 12 on the control thread and inlines its compiled
opcodes. This works inside patterns, defers, repeats, and nested macros.
Expansion uses snapshot semantics, so editing buffer 12 later does not change
an existing pattern or queued event. Undefined buffers, recursive cycles,
e!$N, and expansions beyond 32 operations are rejected. Argumentless e!
continues to mean “execute the current parser string” and is immediate-only.
eR and eRR use the same snapshot compilation but schedule the resulting
program repeatedly without expanding it once per repetition.
Sequence support is compiled under the SEQ feature.
| Command | Arguments or string | Behavior | Main function or state |
|---|---|---|---|
M |
bpm [subdivision] |
Set tempo (BPM) with optional step subdivision per quarter note (default 16 for 16th-note drum steps; 8 for 8th notes, 4 for quarter notes, 32 for 32nd rolls). | tempo_set_subdivision() |
y |
pattern |
Select the editing pattern | ctx->pattern |
[text] yt |
string | Name the selected pattern | seq_text[] |
ym |
0/1 |
Mute the selected pattern | seq_mute_set() |
Y |
pattern |
Clear a pattern | pattern_reset() |
[commands] xa |
string | Append a compiled step | seq_step_append() |
[commands] x step |
string | Set a compiled step; x- advances the edit cursor |
seq_step_set() |
[-N] x step |
string (-0..-127) |
Cross-pattern wait step: holds pattern at step until target pattern N reaches step 0. Emits SKRED_CONTROL_EVENT_PATTERN_WAIT (code 8). |
seq_step_set() |
<x step |
numeric | Copy a step’s source text into the parser string | seq_step_get() |
zg step, z g step |
step |
Immediately jump pattern playback pointer to a specific step | seq_step_goto() |
zq |
0/1 |
Queue pattern start (zq1) or stop (zq0) on next downbeat (seq_pointer[0] == 0) |
seq_state_queue() |
%, z% |
modulus |
Set selected pattern step clock division modulus (default 1 per 16th-note tick; e.g. z%16 16th notes, z%8 8th notes, z%32 32nd notes) |
seq_modulo_set() |
z* |
count |
Step ratchet opcode: subdivide step duration into count micro-triggers (z*2, z*3, z*4, z*8) |
skode_program_push() |
z |
[0..3] |
Set selected pattern state (0=stop, 1=start phase-aligned at step 0) |
seq_state_set(), pattern_show() |
z? |
none | Show selected pattern | pattern_show() |
Z |
[0..3] |
Set all pattern states (0=all stop, 1=all start phase-aligned at step 0; empty pattern slots omitted) |
seq_state_all(), pattern_show() |
Z?, z?? |
none | Show all populated patterns with steps (empty pattern slots omitted) | pattern_show() |
/SS |
stream |
Set stream from stack array data | skode_stream_set() |
/SC |
dst, src |
Copy stream | SKODE_OP_STREAM_COPY, skode_stream_copy() |
/SM |
stream, mode |
Set stream playback mode | SKODE_OP_STREAM_MODE, skode_stream_mode() |
/SP |
stream, pos |
Set stream playback position | SKODE_OP_STREAM_POS, skode_stream_pos() |
Sequence steps retain their source text for editing and diagnostics, but
playback uses the compiled event_program_t. Each pattern keeps its own
persistent current voice during playback. Clearing a pattern resets that
selection to voice 0 before the pattern next runs.
Tempo is limited to 960 BPM. At four sequence steps per beat this is 64 steps per second, which remains practical for control-rate sequencing while avoiding unbounded catch-up work in the audio callback. If processing falls behind, SKRED executes at most 64 missed pattern ticks in one callback and resumes from the current timeline.
The audio callback renders adaptively around queued events and pattern clock ticks. A block containing a timing boundary is split at that sample; a block without one is rendered in a single call. Integer-sample events are exact, and fractional tempo boundaries are rounded forward by less than one sample. Device and hardware buffering latency is unaffected.
The following commands are immediate-only even when related voice opcodes are schedulable.
| Command | Arguments or string | Behavior | Main function or state |
|---|---|---|---|
V |
dB |
Set main output volume | volume_set() |
[name] vt |
string | Set selected voice label | sv.text[] |
[name] wt wave |
string, numeric | Set wavetable label | sw.name[] |
W |
[wave [, end-or-width [, height]]] |
Show wavetable or recording data, including loop metadata and one-shot baseline duration for single-wave displays | wavetable_show(), waveform display helpers |
VW |
[voice [, width, height]] |
Show the wavetable assigned to one voice with its current loop markers | wavetable_waveform_show() |
WS |
wave |
Show a compact spectrogram over a wavetable loop region | wavetable_spectrogram_show() |
W* |
wave,param[,register] |
Read wave size, rate, duration, loop start, or loop end | sw, register write |
v* |
param[,register] |
Read selected voice wave, amplitude, or frequency | sv fields, register write |
ds |
amount |
Set selected voice send amount to the delay owned by its r1..r4 record/scope track; effective only when p0 and pan modulation is off |
delay_send_set() |
DL |
track,coarse,fine,feedback,modfreq,moddepth,level |
Set the DW-style mono-send/stereo-return delay attached to record/scope track 1..4 |
delay_params_set() |
DG |
track [bits] [native] |
Set the bit depth reduction applied to the delay line for the specified track | delay_grit_set() |
DL? |
[track] |
Show one track delay or all four track delays as copy/pasteable DL... commands |
delay_format(), delay_bus_format() |
GS |
[full] |
Show copy/pasteable version, master volume, tempo, and track delays; full > 0 adds a larger text snapshot |
global_status_show() |
[filename] GS> |
string | Save the complete current REPL/session state as an ordinary versioned ZIP | skode_session_save() |
[filename] GS< |
string | Validate and restore a complete session ZIP | skode_session_load() |
w>d |
wave |
Copy wavetable samples to parser data | sw, ands_data_resize() |
w>r |
wave |
Copy wavetable samples to recording buffer | skode_sample_alloc() |
[filename] w>w wave |
string, numeric | Write wavetable samples unchanged at their stored sample rate | miniaudio encoder API |
d>r |
none | Copy parser data to recording buffer | skode_sample_alloc() |
w! |
none | Apply current recording frame offset and trim | recording-buffer state |
w* |
none | Reset recording offset and trim | recording-buffer state |
w> |
[frames] |
Move recording start offset | sampling.offset |
w< |
[frames] |
Increase or decrease end trim | sampling.trim |
w<> |
[threshold [, end-threshold [, margin-frames]]] |
Find recording trim points using a default 0.001 silence threshold, four consecutive audible frames, and optional frame margin |
record_find_trim() |
/r |
[slot [, mode [, channel]]] |
Load recording into a mono wave; stereo defaults to a downmix, or channel 0/1 selects left/right |
rec_load() |
/d |
[slot [, rate [, mode [, offset]]]] |
Load parser data into a wave slot; defaults to cycle mode | data_load() |
/wex |
wave |
Expand a dynamic wave slot in the 200-999 range | wave_table_dynamic_expand() |
For W*, parameter 0 is sample count, 1 is sample rate, 2 is duration
(size / rate), 3 is loop start, and 4 is loop end. For v*, parameter
0 is wave index, 1 is user amplitude, and 2 is frequency.
Single-wave W output prints a labeled marker row under the waveform using
the same [loop_start..loop_end) convention as WL; the header includes the
baseline duration at the wave’s stored playback rate and the loop duration.
| Command | Arguments | Behavior | Main function |
|---|---|---|---|
dup |
numeric stack | Duplicate the first pending argument for the next atom | ands_arg_dup() |
drop |
numeric stack | Drop the first pending argument for the next atom | ands_arg_drop() |
swap |
numeric stack | Swap the first two pending arguments for the next atom | ands_arg_swap() |
over |
numeric stack | Copy the second pending argument to the front | ands_arg_over() |
rot |
numeric stack | Rotate the first three pending arguments left | ands_arg_rot() |
clr |
numeric stack | Clear pending arguments before the next atom | ands_arg_clear() |
D |
[capacity] |
Show or increase parser data capacity | ands_data_cap(), ands_data_resize() |
/D |
[capacity] |
Resize data and print capacity details | ands_data_resize() |
?d |
none | Print parser data | skode_double_dump() |
d* |
index |
Print one data element | ands_data() |
=d |
register,index |
Copy a data element to a shared register | register write |
= |
[register [, value]] |
Set, inspect, or list shared registers | register read/write |
*= |
register,a,b |
Store a * b |
register write |
/= |
register,a,b |
Store a / b when b != 0 |
register write |
a= |
register,a,b |
Store a + b |
register write |
s= |
register,a,b |
Store a - b |
register write |
The two meanings of = share syntax. =N,value is also accepted by the
scheduled opcode compiler and writes the shared register array when executed.
The immediate read/math forms d*, W*, v*, =, *=, /=, a=, and
s= also return their result as the next command’s pending first argument
when they are followed by another atom in the same chunk.
ANDS macros are global named commands. Define them with [name] : body ;.
Names use the same four-character atom width as commands; longer definition
names are silently truncated to four characters. Macro arguments are written
as $$0..$$7. @0..@9 now read parser-local return values and are not
macro-parameter aliases. Plain $0 keeps its normal meaning as a shared
register reference. Because macros are global, scripts loaded with /l can
define macros for later interactive use.
[ar] : t $$0 0 $$1 0 ;
ar 0.01 0.4
The example above compiles to the same opcodes as t 0.01 0 0.4 0, which
configures an attack/release-style amplitude envelope. Definition uses the
real compiler: ?m reports realtime when the bounded program is cached in
the dictionary, and immediate when the body must retain text expansion.
Invalid and oversized bodies are reported separately.
Macros can bundle small voice recipes:
[one] : w $$0 1 1 k1 t $$1 0 $$2 $$3 ;
one 12 0.002 1 0.08
That selects wave 12, enables interpolation and one-shot playback, enables
timed ASR mode, and configures the envelope. Macro definitions can be inspected
and removed from the current parser context:
?m
[one] /m
/m!
Read commands can feed later commands in the same chunk. The return value is left as the next command’s first pending argument:
1 v* a # read selected voice amplitude and feed it to a
0 3 4 a= a # store 3+4 in $0, then feed 7 to a
(2 5 8) 1 d* f
Immediate macros can return a tuple explicitly:
[pair] : *R .1 .2 ;
pair
?R # non-consuming: @0=.1, @1=.2
@0 a
*R replaces the return tuple with its arguments. @0..@9 read it, and
?R displays it without changing it. These parser-local operations have no
scheduled representation, so macros containing them are immediate.
The stack helpers rearrange pending numeric arguments before the next atom:
0 5 swap v # v receives 5 instead of 0
7 3 drop v # v receives 3
4 2 over v # v receives 2, with the copied value at the front
1 6 7 rot v # v receives 6
Context-local string slots are useful scratch pads that do not use the global external string buffers:
[lead voice] 0 s>
[temporary] vt
0 <s vt
s?
Runtime string formatting replaces @0..@7 in the current parser string
with numeric arguments:
[take-@0.wav] 12 s% /rg 10
[voice @0 wave @1] 3 14 s% vt
Skode has SKODE_EXTRA_MAX (128) external string buffers of 256 bytes each.
| Command | Arguments or string | Behavior | Main function |
|---|---|---|---|
[text] e> index |
string | Copy parser string to external buffer | skode_copy_string() |
<e index |
numeric | Copy external buffer to parser string | ands_string_from_external() |
e? [index] |
optional numeric | Show one or all non-empty buffers | direct buffer inspection |
[text] s> index |
string | Copy parser string to a context-local string slot | ctx->string_slot[] |
<s index |
numeric | Copy a context-local string slot to parser string | ands_string_from_external() |
s? [index] |
optional numeric | Show one or all non-empty context-local string slots | direct slot inspection |
[template] s% args... |
string and numeric | Replace @0..@7 in parser string with numeric args |
skode_format_string_args() |
e! index |
literal numeric | Compile and inline a macro buffer; schedulable | skode_extra_copy(), skode_compile_program() |
eR index,count,seconds[,tag] |
numeric | Repeat a compiled macro snapshot using seconds | skode_repeat_macro() |
eRR index,count,beats[,tag] |
numeric | Repeat a compiled macro snapshot using tempo | skode_repeat_macro() |
[file] /ks [verbose] |
string | Load a named Ksynth source file | ksynth_load_name() |
/k file-number[,verbose] |
numeric | Load numbered Ksynth source | ksynth_load() |
[code] ks, [code] k! |
string | Run Ksynth source in this Skode context | skode_ks_eval() |
kw [timeout-ms] |
optional numeric | Compatibility no-op; Ksynth now runs synchronously | parser command |
kw> [timeout-ms] |
optional numeric | Copy latest Ksynth result to parser data | skode_ks_result_to_data() |
k? |
none | Print latest Ksynth result | direct result inspection |
k>d |
none | Copy latest Ksynth result to parser data | skode_ks_result_to_data() |
k>w |
[slot[,rate[,mode[,offset]]]] |
Load latest Ksynth result directly into a wave; defaults to slot 300 and cycle mode | skode_ks_result_to_data(), data_load() |
d>k variable |
numeric 0..25 |
Copy parser data to Ksynth A..Z |
ks_bind_vector() |
w>k wave,variable |
numeric | Copy a wavetable directly to Ksynth A..Z |
ks_bind_vector() |
Ksynth commands require the KSYNTH feature and are immediate-only.
Each vector is limited to one million elements. Each Skode command context
owns its own lazy Ksynth context and persistent A..Z variables; local input
and each UDP client therefore keep independent Ksynth state. kw remains
accepted for older scripts, but no longer waits for a worker. kw> and k>d
copy the latest numeric result into parser data. k>w combines that copy with
wave installation. Wavetable metadata is not part of the transferred Ksynth
vector, so k>w arguments supply the desired rate and playback mode.
| Command | Arguments or string | Behavior | Main function |
|---|---|---|---|
/l file-number[,verbose] |
numeric | Load a numbered .sk file |
skode_load() |
[filename] /ls [verbose] |
string | Load a named Skode file; bare names also fall back to sk/filename |
skode_load_name() |
[filename] /ws wave[,channel] |
string | Load an audio file into a writable wave slot | wave_load_string() |
/w file-number[,wave[,channel]] |
numeric | Load a numbered audio file | wave_load() |
<r seconds[,source[,voice]], ^r ... |
numeric | Record source 0 dry mono, 1 selected voice, or 2 audible stereo master |
skode_sample_go() |
[filename] >r |
string | Normalize the completed mono or stereo sample buffer and write the named WAV file | miniaudio encoder API |
[filename] /rg [max-seconds] |
string | Start multitrack recording | recorder_start() |
/rs |
none | Stop multitrack recording | recorder_stop() |
/r? |
none | Show multitrack recorder status | recorder status functions |
[name] /sg [channel-mask[,buffer-seconds]] |
string and numeric | Start shared-memory audio publication | scope_ipc_start() |
/ss |
none | Stop and unlink shared-memory publication | scope_ipc_stop() |
/s? |
none | Show shared-memory publication status | scope IPC status functions |
[filename] %cat |
string | Print a text file from the active VFS/search path | skode_asset_read() |
[zip-or-directory] %z |
string | Mount a ZIP archive or disk directory | skred_vfs_mount() |
%zu |
none | Unmount and return to real-directory disk mode | skred_vfs_unmount() |
%pwd |
none | Show VFS mode, root, and working directory | skred_vfs_status() |
[directory] %cd |
string | Change VFS working directory | skred_chdir() |
%ls [type] |
optional numeric | List VFS files; 0=.sk, 1=.wav, 2=.mp3, 3=.ks |
skred_opendir(), skred_readdir() |
Multitrack file commands require RECORD. Shared-memory publication commands
require SCOPE. The r stem-routing command is available with either feature.
/als and /a? refresh/list devices and show status. /ao selection chooses
playback and /ai selection chooses capture; -1 means default and capture
selection -2 means off. These are immediate Skode commands and therefore work
from files and immediate macros. The legacy host aliases /aout default and
/ain off remain at the API boundary but are not Skode commands.
With MIDI=1, MO status[,data1[,data2]] sends one to three raw bytes to the
open MIDI output. d>MO sends the current data array as raw bytes, including a
complete SysEx buffer. Both are immediate commands and validate every element
as an integer byte. MIDI endpoint commands /mL, /mi, /miV, /mic, /mo,
/moV, /moc, and /m? are also genuine immediate Skode commands, so they
work through the public API, loaders, and immediate macros. /mv
channel,voice[,bend] and /mp channel,pool[,bend] install
control-plane input routes (. or - selects every channel); /mvd and /mpd
remove routes, /mR lists them, and /mC clears them. Poly input targets the
pool rather than its group template. Generic [command] /mb
type,channel,data1
bindings use the numeric SKRED_MIDI_* type values, filter any fixed-size MIDI
message, and expand {ch}, {d1}, {d2},
{unit}, or {bend} into arbitrary Skode; /mb?, /mbd, and /mbC inspect,
remove, and clear these bindings. These names deliberately fit Skode’s
four-character atom limit.
| Command | Arguments | Behavior | Main function or state |
|---|---|---|---|
?, v? |
none | Show selected voice | voice_show() |
\ |
none | Show selected voice verbosely | voice_show() |
??, v?? |
none | Show active voices | voice_show_all() |
?s |
none | Show parser string | ands_string() |
?m |
none | Show global ANDS macros | ands_macro_get() |
[name] /m |
string | Remove one global ANDS macro | ands_macro_remove() |
/m! |
none | Clear all global ANDS macros | ands_macro_clear() |
GS |
[full] |
Show copy/pasteable version, master volume, tempo, and track delay commands; full > 0 adds a larger text snapshot |
global_status_show() |
[filename] GS> |
string | Save settings, writable waves, recording buffer, variables, macros, KSynth state, and control/MIDI mappings to ZIP | skode_session_save() |
[filename] GS< |
string | Restore a validated session ZIP into the receiving parser context | skode_session_load() |
/s [section] |
optional numeric | Show system, audio, synth, Skode, string, or benchmark state | system_show() and related helpers |
/t [level] |
optional numeric | Toggle or set command/parser tracing | ctx->trace, ands_trace_set() |
/v [level] |
optional numeric | Toggle or set verbose output | ctx->verbose |
/f [value] |
optional numeric | Show or set context flag | ctx->flag |
log state |
numeric | Enable or disable context log capture | ctx->log_enable |
udp value |
numeric | Show UDP endpoint information | context fields; UDP feature |
wait ms |
numeric | Block the control thread | sk_sleep() |
/m_ |
none | Benchmark selected voice | synth_voice_bench(); BENCH feature |
/q |
none | Request shell exit | ctx->quit |
I currently parses a numeric argument but is a stub; it does not enable an
event logger.
Commands enclosed by #ifdef ... in the C sources only exist when those
features are enabled. Important command features include:
| Feature | Commands |
|---|---|
ADSR |
k, t |
AM |
A |
BENCH |
/m_ and extra /s sections |
CRUSH |
q |
FADSR with FILT |
ft, fd |
FILT |
J, K, Q |
FM |
F, FF, FB |
GLISS |
g |
KSYNTH |
/ks, /k, ks, k!, kw, kw>, k?, k>d, k>w, d>k, w>k |
MIDI |
/mL, /m?, /mi, /miV, /mic, /mo, /moV, /moc, MO, d>MO, /mv, /mvd, /mp, /mpd, /mR, /mC, /mb, /mb?, /mbd, /mbC |
PANMOD |
P |
PD |
c, C, ct, cd |
RECORD |
r, rt, rv, ?r, /rg, /rs, /r? |
SCOPE |
r, rt, rv, ?r, /sg, /ss, /s? |
TRACKS |
r, rt, rv, ?r, ds, DL, DL? |
SAH |
h |
SEQ |
timing, queue, and pattern commands |
SMOOTHER |
s |
UDP |
udp |
XM |
XM |
MIDI management, route, binding, and output atoms are present even without
MIDI=1 so scripts receive a stable unavailable-backend result. The actual
backend, device access, and MIDI feature report require MIDI=1.
The normal WASM build enables the voice and sequence features listed in
WASM_KIT_OPTS in Makefile, but does not enable every optional command.
| Area | Source |
|---|---|
| Parser context and public Skode API | skode.h |
| Immediate command dispatch | skode.c, skode_function() |
| Dictionary word registration and lookup | skode-dict.c, skode-dict.h |
| Scheduled command compiler | skode-event.c, skode_compile_program() |
| Legacy hardcoded opcode real-time check | skode-event.c, skode_is_legacy_realtime_opcode() |
| Event and program execution | skode-event.c, run_program() |
| Voice opcode dispatch | skode.c, skode_execute_voice_opcode() |
| Synth functions and state | synth.c, synth.h |
| Queue implementation | skqueue.c, skqueue.h |
| Sequence implementation | seq.c, seq.h |
| Parser implementation | ands.c, ands.h |
| Scheduled-opcode design notes | OPCODES.md |
There are two paths for new commands. Choose based on whether you are extending the dictionary system or working with a legacy hardcoded opcode.
Preferred: dictionary-registered word (new additions)
skode_word_t in skode-dict.c with the appropriate .safety..safety = WORD_REAL_TIME_SAFE and either:
.opcode_id to an existing skode_opcode_t value (no new opcode needed), or.compile to a custom compile callback and add an opcode entry in skode.h..execute for the immediate execution path..safety = WORD_IMMEDIATE_ONLY and only .execute.Legacy path (modifying existing hardcoded commands in skode-event.c)
The legacy switch-based compiler in skode-event.c handles many voice
commands that predate the dictionary system. These words have .safety =
WORD_IMMEDIATE_ONLY in their skode_word_t struct to prevent the dictionary
compiler from handling them, but they are real-time safe through the legacy
path. The bridge is skode_is_legacy_realtime_opcode() in skode-event.c.
When adding to or modifying the legacy path, update all of the following:
skode_opcode_t in skode.h.skode_opcode_name() in skode-event.c.skode_compile_callback() in skode-event.c.skode_opcode_supported() in skode.c.skode_execute_voice_opcode() in skode.c.skode_function().skode_is_legacy_realtime_opcode() if the word should appear in 1 ?M.