← Back to index · Previous: Clock & tempo
Process blocks are the entry points of an Idƴl program. They are top-level blocks that contain temporal functions, variable bindings, and orchestration logic. The evaluator collects all process blocks and runs them.
freq(dt=100ms) = v |> {
init: { v = 440 }
v = v + 1
}
process: {
osc = osc_out("127.0.0.1", 9000)
f = freq()
osc_send(osc, "/synth/freq", f)
}Function definitions must live at global scope — process blocks can only contain bindings, bare calls, and control statements. This is the simplest program structure.
Giving a process block a name allows selective execution:
process drums: {
// drum pattern logic
}
process bass: {
// bass line logic
}
process lead: {
// lead melody logic
}Run only one:
idyl song.idyl --process drums
# or
idyl song.idyl -p drumsWhen --process is not specified, all
process blocks run.
A process block can have a finite duration:
process intro, dur=8b: {
// runs for 8 beats, then stops automatically
}
process loop: {
// runs indefinitely
}The dur= parameter accepts any time expression — literal
or computed:
process short, dur=2000ms: { ... }
process timed, dur=4b: { ... }When the elapsed time exceeds the duration, the evaluator automatically unsubscribes the process block from the scheduler.
The --listen flag starts the program without running any
process blocks. Instead, it waits for OSC commands to
start and stop them on demand.
idyl song.idyl --listen # default port 9000
idyl song.idyl --listen 9090 # custom port
idyl song.idyl -l # short form
idyl song.idyl -l 9090| OSC address | Arguments | Effect |
|---|---|---|
/idyl/process/start |
name (string) |
Start the named process block |
/idyl/process/stop |
name (string) |
Stop the named process block |
/idyl/process/list |
(none) | Print all stored process block names |
Terminal 1:
idyl song.idyl --listen 9000
# Output:
# idyl: listening on port 9000
# Stored process blocks: drums, bass, leadTerminal 2 (using oscsend or similar):
oscsend localhost 9000 /idyl/process/start s "drums"
oscsend localhost 9000 /idyl/process/start s "bass"
oscsend localhost 9000 /idyl/process/stop s "drums"
oscsend localhost 9000 /idyl/process/list--listen and --processYou can pre-start specific process blocks while in listen mode:
idyl song.idyl --listen --process drumsThis starts drums immediately and waits for OSC commands
to start/stop others.
osc_out(...)) execute once at process startdur=, the block stopsProcess blocks accept both assignments (x = expr) and
bare expression calls (function calls without
assignment). Bare calls run for their side effects:
process: {
out = osc_out("127.0.0.1", 9000)
osc_send(out, "/gate", 1) // bare call — valid
print("gate sent") // bare call — valid
}Note: Bare expression calls are only valid inside process blocks and lambda blocks. Global scope is declaration-only — attempting a bare call at the top level of a file is a parse error.
A program can have any number of process blocks. They all share the global scope but run independently:
show(dt=1000ms) = t |> {
init: { t = 0 }
t = t + 1
}
freq_osc(dt=10ms) = v |> {
init: { v = 440 }
v = v + sin(now() * 0.001) * 100
}
process clock_display: {
t = show()
print("tick:", t)
}
process audio: {
osc = osc_out("127.0.0.1", 9000)
f = freq_osc()
osc_send(osc, "/freq", f)
}start
and stop — process control from within a processA running process block can start or stop another named process block
using the start and stop keywords.
start nameStarts the named process block. The process must have been stored
(either loaded in --listen mode, or defined as a named
block in the same file).
countdown(dt=500ms) = t |> {
init: { t = 0 emit end = _ }
t = t + 1
emit end = _; ! ? (t >= 4)
}
process launcher: {
timer = countdown()
timer catch end: {
start synth // start "synth" when timer ends
}
}
process synth: {
osc = lfo(440hz, 0.8, dt=10ms)
print("synth freq:", osc)
}stop nameStops the named process block, unsubscribing all its temporal instances from the scheduler.
guard(dt=1000ms) = t |> {
init: { t = 0 emit timeout = _ }
t = t + 1
emit timeout = _; ! ? (t >= 10)
}
process watchdog: {
g = guard()
g catch timeout: {
stop audio_loop // stop "audio_loop" after 10 seconds
}
}stop (no name)Used without a name, stop stops all
currently running process blocks:
ticking(dt=100ms) = n |> {
init: { n = 0 emit done = _ }
n = n + 1
emit done = _; ! ? (n >= 5)
}
process oneshot: {
counter = ticking()
counter catch done: {
print("done, stopping everything")
stop // stops all running processes
}
}start and stop take effect immediately —
the next scheduler tick of the target process will either fire (start)
or not fire (stop).on blocks — trigger
reactionsAn on block fires its body every time a trigger
expression is live (!). On rest ticks, the
body is skipped entirely.
process: {
m = metro(dt=100ms)
on m: {
print("fired")
}
}This is equivalent to checking m manually in a reaction,
but reads as a clear intent: “when m fires, do this”.
The braces are optional for a single statement:
process: {
m = metro(dt=500ms)
on m: print("tick")
}on blocks compose naturally with flow gates. Use a
flow’s trigger member as the guard:
import("stdlib")
flow pattern = {
rhythm : [!, _, _, !, !, _]
melody on rhythm : [60, 63, 65]
}
process: {
m = metro(dt=200ms)
p = pattern[m]
on p.rhythm: {
print("note:", p.melody)
}
}An on block is a reaction — it is
placed in the scheduler segment that drives its trigger expression. If
the expression references multiple temporal sources, the block is placed
in all of them (the same redistribution logic that governs other
reactions applies).
catchon expr: { } |
expr catch event: { } |
|
|---|---|---|
| Fires when | expression is a live trigger | instance emits a named event |
| Guard | value type (trigger/rest) | event name from emit |
| Repeats | every trigger tick | once — deactivated after first fire |