P0: Rust engine, wire daemon and audio-reactive descent surface
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
# P0 · hello engine
|
||||
|
||||
The first phase proves the spine: Rust audio, a session with a journal, a protocol with two transports, and a surface that reacts to the sound. The demo in `reference/demo` is the visual reference; none of its code is used here.
|
||||
|
||||
## What was built
|
||||
|
||||
| Area | Deliverable |
|
||||
|---|---|
|
||||
| Protocol | `zhila-protocol`: JSON messages, five veins, pattern, state, binary meter frame; unit tests |
|
||||
| Session | `zhila-core`: engine trait, validation, revisions, journal for tempo/gain, default sixteen-step phrase; tests |
|
||||
| Engine | `zhila-engine`: cpal 0.18, 48 kHz stereo, fixed 256-frame buffer where available, 32-voice pool, five vein timbres, phrase clock, per-vein levels, master level; no allocation on the audio path |
|
||||
| Daemon | `zhila-daemon`: WebSocket, hello/token/protocol/origin checks, snapshot, results, broadcast events, 30 Hz binary meters, current-thread Tokio, graceful Ctrl-C |
|
||||
| Surface | Svelte 5 + Vite: audio-reactive descent, wire log, engine facts, cilia, signal trace, per-vein gain sliders, transport, keyboard |
|
||||
| Verification | `cargo test`, `cargo clippy`, `svelte-check`, production build, live wire test, browser smoke + screenshots |
|
||||
| Shell | Tauri 2 scaffold in `app`; build blocked by missing system development packages (below) |
|
||||
|
||||
## Decisions taken in P0
|
||||
|
||||
- **cpal, not Web Audio.** The browser is a surface; sound is native Rust. The cpal 0.18 API uses a unified `cpal::Error`, `SampleRate = u32`, `Display` for the device name and a by-value `StreamConfig`.
|
||||
- **JSON control + binary meters.** Control metadata is small and debuggable; meters are 31-byte frames at 30 Hz.
|
||||
- **One daemon, one session.** Loopback + token + origin guard are the P0 security boundary. No production auth yet.
|
||||
- **The journal is for project edits.** Play/stop/strike are performance and do not pollute undo.
|
||||
- **The surface is a projection.** The server owns state; the UI applies snapshots, results and events. The canvas owns only its visual phase.
|
||||
- **Every layer reacts to sound.** The terrain alpha/offset, eye size/glow/rings, city lights, dust, road bands, stone glow, playhead, lane fills, chrome hairlines, cilia, signal trace and master line all read the five vein levels and the master. No decorative animation is independent of the audio.
|
||||
|
||||
## Tauri blocker
|
||||
|
||||
`cargo check` in `app/src-tauri` failed on this machine because the GTK/WebKit/dbus development packages are absent and `sudo` needs a password:
|
||||
|
||||
```
|
||||
Package 'dbus-1' was not found
|
||||
The system library `dbus-1` required by crate `libdbus-sys` was not found.
|
||||
```
|
||||
|
||||
`pkg-config` also reports missing `webkit2gtk-4.1` and `gtk+-3.0`. The shell is scaffolded, not verified.
|
||||
|
||||
Install the Fedora development packages (one command, needs your password):
|
||||
|
||||
```bash
|
||||
sudo dnf install dbus-devel webkit2gtk4.1-devel gtk3-devel libappindicator-gtk3-devel \
|
||||
librsvg2-devel openssl-devel libsoup3-devel javascriptcoregtk4.1-devel pkgconf-pkg-config
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
cargo check --manifest-path app/src-tauri/Cargo.toml
|
||||
npm --prefix ui run build
|
||||
npm --prefix app run tauri dev # expects the daemon and Vite to be running
|
||||
```
|
||||
|
||||
The Tauri window loads `http://127.0.0.1:5173`; the daemon remains a separate process in P0. Embedding the engine in the Tauri backend is P1 work, after the engine's lifecycle is settled.
|
||||
|
||||
## P0 exit criteria
|
||||
|
||||
- A browser surface talks to a Rust daemon over a local protocol: **passed**.
|
||||
- Real cpal output with five veins and non-zero meters: **passed** (Default Audio Device, 48 kHz, 256-frame buffer, master peak ~0.45 in the wire test).
|
||||
- A session that validates and journals project edits: **passed** (Rust tests).
|
||||
- A surface that visibly reacts to the five levels: **passed** (canvas, cilia, trace, gains; screenshots).
|
||||
- A Tauri window on Linux: **blocked** by system development packages; the exact command is above.
|
||||
|
||||
## Next
|
||||
|
||||
P1: core session and material. Import/record, analysis, cores and faces, a zoomable waveform, a real pattern model, a flat precision view while the descent remains the primary arrangement, and the first journal-backed undo. The plan and cut lines are in `PLAN.md`.
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# ZHILA — план проекта
|
||||
|
||||
Черновик v1.1 · 1 ноября 2026
|
||||
Статус: **план**. Код проекта не начат. Демо `zhila/` — референс, не кодовая база.
|
||||
Статус: **P0 выполнен** (см. `P0.md`). Демо `reference/demo/` — референс, не кодовая база.
|
||||
|
||||
**Зафиксировано 1 ноября:** descent — основной вид; камень = паттерн; климат — на жилу (концепт погоды остаётся); платформа v1 — Linux; UI — Svelte 5. Журнал — §9.
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# ZHILA wire protocol · v1
|
||||
|
||||
Loopback only, one daemon per session. Text frames are JSON; one binary frame type carries meters. The protocol is deliberately small in P0; the revision model and the command bus are the parts meant to survive into P1.
|
||||
|
||||
## Transport
|
||||
|
||||
- WebSocket at `ws://127.0.0.1:4317` by default.
|
||||
- The daemon binds `127.0.0.1` only.
|
||||
- The first client frame must be `hello` within 5 seconds.
|
||||
- If the client sends an `Origin`, it must be `http://localhost[:port]`, `http://127.0.0.1[:port]`, or `https://tauri.localhost`. Native clients may omit it.
|
||||
- The dev token is `dev`. Set `ZHILA_TOKEN` or `--token` to change it. Tokens are compared in the hello, never put in the URL.
|
||||
|
||||
## Client → daemon
|
||||
|
||||
```json
|
||||
{"type":"hello","protocol":1,"token":"dev","client":"zhila-ui"}
|
||||
{"type":"command","id":7,"command":{"type":"play"}}
|
||||
```
|
||||
|
||||
Commands:
|
||||
|
||||
| command | args | notes |
|
||||
|---|---|---|
|
||||
| `get_snapshot` | — | returns a snapshot, no revision change |
|
||||
| `play` | — | transport, idempotent |
|
||||
| `stop` | — | transport, idempotent |
|
||||
| `strike` | `vein` (bass/crack/voice/noise/silt), `velocity` 0..1 | performance; not journaled |
|
||||
| `set_tempo` | `bpm` 40..220 | journaled |
|
||||
| `set_vein_gain` | `vein`, `gain` 0..1.2 | journaled |
|
||||
|
||||
## Daemon → client
|
||||
|
||||
```json
|
||||
{"type":"hello","protocol":1,"session":"zhila-1a193dfd50c","engine":{"running":true,"device":"Default Audio Device","sample_rate":48000,"buffer_frames":256,"channels":2}}
|
||||
{"type":"snapshot","revision":0,"state":{...}}
|
||||
{"type":"result","id":7,"result":{"kind":"state","state":{...}}}
|
||||
{"type":"event","revision":1,"event":{"kind":"transport","playing":true}}
|
||||
{"type":"error","id":7,"code":"bad_request","message":"tempo must be between 40 and 220 bpm"}
|
||||
```
|
||||
|
||||
`state`:
|
||||
|
||||
```json
|
||||
{
|
||||
"revision": 1,
|
||||
"transport": {"playing": true, "bpm": 128.0, "bar": 1.0, "loop_bars": 2.0},
|
||||
"veins": [{"id":"bass","name":"bass","gain":0.82}, "..."],
|
||||
"pattern": {"steps":16,"steps_per_bar":8,"rows":[{"vein":"bass","velocities":[112,0,0,86,...]}]},
|
||||
"audio": {"running":true,"device":"Default Audio Device","sample_rate":48000,"buffer_frames":256,"channels":2},
|
||||
"journal_len": 0
|
||||
}
|
||||
```
|
||||
|
||||
`result.kind` is `snapshot` or `state`; both carry the full state.
|
||||
|
||||
Events:
|
||||
|
||||
- `{"kind":"transport","playing":bool}`
|
||||
- `{"kind":"tempo","bpm":number}`
|
||||
- `{"kind":"vein_gain","vein":VeinId,"gain":number}`
|
||||
- `{"kind":"struck","vein":VeinId,"velocity":number}`
|
||||
|
||||
Errors: `bad_request` (validation), `unauthorized`, `protocol` (version/hello), `internal`.
|
||||
|
||||
## Binary meter frame
|
||||
|
||||
Sent at ~30 Hz, 31 bytes, little-endian:
|
||||
|
||||
```
|
||||
offset size field
|
||||
0 1 kind = 1
|
||||
1 2 count = 5
|
||||
3 4 bar (f32)
|
||||
7 4 master level (f32)
|
||||
11 20 five vein levels (f32 each)
|
||||
```
|
||||
|
||||
The browser sets `binaryType = "arraybuffer"` and reads a `DataView`. The frame is deliberately separate from JSON; no meter ever goes through text frames.
|
||||
|
||||
## Semantics
|
||||
|
||||
- `revision` increments on every state-changing command: transport, tempo, gains. It is a state version, not the journal length.
|
||||
- `journal_len` counts project edits only: tempo and gains in P0. Play, stop and strike are performance.
|
||||
- The daemon broadcasts events to every client. A command's `result` goes only to the requester.
|
||||
- The server is authoritative. The surface holds a projection and updates it from snapshots, results and events.
|
||||
- P0 has no persistence; a daemon restart starts a fresh session at 128 bpm, not playing, journal empty.
|
||||
|
||||
## Not in v1 yet
|
||||
|
||||
- Binary snapshots / CRDTs.
|
||||
- Project/asset commands.
|
||||
- Agent tools and MCP.
|
||||
- Per-client cursors or collaboration.
|
||||
Reference in New Issue
Block a user