139 lines
11 KiB
Markdown
139 lines
11 KiB
Markdown
# ZHILA
|
||
|
||
Sound is terrain. A sampling instrument where material is cut in a quarry, laid as stones in a nave, and played by a hand or an agent through the same verbs.
|
||
|
||
This repository is **P2 in progress**: the engine, the wire, the descent, the quarry, a persistent project, recording, manual faces, undo across restarts, a native host, an **8-bar arrangement of four sections**, and an **offline WAV render**. A found or recorded sound is cut into sixteen faces, laid on a vein, arranged across A–D, heard as an eight-bar loop and rendered to a 16.5-second WAV; the agent is the remaining P2 slice. The visual demo in [`reference/demo`](reference/demo) remains a design reference, not the codebase. The roadmap is in [`docs/PLAN.md`](docs/PLAN.md).
|
||
|
||

|
||
|
||

|
||
|
||

|
||
|
||

|
||
|
||

|
||
|
||

|
||
|
||

|
||
|
||

|
||
|
||
## What is real
|
||
|
||
- **Rust engine.** `cpal` output at 48 kHz, five veins, a sixteen-step phrase, a fixed voice pool, per-vein levels and gains, a bar clock. Procedural voices for empty pattern rows; real sample voices for rows with a face. No Web Audio.
|
||
- **Material.** Import through the local wire; `symphonia` decodes WAV/FLAC/MP3/OGG; onset analysis finds seams and cuts sixteen faces with RMS, rank and waveform peaks; the archive keeps the mono f32 samples. The sample rate is preserved and the engine handles the playback ratio.
|
||
- **Project.** A portable folder: `project.db` (SQLite) plus `assets/`. On daemon start it loads the cores, pattern, tempo, gains and journal; every project edit autosaves. The default is `~/.local/share/zhila/descent-04`; use `--project PATH` for another. The project name is shown in the top rail.
|
||
- **Undo/redo.** Tempo, gains, pattern steps/faces and face cuts push a small history; `Ctrl/⌘+Z` and `Shift+Ctrl/⌘+Z`, or the rail buttons. The bounded history and redo stacks are saved in the project (up to 64 edits, with every core's cuts) and restored on load; the journal still records every edit.
|
||
- **Recording.** `record` in the quarry opens the microphone, captures mono PCM through an AudioWorklet, encodes a 16-bit WAV in the surface and sends it through the same import path. The take becomes a core with sixteen faces, seal `recorded`, and is autosaved in the project. Stops at 30 s; no monitoring.
|
||
- **Quarry editing.** Drag any of the seventeen seams on the waveform: the inner seams recut the sixteen faces, the outer seams trim the playable range. The core recomputes RMS and rank from the samples; the engine hears the new ranges on the next trigger; the project stores the cuts; the edit is undoable.
|
||
- **Quarry.** Import, core strip, zoomed waveform with face boundaries, sixteen-face wall, face audition, seal and stats; assign a face to a pattern row; edit the sixteen steps; strike a face; all against the live session.
|
||
- **Protocol v1.** JSON control frames, a 31-byte binary meter frame, and a binary import path (an import header followed by the audio bytes). Snapshot, results, events, token and origin guard.
|
||
- **Session + journal.** Validation, revisions, a journal for project edits (tempo, gains, pattern faces and steps); play, stop and strikes are performance, not history.
|
||
- **Surface.** A Svelte 5 descent whose terrain, eye, city, road, stones and playhead react to the five levels and the master; a real wire log; engine facts; cilia; a master signal trace; vein gains; transport; keyboard.
|
||
- **Native host.** The Tauri app starts the same host inside its own process: engine, session, SQLite project, assets and loopback wire. The built window is self-contained; the browser path uses the standalone daemon. One process owns `127.0.0.1:4317` at a time.
|
||
- **Arrangement.** Four 2-bar sections (A–D) make an 8-bar loop. The engine plays the sections in order; the descent draws all eight bars with section seams and letters; the quarry edits the active section, selects, copies and clears. The sections, the active section and their edits are persisted and undoable; the pattern rows and faces stay per section.
|
||
- **Render.** The `render` button (or the `render` command) renders the full 8-bar arrangement offline with the same voices, gains, sample faces and master as the live engine, then writes a 32-bit float stereo WAV to `<project>/renders/`. The `rendered` event carries the path and duration; no device is opened for the render.
|
||
- **Verification.** Rust unit tests, a live wire test, a material import test, a browser quarry test and the P0 regression smoke, plus `svelte-check` and a production build.
|
||
|
||
The Tauri 2 app is **built and running with its own engine**: the native window loads the embedded surface and the host starts the engine, session, project and wire in the app process. It is built with Tauri 2.12.5 / CLI 2.12.4 against WebKitGTK 2.54.2, GTK 3.24.52 and dbus 1.16.2. The binary is `app/src-tauri/target/release/zhila-app` (14 MB); installers are a later step.
|
||
|
||
## Quick start
|
||
|
||
Requirements: Rust (tested on 1.99), Node 22, ALSA. The daemon uses the default output device; without one it reports offline and the surface still connects.
|
||
|
||
```bash
|
||
./scripts/dev.sh
|
||
```
|
||
|
||
Open **http://127.0.0.1:5173**. The daemon listens on `ws://127.0.0.1:4317` with the development token `dev`.
|
||
|
||
Or the two halves separately:
|
||
|
||
```bash
|
||
cargo run -p zhila-daemon -- --port 4317
|
||
npm --prefix ui run dev
|
||
```
|
||
|
||
### Try the material loop
|
||
|
||
1. Open **quarry** in the top rail.
|
||
2. `take a core` and choose a WAV (a short field recording works; the fixture `fixtures/clicks-01.wav` is one).
|
||
3. The file is decoded, the seams are found and sixteen faces are cut. Or press `record`, speak/play into the microphone, press `stop`; the take arrives the same way. Click a face, then `lay on bass`.
|
||
4. `space` plays the loop; the bass row now triggers the sample while the other veins keep their procedural voices. Edit steps in the pattern; each edit goes to the engine and the journal.
|
||
5. Go back to **nave** to see the pattern on the road and the levels in the room.
|
||
|
||
### Tauri
|
||
|
||
```bash
|
||
npm --prefix ui run build
|
||
cd app
|
||
./node_modules/.bin/tauri build --no-bundle
|
||
./src-tauri/target/release/zhila-app
|
||
```
|
||
|
||
The built window embeds the surface and starts the host itself; it does not need the daemon. The host owns `127.0.0.1:4317`, loads the default project, and stops when the app exits. For browser development use the standalone daemon (`cargo run -p zhila-daemon -- --port 4317`) and stop the native app, because both bind the same port. `tauri dev` uses the Vite server at `127.0.0.1:5173`. Rebuild the native app after any UI change; the binary embeds `ui/dist` at compile time. The recording and cutting UI are in the shared surface; WebKitGTK microphone permission still needs a manual check on this machine.
|
||
|
||
Controls: `space` play/stop · `s` strike the selected face in the quarry or the selected vein in the nave · `1`–`5` select a vein (nave) or a pattern row (quarry) · click a face to select, double-click to strike · tempo `−` / `+`.
|
||
|
||
The daemon prints the audio device, rate, buffer and session id. Use `--no-audio` for UI/protocol tests, `--port` to change the port, and `ZHILA_TOKEN` or `--token` to change the dev token. The surface reads `VITE_ZHILA_WS` and `VITE_ZHILA_TOKEN` at build time.
|
||
|
||
## Layout
|
||
|
||
```
|
||
crates/zhila-protocol wire types, binary meter frame, cores/faces, tests
|
||
crates/zhila-core session, journal, material snapshot, engine trait
|
||
crates/zhila-engine cpal stream, procedural + sample voices, levels
|
||
crates/zhila-material decode, onset analysis, sixteen faces, peaks
|
||
crates/zhila-project SQLite project, assets, load/save, undo persistence
|
||
crates/zhila-daemon WebSocket daemon, auth, binary import, autosave
|
||
ui Svelte 5 surface: descent + quarry
|
||
fixtures a small click track for the smoke tests
|
||
app Tauri 2 app with the embedded host
|
||
docs plan, protocol, P0/P1 notes
|
||
reference/demo the earlier design demo
|
||
```
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
./scripts/check.sh # fmt, clippy, cargo tests, svelte-check, build
|
||
node scripts/wire-smoke.mjs # daemon running
|
||
node scripts/material-smoke.mjs # daemon running: import, face, play
|
||
node scripts/persistence-smoke.mjs # spawned daemon: import/edit/cut/undo/restart/history
|
||
npm --prefix ui run smoke # daemon + Vite: P0 regression, screenshots
|
||
npm --prefix ui run quarry-smoke # daemon + Vite: import, assign, play, screenshots
|
||
npm --prefix ui run project-smoke # daemon + Vite: project, undo, import, screenshots
|
||
npm --prefix ui run record-smoke # daemon + Vite: fake microphone take, faces, seal
|
||
npm --prefix ui run faces-smoke # daemon + Vite: drag a seam, trim, save, screenshot
|
||
npm --prefix ui run arrangement-smoke # daemon + Vite: sections, edit B, 8-bar descent
|
||
node scripts/render-smoke.mjs # daemon running: render, parse the WAV
|
||
npm --prefix ui run render-ui-smoke # daemon + Vite: render button, wire, screenshot
|
||
cd app && ./node_modules/.bin/tauri build --no-bundle # native app with the embedded host
|
||
npm --prefix ui run restored-smoke # after restarting the daemon on the saved project
|
||
```
|
||
|
||
`npm --prefix ui run smoke` writes `preview-p0*.png`; `quarry-smoke` writes `preview-p1-*.png`.
|
||
|
||
## P2 limits (in progress, not hidden)
|
||
|
||
- The arrangement is exactly four 2-bar sections; no section length, reorder, insert or per-section mute yet.
|
||
- Render is 48 kHz stereo 32-bit float with a 1.5 s tail; no stems, normalisation or per-section export.
|
||
- The agent is the remaining P2 slice.
|
||
|
||
## P1 limits (kept honest)
|
||
|
||
- Recording is mono, 16-bit, requested 48 kHz, up to 30 s, no monitoring and no input device choice. WebKitGTK/Tauri microphone permission is untested; the browser path is verified with Chromium's fake capture.
|
||
- Face editing is seventeen contiguous cuts: no zoom, fades or per-face loop points. The trim is the outer pair of cuts; the source bytes are untouched.
|
||
- Undo history is a bounded snapshot stack stored as JSON; imports are still not undoable, and the journal is an audit trail rather than the undo source.
|
||
- The native app uses the development token and a fixed port; the browser and the app cannot share 4317 at the same time.
|
||
- The arrangement is still one two-bar loop on five veins; no render, no 8-bar timeline, no agent yet.
|
||
- One fixed two-bar phrase and five veins; pattern rows are veins, one face per row, no per-step face or pitch.
|
||
- The Tauri binary currently embeds an older surface build; rebuild it after UI changes (`npm --prefix ui run build`, then `tauri build`). The engine remains the separate daemon.
|
||
- Formats depend on symphonia; WAV is the tested path. AIFF, multi-channel files and very long files are not specifically handled.
|
||
- No agent/MCP in this phase.
|
||
- The Tauri shell now builds and runs; the engine is still the separate daemon (embedding is next).
|
||
|
||
P2 is the arrangement, the offline WAV render and the in-app agent. P2a (the arrangement) and P2b (the render) are in this tree; P2c follows. The plan is in [`docs/PLAN.md`](docs/PLAN.md); the phase notes are in [`docs/P2.md`](docs/P2.md). The plan is in [`docs/PLAN.md`](docs/PLAN.md); the phase notes are in [`docs/P1.md`](docs/P1.md).
|