xtc/docs/ARCHITECTURE.md

12 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

  1. C++ GUI: input, drawing, windows, view state. FLTK is the default; src/gui/JUCE is an independently optional JUCE frontend using the same C API.
  2. C core: project model, commands, parameters, persistence, validation.
  3. C engine: prepared processing graph, DSP, transport timing, event delivery.
  4. 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 GUI 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.


Sequencer Widget Ownership

SequencerWidget is the master FLTK group. It owns the timeline coordinate system, grid/background, selection, zoom, scrolling extent, expansion state, shared edit hooks and gestures that cross tracks. Drawing and local input belong to child widgets:

flowchart TD
    S[SequencerWidget] --> R[SequencerRulerWidget]
    S --> T[SequencerTrackWidget per track]
    T --> H[SequencerTrackHeader]
    H --> I[Fl_Choice instrument selector]
    T --> C[SequencerClipWidget per clip]
    T --> V[SequencerVelocityWidget]

FLTK groups own and destroy their children. The master's track index and each track's clip index contain borrowed pointers; they do not independently delete children. Views reconcile copied C metadata by stable IDs when edits, project installation or drawing refresh the timeline. Surviving track views retain their identity through reorder and undo; clips moved between tracks acquire a view under their destination group. No child keeps a borrowed pointer into a C project snapshot. Removed views are detached, hidden and scheduled with Fl::delete_widget, allowing the current event handler to return safely. A project replacement cancels captured gestures before refreshing the hierarchy.

The ruler handles seeking and loop selection. Track groups manage row layout; headers handle disclosure/selection and own instrument controls. Clip widgets draw notes or cached waveforms and start selection/move/trim gestures; the master completes cross-track edits. Velocity children own MIDI/audio previews and local point/bar hit testing. All mutations still use the master's before-edit and changed callbacks, preserving C history, transport stopping and plan publication. Instrument keyboard focus is handled before timeline shortcuts.

Implementation map:

Files in src/gui/ Responsibility
sequencer_widget.hpp/.cpp Master API, layout/reconciliation, shared selection, gestures and timeline overlays
sequencer_track_widget.hpp/.cpp Track groups, pinned header groups and nested instrument selectors
sequencer_clip_widget.hpp/.cpp Clip drawing and local selection/gesture initiation
sequencer_velocity_widget.hpp/.cpp MIDI velocity bars and continuous audio gain envelopes, including preview state
sequencer_ruler_widget.hpp/.cpp Bar/marker labels, loop display, seeking and loop gestures
sequencer_commands.cpp Editing commands and dialogs exposed by the master
sequencer_instruments.cpp Instrument catalog and selector callbacks
sequencer_waveforms.cpp Shared control-thread waveform cache and waveform drawing
sequencer_group_scope.hpp Restore FLTK's implicit parent around dynamic child construction

cmake/Gui.cmake lists all implementation files explicitly in xtc-gui. The hierarchy adds no dependency, C API, project format or audio-thread work. GUI integration checks cover actual parent relationships, stable track views, FLTK window-dispatched clip/velocity drags, cross-track movement, deferred removal, project replacement during a captured gesture and undo/redo.


Editable Graph and Canvas

Read-only C snapshots, monotonic node IDs, and a small set of C edit commands are provided. 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

A C-owned project contains 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.


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; 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.


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.


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.


Native Lifecycle and Distribution

xtc_native.h defines copied versioned C descriptors and bounded block contexts. Native effect/source nodes prepare independent instances on control-thread graph compilation and reclaim them only on compile failure, quiescent destruction or retirement collection. Event instruments can render into the public graph source buffer. No descriptor/state pointers enter project files; unsupported native saves preserve files. See NATIVE_API.md and decision 0008. Release tooling lives outside the runtime, with source manifests, deterministic exports and staged installation/consumer checks. See RELEASING.md for reproducibility scope.

JUCE frontend ownership

The optional xtc-juce application ports the complete retained GUI command structure under src/gui/JUCE, without linking FLTK. ui.* supplies JUCE components, native controls, graphics, event dispatch, timers, scrolling and dialogs while preserving the original C-facing command boundaries. The master sequencer owns JUCE ruler/track groups; tracks own header, instrument, clip and velocity components. Group ownership, borrowed indexes, immutable copied C metadata and deferred deletion follow the same rules as the FLTK frontend. JUCE renders overlays after child components and routes callbacks on the message thread. The audio backend and callbacks remain in C. See the JUCE source map.