Files

52 lines
5.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P2 · arrangement, render, agent
P2 turns the loop into a thing that can be played, rendered and performed with an agent. It is delivered in three slices.
## P2a · the arrangement
Four 2-bar sections (A–D) form an 8-bar loop. Each section is a full `Pattern` (5 rows × 16 steps, with per-row faces). The engine walks 64 steps, picks the section and the pattern step, and triggers the section's rows; the bar clock runs 1–8. The state carries `arrangement: [Pattern; 4]`, `active_section`, and `pattern` = the active section for the editors.
Commands: `select_section { index }`, `clear_section { index }`, `copy_section { from, to }`, event `arrangement { sections, active }`. Pattern edits apply to the active section. Select is persisted but not undoable; clear/copy push history. Sections, the active index and their history are stored in SQLite (`arrangement`, `active_section` columns, migrated with `ensure_column`) and restored on load.
Surface: the quarry gets a section strip (A–D with note counts, copy →, clear); the pattern grid edits the active section. The descent draws the 64-step road, bar labels 01–08, section seams and letters A–D, with the active section emphasized; the playhead follows the 8-bar clock.
Verification:
- `cargo test -p zhila-core arrangement`: sections independent, select/copy/clear, undo, material carries four sections, loop_bars = 8.
- `node scripts/persistence-smoke.mjs`: imports, edits section B, copies A→C, cuts, restarts, verifies the arrangement, active B, B's step 0 = 0, C = A, and undo/redo.
- `npm --prefix ui run arrangement-smoke`: four sections, select B, edit a step, switch to the nave, play, screenshot `preview-p2-arrangement.png`.
Limits: exactly four sections of two bars; no insert/reorder/length; select is not undoable; no per-section mute.
## P2b · render
`zhila_engine::render_arrangement(RenderRequest)` drives the same `AudioState` offline: no cpal device, the same voices, sample faces, gains, master and the 8-bar sections, plus a 1.5 s tail. `Command::render` is intercepted by the host, rendered in `spawn_blocking`, written with `hound` as 48 kHz stereo 32-bit float to `<project>/renders/<project>-<ms>.wav`, and announced with `Event::rendered { path, duration_sec }`; the command result is the unchanged state. Render is not a project edit and does not push history.
Surface: `render` in the top rail; the wire logs the send and the `rendered` path; the button's title keeps the last path.
Verification: `cargo test -p zhila-engine` (non-silent 16.5 s buffer, correct frame count); `node scripts/render-smoke.mjs` (status 16.5 s, peak 0.442, 6.3 MB, float 48 kHz stereo, event duration matches); `npm --prefix ui run render-ui-smoke` (button → wire path, screenshot `preview-p2-render.png`).
Limits: WAV only, stereo float, fixed 48 kHz and 1.5 s tail; no stems, normalisation or section ranges.
## P2c · the choir
`crates/zhila-daemon/src/agent.rs` is the performer. It receives the project state and live meter as text, a system prompt with the ZHILA rules, and an OpenAI-compatible tool list. Safe verbs (play, stop, strike, strike_face, render) execute immediately; project verbs (tempo, gains, pattern, faces, sections, cuts, undo/redo) are collected into one proposal stored with an id. The hand approves or rejects; approval applies every command with `Actor::Agent`, saves the project, and emits the normal events plus an agent confirmation.
Wire: `agent_ask { text }`, `agent_approve { id }`, `agent_reject { id }`, `agent_reset`; events `agent_message { role, text }`, `agent_proposal { id, description, commands }`, `agent_resolved { id, applied }`; `agent_history { messages }` on hello. The host keeps the conversation and pending proposals in memory.
Config: `ZHILA_AGENT_KEY` or `DEEPSEEK_API_KEY` enables the live provider (`ZHILA_AGENT_URL`, default `https://api.deepseek.com`; `ZHILA_AGENT_MODEL`, default `deepseek-chat`). No key, or `ZHILA_AGENT_MOCK=1`, uses the deterministic mock: "tempo" proposes 132 bpm, "listen" strikes the bass, "play"/"stop"/"render" perform, "bass" proposes a gain.
Surface: the `choir` room (top rail) with the conversation, proposals (commands labelled, approve/reject), a listen button, an input, and a readout of sections, levels, bpm and cores. The agent's voice is Cormorant; the commands are mono.
Verification: `node scripts/agent-smoke.mjs` (mock: ask → proposal → approve → tempo 132, resolved, history); `npm --prefix ui run agent-ui-smoke` (choir ask/approve, readout 132, screenshot `preview-p2-agent.png`); `cargo test -p zhila-daemon` (mock turn, tool mapping).
Limits: in-app only, no MCP; one tool round per ask; the conversation is daemon memory; the mock is the default without a key; the host restart clears the chat (the project keeps the approved edits).
## P2 acceptance
- Arrangement: four 2-bar sections, 8-bar playback, per-section editing, persisted and undoable.
- Render: a valid 48 kHz stereo float WAV of the arrangement (16.5 s, non-silent, peak 0.442).
- Agent: ask → proposal → approve → a project edit applied with `Actor::Agent`, on the same wire.
- The native Tauri app hosts all of it in-process; the browser path uses the standalone daemon.
- `./scripts/check.sh`, the persistence smoke and the three P2 smokes pass.