← Back to index · Previous: Emit & catch
Idƴl has a built-in clock hierarchy. A main clock is created at startup, child clocks can be created and bound to parents, and tempo changes propagate through the tree.
When a program starts, a main clock is created at
120 BPM. All beat-relative time literals (4b,
2b) resolve against this clock.
Query the main clock’s tempo:
process: {
print("tempo:", tempo()) // 120
}Set it:
process: {
tempo(140bpm)
print("tempo:", tempo()) // 140
}bpm literalThe bpm suffix converts beats-per-minute to a period in
milliseconds:
period = 120bpm // 500ms (60000 / 120)
period = 60bpm // 1000ms
period = 240bpm // 250msThis is useful as a dt parameter:
beat_counter(dt=120bpm) = n |> {
init: { n = 0 }
n = n + 1
}Use clock(bpm) to create a child clock bound to the main
clock:
process: {
c1 = clock(60bpm)
c2 = clock(90bpm)
print("c1:", tempo(c1), "c2:", tempo(c2))
// c1: 60 c2: 90
}c1 = clock(60bpm)
c3 = clock(30bpm, parent=c1) // c3 is child of c1, not mainA clock with parent=0 is free-running — unaffected by
any tempo changes:
c_free = clock(100bpm, parent=0)When a parent’s BPM changes, children scale proportionally. The ratio is captured at creation time.
process: {
c1 = clock(60bpm) // ratio to main = 60/120 = 0.5
c2 = clock(90bpm) // ratio to main = 90/120 = 0.75
tempo(240bpm) // main → 240
print("c1:", tempo(c1)) // 240 * 0.5 = 120
print("c2:", tempo(c2)) // 240 * 0.75 = 180
}process: {
c1 = clock(60bpm) // child of main, ratio 0.5
c3 = clock(30bpm, parent=c1) // child of c1, ratio 30/60 = 0.5
tempo(240bpm) // main → 240
// c1 → 120 (0.5 × 240)
// c3 → 60 (0.5 × 120)
print("c3:", tempo(c3)) // 60
}tempo(c1, 80bpm) // set c1 to 80, propagate to c1's childrenA clock handle is also callable. Calling it with a beat count returns the duration of that many beats at the clock’s current BPM:
process: {
c1 = clock(60bpm)
// c1(2b) → duration of 2 beats at 60 BPM = 2000ms
m = metro(c1(2b)) // fires every 2 beats of c1
m2 = metro(c1(0.5b)) // fires every half-beat of c1
m3 = metro(c1(500ms)) // plain ms pass-through
m4 = metro(c1()) // 1 beat (default)
print(m, m2)
}This is the primary way to drive temporal functions from a specific
clock: the beat literal 2b is resolved against that
clock’s BPM, not the main clock.
| Function | Description |
|---|---|
clock(bpm) |
Create clock bound to main |
clock(bpm, parent=h) |
Create clock bound to parent h |
clock(bpm, parent=0) |
Create free-running clock |
tempo() |
Query main clock BPM |
tempo(handle) |
Query a specific clock’s BPM |
tempo(bpm) |
Set main clock BPM (propagates) |
tempo(handle, bpm) |
Set specific clock BPM (propagates) |
handle(Nb) |
Duration of N beats at that clock’s BPM |
handle(ms) |
Pass-through: returns the ms value unchanged |
handle() |
Duration of 1 beat at that clock’s BPM |
All clocks return handles — opaque values that identify the clock in the registry.
The clock system is designed for musical contexts where tempo is hierarchical. A conductor (main clock) sets the global pulse, and individual instruments (child clocks) may run at different subdivisions. When the conductor changes tempo, everyone follows proportionally.
Free-running clocks (parent=0) are for things that
should never change speed — metronomes locked to an external source, or
clocks driven by hardware.