Files
zhila/docs/P1.md
T

6.1 KiB

P1a · material and the quarry

P0 proved the spine: Rust audio, a session, a protocol, a surface. P1a puts material into it: a file becomes a core, a core becomes sixteen faces, a face becomes a voice in the pattern.

The path of a sound

file input (browser)
  → ArrayBuffer
  → ws: import_core { id, name, seal } + binary audio
  → zhila-material: symphonia decode → mono f32 at the source rate
  → onset analysis: 10 ms energy hops, positive flux, local peaks
  → sixteen faces (sample ranges + RMS + rank)
  → waveform peaks (1400 bins, normalised)
  → zhila-core: Core { summary, Arc<[f32]>, faces }
  → Session::import_core: archive + journal + sync_material
  → engine: Arc<MaterialSnapshot> through the control queue
  → snapshot broadcast → the quarry

The pattern keeps its procedural voices until a row is given a face. set_pattern_face writes the reference and syncs a new material snapshot; the audio thread triggers a sample voice for that row's steps and leaves the other rows procedural. set_pattern_step edits the rhythm; both are journaled. strike_face auditions a face without touching history.

Pieces

Piece What it does
zhila-material decode (symphonia), downmix to mono, onset analysis, sixteen faces, ranks, waveform peaks, content-hash id
zhila-core Core, AudioCore, AudioFace, MaterialSnapshot; archive; pattern face/step commands; journal; engine sync
zhila-engine sample voices alongside procedural voices; a step plan that resolves a row's face to a core/face; rate-correct playback; 4 ms fades
zhila-daemon binary import after a text header, decoded in spawn_blocking; broadcast snapshot; new commands flow through the session
zhila-protocol core/face summaries, face references, import messages, pattern commands/events
ui quarry: import, core strip, waveform, face wall, audition, assign, pattern grid; the descent remains the nave

Verification

  • cargo test --workspace: 10 core tests, 2 material tests, 5 protocol tests.
  • cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --check.
  • node scripts/material-smoke.mjs: a live import of fixtures/clicks-01.wav, sixteen faces, a face laid on row 1, playback, non-zero meters, face_struck.
  • npm --prefix ui run quarry-smoke: the browser imports the fixture, selects a face, assigns it, plays, screenshots the quarry and the nave, checks 80 steps and 16 faces.
  • npm --prefix ui run smoke: the P0 regression still passes.

The screenshots are preview-p1-quarry.png, preview-p1-nave.png and the empty state preview-p1-quarry-empty.png.

Limits

  • In-memory archive and pattern; no project file, no save/load, no asset persistence. A daemon restart loses the material and edits.
  • No recording, no waveform trim, no manual cut/loop points. Seams are automatic.
  • No undo/redo for pattern edits; the journal is written but not exposed.
  • One face per pattern row; no per-step face, pitch or probability yet.
  • Formats are whatever symphonia decodes; WAV is the tested path.
  • The Tauri shell now builds and runs; the engine remains the separate daemon (embedding is next).

Next

P1b: a project folder with SQLite, content-addressed assets, autosave and reload, pattern-edit undo, recording, and waveform trim. Then the arrangement and the agent.


P1b · project and history

P1b makes the session durable and reversible.

Project folder

The daemon loads and saves a portable folder, by default ~/.local/share/zhila/descent-04 (override with --project PATH):

project.db      SQLite: project row, cores, journal
assets/<id>.<ext>   the encoded source bytes, content-hash named

The project row stores name, revision, bpm, gains and the pattern JSON. Cores store id, name, seal, asset path and source name. The journal stores every project edit with its actor and timestamp. On load the material crate decodes each asset again, so faces and peaks are derived from the bytes rather than trusted metadata; patterns that reference a missing face are sanitised with a warning.

Autosave happens after imports and after every project command (tempo, gains, pattern step/face, undo/redo). The save runs under the session lock; the project is small, the write is a transaction. Play/stop/strike are performance and do not trigger a save.

Undo

The core keeps up to 64 snapshots of (bpm, gains, pattern). Tempo, gain, step and face edits push a snapshot before applying. undo restores the previous snapshot and pushes the current one to redo; redo reverses. Imports are not undoable in P1b (the archive and the pattern faces would need a coordinated removal); the import remains. The journal records undo/redo; the history itself is not persisted, so a reloaded project starts with an empty undo stack.

Protocol

  • State.project_name, State.can_undo, State.can_redo.
  • Commands undo, redo; event history { can_undo, can_redo }.
  • The project path is server-side; only the name is in the state.

Verification

  • cargo test --workspace: 12 core tests (including undo/redo and project loading), 1 project store test, 2 material, 5 protocol.
  • node scripts/persistence-smoke.mjs: spawns a daemon on a temporary project, imports the fixture, assigns a face, undoes/redoes, edits a step and tempo, restarts the daemon, and verifies the core (16 faces), face, step 96, 132 bpm, journal and project.db.
  • npm --prefix ui run project-smoke: browser project name, import, face assign, undo → procedural, redo → face, screenshot.
  • npm --prefix ui run restored-smoke: after restarting the daemon on the saved project, the browser shows the core, sixteen faces and the face on the row, with undo disabled.

Screenshots: preview-p1b-quarry.png, preview-p1b-restored.png.

Limits

  • Undo history is per session; no reconstruction from the journal yet.
  • Only the source bytes are archived; trimming/recording will add derived assets.
  • The engine is still the standalone daemon; the Tauri backend does not host it yet.
  • A failed autosave logs a warning and leaves the in-memory session running; the surface does not yet show a save error.