stmadvance
Applies the pending transition request of a runner and moves its clock.
Call it once per control period, after the code for the current node has run.
Syntax
Initialization
irunner -- a runner handle, returned by stminstance.
kstatus -- the outcome of the advance:
| Status |
Meaning |
| 0 |
No request was pending. |
| 1 |
The request was legal and the runner moved to another node. |
| 2 |
The current node has no edge to the requested node. Nothing changed. |
| 3 |
The request was a legal self edge: the runner stays on the same node, but the move counts as a transition. |
| 4 |
Two different nodes were requested in the same control period. Neither is applied. |
| 5 |
The runner is paused. |
| 6 |
The runner is deleted. |
kfrom -- the id of the current node before the advance.
kto -- the id of the requested node. It is -1 when no single node was requested (status 0 and 4) and when the runner is paused or deleted.
The pending request is cleared whatever the outcome. A transition (status 1 or 3) restarts the node clock and is recorded in the event history, where stmonenter, stmonexit and stmevent see it.
Each call also adds one control period to the runner's clock, which is why it should be called once per control period. The period is that of the instrument or UDO that calls it. The clock does not move while the runner is paused or deleted, nor in the control period of an stmreset. Read the clock before stmadvance: after it, the clock shows the values of the next control period.
This opcode changes the runner, so it counts as a writer: a runner accepts writers from one instrument instance at a time. See Writers and observers.
Examples
A scripted run showing every advance status
A scripted run of a small graph. It prints, for each control period, the current node and the graph clock as read before stmadvance, the request made, and what stmadvance returned. Each control period lasts 10 ms. It uses the file stm-trace.csd.
| A scripted run showing every advance status. |
|---|
| <CsoundSynthesizer>
<CsOptions>
-n -d -m0
</CsOptions>
<CsInstruments>
sr = 48000
ksmps = 480
nchnls = 1
0dbfs = 1
; A scripted run of a small graph. Each control period lasts 10 ms.
; The trace shows what stmadvance returns for each kind of request,
; and how the graph clock moves.
; Idle -> Play, Play -> Play (self edge), Play -> Hold, Hold -> Play or Idle
builder:i = stmcreate()
stmaddnode(builder, "Play") ; id 0
stmaddnode(builder, "Idle") ; id 1
stmaddnode(builder, "Hold") ; id 2
stmentry(builder, "Idle") ; start from Idle instead of id 0
stmaddedge(builder, "Idle", "Play")
stmaddedge(builder, "Play", "Play")
stmaddedge(builder, "Play", "Hold")
stmaddcondedge(builder, "Hold", ["Play", "Idle"])
definition:i = stmcompile(builder)
gi_runner@global:i = stminstance(definition)
instr 1
r:i = gi_runner
cycle:k init 0
cycle += 1
if cycle == 1 then
printks("%d nodes, %d edges: %s %s %s\n", 0, stmnodecount(r), stmedgecount(r), \
stmnodename(r, 0), stmnodename(r, 1), stmnodename(r, 2))
printks("cyc node id tick time nodetime request status from to\n", 0)
endif
; read the node and the clock before stmadvance
node:S = stmcurrent(r)
id:k = stmcurrentid(r)
tick:k = stmtick(r)
time:k = stmtime(r)
nodetime:k = stmnodetime(r)
request:S = strcpyk("-")
if cycle == 2 then
stmnext(r, "Play")
request = strcpyk("Play")
elseif cycle == 3 then
stmnext(r, "Idle") ; there is no Play -> Idle edge
request = strcpyk("Idle")
elseif cycle == 4 then
stmnext(r, "Play") ; self edge
request = strcpyk("Play")
elseif cycle == 5 then
stmnext(r, "Hold") ; two different requests
stmnext(r, "Idle")
request = strcpyk("Hold+Idle")
elseif cycle == 6 then
stmnext(r, stmnodeid(r, "Hold")) ; request by id
request = strcpyk("id of Hold")
elseif cycle == 8 then
stmreset(r, 1) ; back to the entry node, Idle
request = strcpyk("reset")
endif
status:k, from:k, to:k = stmadvance(r)
printks("%3d %-5s %3d %5d %5.2f %9.2f %-12s %6d %4d %2d\n", 0, \
cycle, node, id, tick, time, nodetime, request, status, from, to)
if cycle == 9 then
turnoff
endif
endin
</CsInstruments>
<CsScore>
i 1 0 1
e
</CsScore>
</CsoundSynthesizer>
|
Its output is:
3 nodes, 5 edges: Play Idle Hold
cyc node id tick time nodetime request status from to
1 Idle 1 0 0.00 0.00 - 0 1 -1
2 Idle 1 1 0.01 0.01 Play 1 1 0
3 Play 0 2 0.02 0.00 Idle 2 0 1
4 Play 0 3 0.03 0.01 Play 3 0 0
5 Play 0 4 0.04 0.00 Hold+Idle 4 0 -1
6 Play 0 5 0.05 0.01 id of Hold 1 0 2
7 Hold 2 6 0.06 0.00 - 0 2 -1
8 Hold 2 7 0.07 0.01 reset 0 1 -1
9 Idle 1 0 0.00 0.00 - 0 1 -1
Cycle 3 asks for a move that has no edge (status 2), cycle 4 uses the self edge on Play (status 3), and cycle 5 asks for two different nodes (status 4). The node time restarts after every accepted move, including the self transition, but not after the rejected ones. The reset in cycle 8 returns to the entry node Idle, which is id 1, and the clock reads 0 again in cycle 9.
A state machine that shapes a texture
A texture moves through three nodes, Calm, Build and Burst. Each node is a UDO that writes a shared struct and asks for the next node with stmnext when its time is up. The runner is created inside the instrument, so it lasts as long as the note. It uses the file stm.csd.
| A state machine that shapes a texture. |
|---|
| <CsoundSynthesizer>
<CsOptions>
; Select audio/midi flags here according to platform
-odac ;;;realtime audio out
;-iadc ;;;uncomment -iadc if realtime audio input is needed too
; For Non-realtime ouput leave only the line below:
; -o stm.wav -W ;;; for file output any platform
</CsOptions>
<CsInstruments>
sr = 48000
ksmps = 32
nchnls = 2
0dbfs = 1
; A state machine that shapes a texture. The graph only says which node is
; current and which moves are allowed:
;
; Calm -> Build -> Burst -> Calm or Build
;
; What each node does is an ordinary UDO. It writes the shared state and,
; when its time is up, asks for the next node with stmnext.
struct Texture cutoff:k, rate:k
opcode CalmNode(r:i, tx:Texture, t:k):void
tx.cutoff = 500
tx.rate = 1
if t >= 3 then
stmnext(r, "Build")
endif
endop
opcode BuildNode(r:i, tx:Texture, t:k):void
tx.cutoff = 500 + t * 1500 ; opens up during the node
tx.rate = 1 + t * 3
if t >= 2 then
stmnext(r, "Burst")
endif
endop
opcode BurstNode(r:i, tx:Texture, t:k):void
tx.cutoff = 6000
tx.rate = 12
if t >= 1.5 then
; a conditional edge: Burst may go to either node
if random:k(0, 1) < 0.5 then
stmnext(r, "Calm")
else
stmnext(r, "Build")
endif
endif
endop
; build the graph once, in the global space
builder:i = stmcreate()
stmaddnode(builder, "Calm")
stmaddnode(builder, "Build")
stmaddnode(builder, "Burst")
stmaddedge(builder, "Calm", "Build")
stmaddedge(builder, "Build", "Burst")
stmaddcondedge(builder, "Burst", ["Calm", "Build"])
gi_definition@global:i = stmcompile(builder)
instr 1
r:i = stminstance(gi_definition)
tx:Texture = init(500, 1)
; the runtime loop: current node -> its UDO -> stmadvance
node:S = stmcurrent(r)
t:k = stmnodetime(r)
if strcmpk(node, "Calm") == 0 then
CalmNode(r, tx, t)
elseif strcmpk(node, "Build") == 0 then
BuildNode(r, tx, t)
else
BurstNode(r, tx, t)
endif
status:k, from:k, to:k = stmadvance(r)
if status == 1 then
printks("%5.2f s: %s -> %s\n", 0, timeinsts(), stmnodename(r, from), stmnodename(r, to))
endif
; the sound only reads the shared state
pulse:k = 0.5 + 0.5 * lfo(1, tx.rate, 0)
cutoff:k = portk(tx.cutoff, 0.1)
env:k = linseg(0, 0.5, 1, p3 - 1, 1, 0.5, 0)
sig:a = vco2(0.1, 110) + vco2(0.1, 165) + vco2(0.1, 220.5)
sig = moogladder(sig, cutoff, 0.4) * pulse * env
out(sig, sig)
endin
</CsInstruments>
<CsScore>
i 1 0 20
e
</CsScore>
</CsoundSynthesizer>
|
See also
State Machine Graphs
stmnext, stmcurrent, stmtick
Credits
Author: Pasquale Mainolfi
Italy
New in Csound version 7.0
October 2026