7 KiB
Architecture
Decision: modular engine first
The processing graph is the common model for audio sources, instruments, effects, mixers, and outputs. The sequencer produces time-based content and events consumed by graph nodes. The default workspace remains sequencer-first; the graph editor is an advanced view of the same project.
Layers
- FLTK/C++ GUI: input, drawing, windows, view state.
- C core: project model, commands, parameters, persistence, validation.
- C engine: prepared processing graph, DSP, transport timing, event delivery.
- Backend: hardware and system audio/MIDI integration.
The GUI must not directly modify engine internals. It calls a stable C API.
Graph execution
The first implementation should reject cycles. Compile a graph off the audio thread: validate, topologically sort, determine buffer requirements, allocate resources, and publish a prepared immutable execution plan. The audio thread must not traverse a graph being edited.
The offline engine now copies a validated graph into a prepared plan with
stable node IDs, topological order, precomputed input adjacency, per-node mono
buffers and oscillator/meter state. Each plan has one processing owner. Graph
edits and destruction cannot affect an existing plan. All nodes are scheduled,
including disconnected nodes; only explicit output nodes are rendered (or
terminal nodes for legacy callers that do not require explicit outputs). See AUDIO_ENGINE.md for contracts.
xtc_realtime now provides a one-pending/one-retired handoff around an
exclusively audio-owned active plan. xtc_backend is the C backend boundary;
the optional JACK implementation drives that same realtime engine.
Ownership and threads
- GUI thread owns FLTK widgets and view state.
- Project/control thread owns mutable project data and graph compilation.
- Audio thread owns the active processing plan and DSP state.
- Backend callbacks follow the backend's documented threading contract.
Only acknowledged retired plans are reclaimed during processing. Backend close synchronizes callbacks before reclaiming remaining plans. See decision 0004 for the release/acquire protocol, bounded resource retention and recovery.
GUI backend ownership
The Phase 3 window owns one C backend handle and serializes all control calls on the GUI thread. A GUI timer reads snapshots and collects retired resources; no audio callback calls FLTK. Successful window close first closes the JACK client (synchronizing callbacks), then removes the GUI timer and hides the window. Failed close leaves both handle and window available for retry. View switching and node positions are GUI state only. Semantic edits use transactional C commands and publish independent compiled snapshots. See GUI.md.
Editable graph and canvas
Phase 4 adds read-only C snapshots, monotonic node IDs, and a small set of C edit commands. The GUI owns an opaque graph handle, never private arrays. An edit produces a candidate, and the GUI commits it only after backend submit succeeds (or locally while offline). A busy pending slot or invalid edit leaves the prior model/plan unchanged. Active/submitted revisions distinguish the queued drawing from the engine's acknowledged plan. Empty/output-less editor graphs render silence; legacy offline terminal fallback remains available.
GraphWidget owns only selection, node positions, pointer gestures and drawing. Its fixed canvas is hosted by FLTK scrolling. C owns nodes, ports, edges, validation, ID assignment, compilation and rendering. No audio thread accesses GUI-owned state. Reconnect republishes the same retained editable graph.
Project ownership and persistence
Phase 5 introduces a C-owned project containing the graph, bounded track/clip arrays, stable ID allocators and constant tempo. Read APIs return copies; GUI canvases borrow the opaque project/graph on the control thread. A graph edit prepares a candidate project before submitting its graph, then commits ownership only after acceptance. Arrangement edits currently change silent metadata and require no audio publication.
Persistence is private C/POSIX code; loading validates an isolated candidate and swaps ownership only after a successful close. The GUI similarly prepares a loaded document before stopping/submitting audio. File operations never run in the callback. See PROJECT_FORMAT.md for schema, bounds and durability. SequencerWidget owns selection, zoom and gestures only; musical range checks, source-preserving trims and ID allocation remain C responsibilities.
Phase 6 assets and recording
Audio paths are C clip metadata. A stopped preparation copies range/offset information, deduplicates paths, validates and preloads bounded WAV assets into an independent C audio plan. The backend owns that plan and a preallocated timeline buffer until callback quiescence. A graph audio-source node consumes the buffer, so effects/output routing remain graph operations. All tracks share the mono source in this phase; no audio summation bypasses the graph.
Arrangement edits stop transport; subsequent Play prepares the new snapshot. Graph-only edits retain live plan publication. Missing assets preserve metadata and render silence, with repair through a C path setter and GUI relinking. Version 2 persistence stores references and explicitly migrates version 1 loads.
Recording has one callback producer and one control-thread consumer. A bounded SPSC ring publishes complete payload blocks through lock-free indices. Only the consumer writes WAV bytes; finalization follows successful callback stop. No editable project pointer, GUI object or FILE is accessed by the producer. See decision 0005 and AUDIO_FILES.md for format, limits and recovery contracts.
Phase 7 MIDI source
The C MIDI plan prepares bounded source-relative notes into sample spans and
renders a separate instrument buffer. xtc_plan_process_sources and
xtc_realtime_process_sources distinguish external audio, WAV timeline and
instrument input; older wrappers supply silence for omitted sources. Graph
instrument nodes consume the shared mono instrument buffer. The backend owns
prepared MIDI runtime state exclusively while active, with lock-free panic
requests and no callback allocation/I/O. Stop/seek clear live voices; scheduled
notes chase their absolute phase. See MIDI.md for event ordering, panic semantics,
capacities and test coverage. No new dependency is required.
Phase 8 mix snapshots and history
src/mixer owns validated per-track settings and bounded gain/pan automation.
Audio/MIDI preparation copies and indexes these into source snapshots; paired
source buffers feed the same graph independently per stereo side. Atomic backend
output-peak snapshots cross to the GUI without exposing DSP state. Mixer edits
stop transport before subsequent preparation. src/project/history.c provides
32-edit C project snapshots; checkout clones a candidate and accept moves the
cursor only after successful GUI/backend installation. No samples or FLTK state
are stored in history. mixer_widget.cpp is only controls/drawing/gestures.
See MIXER.md and PROJECT_FORMAT.md for ownership, limits and v4 migration.