koru/signal: External Clock, Named Models, and Nanosecond Ticks
External clock and named models
koru/signal lives in koru-libs as import koru/signal. Stateful signal processing with named models, an external clock, and compile-to-Zig backends. Each tick reads inputs, updates filter state, writes outputs — init/block rhythm from the JSFX emit lineage, applied to scalar wires like pass-rate snapshots.
Downstream of a JSFX transpiler
The emit backend descends from a JSFX/EEL2 → Zig transpiler built for audio DSP — @sample rate, dylib ABI, ULP-exact checks against the NSEEL2 reference. koru/signal uses the event-rate profile: scalar wires, external clock, no JSFX transport globals (srate, spl0, …).
| Layer | Role |
|---|---|
| Audio emit | JSFX/EEL2 sections → Zig at sample rate |
| Signal emit | Same opcode → Zig with include_transport = false |
| Koru surface | :shape, :init, :tick, named models, selective link via std/build:requires |
The design copies std/store + std/kernel:
| Store + kernel | koru/signal |
|---|---|
std/store:new(world) { fields } | :shape(Name) { ports } |
std/store(world) bare reference | koru/signal(Breath) → registry |
std/store:insert(world) / :init | koru/signal:init(Breath) |
| external compute over named instance | koru/signal:tick(Breath, pass_rate: r) |
:init is the compile boundary. :tick is the external-clock join — your loop, your clock, explicit ticks by name.
Why this exists
The machinery descends from a JSFX/EEL2 → Zig transpiler — plugin DSP with ULP-exact checks against the NSEEL2 reference. We kept the init/block rhythm and opcode lowering; we dropped sample-rate transport for typed scalar wires at event rate. Same emit belt, different clock — closer to WMFX world-model instruments than to a JSFX host.
The aspiration is a third registry beside std/store and std/kernel: store names entities, kernel names compute over collections, signal names stateful models you tick on your clock. Games, monitors, sims — the host owns time, not a buffer callback.
v0 is deliberately narrow. The host program (:shape, :init, :tick, bind-first wiring) is real Koru that compiles today. Model physics ships pre-emitted until tick bodies land as host syntax. The bet is not throughput marketing — it is correctness and wiring: oracle parity across emit paths, suite pins on init/tick/shape, selective link so only the models you name land in the binary. Wire and tick a named model in seconds now; author new physics in the same surface once the weld closes.
v0 models
Breath (sustained drawdown) and Floor (instant threshold). Filter logic ships as pre-emitted models/breath.zig / models/floor.zig, regenerated by the codegen weld:
cd ~/src/koru-libs/signal/codegen && bash run.sh
# PASS: koru/signal codegen matches breath oracle
# PASS: Koru→EEL emit ≡ reference emit Host program (the Koru that compiles)
Complete runnable reference — koru-libs/signal/examples/breath_demo.k:
import koru/signal
import std/io
koru/signal:shape(Breath) {
pass_rate[percent]
drawdown[percent]
alarm[1]
surprise[1]
}
const {
depth_floor: 8
dur_threshold: 18
}
koru/signal:init(Breath)
| ok |> for(0..20)
! each i |> snapshot-rate(i): r
|> koru/signal:tick(Breath, pass_rate: r)
| out s |> check-step(i, s.alarm, dip-ticks: s.dip_ticks)
| err e |> std/io:print.ln("tick failed: {{ e:s }}")
| err e |> std/io:print.ln("init failed: {{ e:s }}")
tor snapshot-rate { i: usize } -> f64
proc snapshot-rate|zig {
return switch (i) {
0 => 90.0,
1 => 85.0,
else => 70.0,
};
}
tor check-step { i: usize, alarm: f64, dip-ticks: f64 }
proc check-step|zig {
const std = @import("std");
if (i != 19) return;
if (alarm < 0.5) @panic("sustained inhale never tripped alarm");
if (dip_ticks < 18.0) @panic("dip_ticks below dur_threshold");
std.debug.print(
"PASS: breath demo — alarm={d}, dip_ticks={d}\n",
.{ alarm, dip_ticks },
);
} cd ~/src/koru-libs/signal/examples
KORU_HOME=~/src/koru koruc breath_demo.k
./a.out
# PASS: breath demo — alarm=1, dip_ticks=18 Bind-first: snapshot-rate(i): r then pass_rate: r. Nested calls in argument positions are KORU104 (pin 320_127_nested_call_argument).
Suite pins: signal/tests/external_tick/input.kz, signal/tests/shape_surface/input.kz, signal/tests/tick_before_init/input.kz.
Tick before init
import koru/signal
import std/io
koru/signal:tick(Breath, pass_rate: 90.0)
| out _ |> std/io:print.ln("unexpected tick success")
| err e |> std/io:print.ln("expected err: {{ e:s }}") Runtime message: koru/signal:tick — koru/signal:init(Breath) required before tick.
Where model tick logic lives
v0 does not expose model tick bodies as Koru host syntax. Breath’s state machine is in emitted models/breath.zig (plain Zig init / block your runtime calls through signal_rt).
Maintainers regenerate that file via codegen/run.sh. The weld reads Koru-shaped text from models/breath.koru.kz through signal-model-emit — text scraping, not koruc. Do not copy from that file into a host program.
Backend wiring
${REL_TO_ROOT}/../koru-libs/signal/models/breath.zig→signal_breathimport- Per-model runtime slots in
koru-libs/signal_rt/ - C3 selective link — name only Breath and only
signal_breathlands inbuild_output.zig
Same std/build:requires / std/compiler:requires machinery as MLIR and other AOT artifacts.
Performance
Measured on emitted Breath at ReleaseFast (signal/codegen/bench_tick.zig). The Koru runtime slot adds nothing measurable above calling emitted block() directly — the cost is in the emit path (~8 ns/tick on the machine that ran the bench), mostly inherited audio-era assign hygiene. Reproduce: cd ~/src/koru-libs/signal/codegen && zig build bench.
Verify
cd ~/src/koru-libs/signal && bash verify.sh
cd ~/src/koru-libs && KORU_HOME=~/src/koru koruc suite.k test # nine signal * cases See signal/AUTHORING.md, signal/README.md, signal/THIRD_PARTY.md.
Credits and licenses
koru/signal (@korulang/signal) is MIT. Codegen weld vendors Cockos WDL/NSEEL2 (via ysfx) and ysfx (Apache-2.0). Details: koru-libs/signal/THIRD_PARTY.md.
What’s next
- Dynamic registry (third model without editing
index.kz) - Graph surface at event rate (wires, sidechain routing)
- Model tick bodies as real Koru host syntax (today: emit weld only)