Daemon protocol
The daemon (niminal daemon) exposes a single session to multiple clients over WebSocket. Messages are JSON-RPC 2.0 text frames. This is separate from niminal lsp, which speaks the Language Server Protocol on stdio for editing only.
Default URL: ws://127.0.0.1:7400/ (see --port and --listen on the daemon).
Connection
Section titled “Connection”- Open a WebSocket to the daemon.
- Send
helloas the first request (within about three seconds, or the connection may time out). - If the daemon was started with
--token, the first message must behelloandparams.tokenmust match. Until then, other requests get a protocol error. - Subscribe to topics if you want push updates (optional).
- Send requests with an
id; the daemon replies with the sameid. Requests without anidare notifications and get no reply (used forpanic).
All connected clients share one session. Disconnecting a client does not stop playback.
Limits: up to 16 clients; maximum message size 4 MiB (large eval sources).
Handshake: hello
Section titled “Handshake: hello”Request
{ "jsonrpc": "2.0", "id": 1, "method": "hello", "params": { "protocol": 1, "client": "my-app", "token": "optional-if-daemon-requires-it" }}Result
{ "protocol": 1, "sample_rate": 48000, "channels": 2, "topics": ["landed", "pending", "transport", "meters", "notices"]}protocol must be 1 (PROTOCOL_VERSION in the daemon). A mismatch returns error code -32000.
Subscriptions
Section titled “Subscriptions”subscribe / unsubscribe — params: { "topics": ["landed", "pending", ...] }.
Valid topics (same as in hello):
| Topic | Notification method |
When |
|---|---|---|
landed |
landed |
A queued change reached its quantize boundary |
pending |
pending |
The set of waiting changes changed |
transport |
transport |
About ten times per second while the clock runs |
meters |
meters |
With transport, if anyone subscribed (peak readings) |
notices |
notice |
Warnings (playback issues, cancelled dependents, NaN voices, …) |
Notifications have no id:
{ "jsonrpc": "2.0", "method": "landed", "params": { ... } }Requests
Section titled “Requests”Compile and queue niminal source. Failed compile returns -32001 with error.data.problems (same shape as editor diagnostics).
Params
| Field | Type | Description |
|---|---|---|
source |
string | Required. One evaluation unit (block, file, or line). |
quantize |
string | When it lands: now, beat, bar, next bar, next 4 bars, cycle, etc. Omitted uses session defaults. |
dir |
string | Optional sample search path for this eval (project-relative). |
Result (accepted)
{ "id": 2, "lands_at": 96000, "position": { "bar": 2, "beat": 1.0 }, "in_seconds": 1.79, "changes": ["play lead = [c4]"]}id— use withcancelto drop this change before it lands.lands_at— sample time on the session clock.changes— human-readable summary of what will apply.
Compilation runs off the audio thread; the server may compile in a worker thread and retry if the pending queue changes mid-compile.
cancel
Section titled “cancel”Drop pending changes before they land.
Params: { "id": 2 } — cancel that change (and dependents). Omit id to cancel one pending change (implementation-defined which when several are queued).
Result: { "cancelled": 1 } — number removed.
Graceful stop: clips stop, notes can ring out. Queued like eval (returns the same Accepted shape as a successful eval).
Immediate silence: stops voices, clears delay lines within one engine block. Result: {}.
Can be sent as a notification (no id) for an instant stop with no JSON reply.
status
Section titled “status”Snapshot of the session (no subscription needed).
Result includes:
transport—sample,seconds,bar,beat,bpm,voices,pending(count)pending— array of pending changes (same fields aspendingnotifications)sample_rate,channelsrealtime— optional counters (late_events,starved_frames,capacity_drops,control_drops,deadline_misses,max_render_micros, …)controls— eachctl:name,value,target,unit(db shown in dB)scenes,clips— defined namestracks—name,clip,muted,soloed
control.set
Section titled “control.set”Update a ctl without recompiling the graph.
Params
| Field | Type | Description |
|---|---|---|
name |
string | Required. Control name. |
value |
number | Required. |
unit |
string | Optional; defaults to the declaration’s unit (hz, db, …). Compatible units (khz, ms, beats, …) are converted. |
smooth_ms |
number | Optional override of glide (0–60000). |
at |
integer | Optional sample timestamp; default is current render clock. |
Result: { "at": 37 } — sample time the update was scheduled for.
Invalid name, unit, value, or full queue → -32001 or -32602.
event.stream
Section titled “event.stream”Inject score-model events from an external client (Python, tracker, etc.) without niminal source.
Params
{ "events": [ { "target": "lead", "at": "0.1sec", "dur": "0.2sec", "args": { "freq": "a4", "gain": "-6db" } } ]}target— track or instrument name in the running session.at,dur— times with units (beat,sec,bar, …) as in Score model.args— named parameters; values are unit strings or numbers as in.nmsJSON.
Validation matches orchestra typing. Unknown target or bad units → -32001 with problems.
There is no separate launch RPC yet — use eval with source like launch chorus or play bass = riff.
Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
| -32700 | Parse error (invalid JSON) |
| -32600 | Invalid request (missing jsonrpc, method, …) |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error (session continues) |
| -32000 | Protocol error (hello, version, token) |
| -32001 | Rejected — compile/validation failed; see data.problems |
Problem object: message, help, line, column, span (start, end), optional in_definition.
Evaluation log and replay
Section titled “Evaluation log and replay”With default logging, the daemon writes session input under ~/.niminal/sessions/. Replay offline:
niminal render --replay ~/.niminal/sessions/<log> --out set.wav --channels 2Evaluations, control.set, MIDI/OSC (when enabled), and related control input are recorded so replay matches live audio.
Planned (spec, not in the daemon yet)
Section titled “Planned (spec, not in the daemon yet)”The language spec also describes score.load, event.update, file.put, remote TLS, and richer meter streaming. Those are not implemented on the wire today; use eval, event.stream, and project-local paths instead.
Minimal client flow
Section titled “Minimal client flow”→ {"jsonrpc":"2.0","id":1,"method":"hello","params":{"protocol":1,"client":"example"}}← {"jsonrpc":"2.0","id":1,"result":{"protocol":1,"sample_rate":48000,"channels":2,"topics":[...]}}
→ {"jsonrpc":"2.0","id":2,"method":"subscribe","params":{"topics":["landed","pending","transport"]}}← {"jsonrpc":"2.0","id":2,"result":{"topics":["landed","pending","transport"]}}
→ {"jsonrpc":"2.0","id":3,"method":"eval","params":{"source":"tempo 120bpm\ninstr x(freq: hz) { osc(sine, freq) }\ntrack t { instrument = x }","quantize":"now"}}← {"jsonrpc":"2.0","id":3,"result":{"id":1,"lands_at":0,...}}
→ {"jsonrpc":"2.0","id":4,"method":"eval","params":{"source":"play t = [c4 e4 g4]","quantize":"next bar"}}← {"jsonrpc":"2.0","id":4,"result":{"id":2,"lands_at":96000,"position":{"bar":2,"beat":1.0},...}}
← {"jsonrpc":"2.0","method":"landed","params":{"id":2,"at":96000,"changes":["play t = [c4 e4 g4]"],...}}See also Command line (send, repl), Editor and LSP, and Live coding.