mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-26 19:46:47 -06:00
The desktop app installed no logger, so every log::warn!/log::error! in the workspace was discarded: the engine's panic report, audio devices that failed to open or broke, plugins that refused their stored state or failed to activate, CLAP plugins' own log messages, files that failed to open. A launch from a desktop menu has no terminal either, so there was nothing to attach to a bug report. apps/soundcraft/src/logging.rs (no new dependency): - standard error plus <settings dir>/logs/soundcraft.log (beside ui.json, Autosave/ and Presets/); each launch rotates the previous log to soundcraft.1.log and .2.log, so the log of a run that crashed survives the next start; the file is capped at 16 MiB - records logged before the settings directory is known are buffered (bounded) and written once the file is attached - default levels: info for soundcraft*, warn for everything else; RUST_LOG replaces them with env_logger-style directives - never panics: an unusable directory or a failed write leaves standard error as the only sink; a poisoned lock is tolerated - runs with SOUNDCRAFT_NO_PREFS (agents' test runs) log to standard error only, so they don't rotate away the user's own logs Realtime audio: a record takes a lock and does I/O, which the audio callback must never do, yet code on the audio thread logs (the cpal error callbacks, the synth's full event queue, a hosted plugin's failed process, a CLAP plugin calling the host's log, the panic hook). The cpal callbacks now mark their thread (soundcraft_mix::mark_audio_thread, re-exported by soundcraft_playback), and the mix engine carries the mark into the rayon workers that render strips for it. A record from a marked thread is not written there: the first is kept in a fixed buffer (try_lock, no allocation, never waits), the rest are counted, and the UI thread writes them every frame (any other thread before its own next record). The logger is installed first in main (before the engine's panic hook) and the file is opened after argument parsing (--version leaves no file). The settings directory moves out of prefs_path into config_dir. The control server's eprintln!s go through the logger (the SOUNDCRAFT_CONTROL_PORT line on standard output stays). Documented in README.md (Getting started > Logs) and docs/architecture.md (Audio).
55 lines
3.1 KiB
Markdown
55 lines
3.1 KiB
Markdown
# Architecture
|
||
|
||
SoundCraft is a Cargo workspace of small crates with enforced layering (`cargo xtask layers`).
|
||
Nothing below the UI knows about egui, so the interface can be replaced.
|
||
|
||
```
|
||
L0 time audio-io midi (standalone: no workspace deps)
|
||
L1 dsp clap-host (plugins; clap-host is the only unsafe crate)
|
||
L2 model (the session document)
|
||
L3 mix (the mix engine)
|
||
L4 engine playback (commands, undo, I/O · audio devices)
|
||
L5 automation (MCP server, control-channel client)
|
||
L6 ui-egui (the user interface)
|
||
apps: soundcraft (desktop) · soundcraft-cli · soundcraft-web (wasm)
|
||
```
|
||
|
||
## The document
|
||
|
||
`soundcraft_model::Session` is plain data (serde): tracks with playlists of clips, mixer state
|
||
(inserts, sends, routing, automation lanes), tempo and meter maps, memory locations, groups,
|
||
busses, and `EditState` (selection, tools, modes, view flags). Decoded audio lives in a
|
||
`SourcePool` of `Arc`s that is not serialised; cloning a session is cheap. Sessions save as
|
||
`.scraft` JSON plus an `Audio Files/` folder.
|
||
|
||
## Commands and undo
|
||
|
||
Every user action is a `CommandSpec` (id, label, menu path, shortcut, params doc, enabled, run)
|
||
registered in `crates/engine/src/cmd/`. `Engine::execute(id, params)` runs it, catches any escaped
|
||
panic, and pushes an undo snapshot (the previous `Arc<Session>`) when the document changed.
|
||
Continuous gestures use `execute_merged` so a fader drag is one undo step. Menus are built from
|
||
the incumbent's menu catalog (`crates/engine/catalog/menus.txt`); a menu item lights up when a
|
||
command has the same menu path and label (or an alias maps it), which also drives the parity
|
||
report.
|
||
|
||
## Audio
|
||
|
||
`soundcraft_mix::MixEngine::render(session, pos, frames, out)` renders one block: clips (gain,
|
||
fades, clip effects) → trim → inserts → pre-fader sends → fader/mute (automation, VCA, trim
|
||
automation) → post-fader sends → pan → busses → aux inputs → master faders. Independent strips
|
||
process in parallel; plugin delay compensation aligns every path. The same engine renders
|
||
bounces offline (`render_range`) and realtime playback (`soundcraft_playback::Player`, which owns
|
||
it on the audio thread and receives new session snapshots through a channel).
|
||
|
||
The cpal callbacks mark their thread (`soundcraft_playback::mark_audio_thread`). Code that may run
|
||
there can ask `on_audio_thread()` before doing anything that blocks; the desktop app's logger does,
|
||
so a `log::` record from the audio thread (a stream error, a full synth event queue, a hosted
|
||
plugin's failed `process`, a CLAP plugin's own log call) is kept without waiting and written later
|
||
by the UI thread (see README › Logs).
|
||
|
||
## Agent control
|
||
|
||
The desktop app's control channel (JSON lines over TCP) exposes engine commands, inspection,
|
||
synthetic input and screenshots; `soundcraft-cli mcp` wraps either a headless engine or a running
|
||
app as an MCP server. See `control-protocol.md` and `mcp.md`.
|