1
0
Fork 0
forked from hanez/xtc

Phase 8: Add stereo track mixer, automation and undo/redo

This commit is contained in:
Johannes Findeisen 2026-10-27 01:06:57 +01:00
commit 7db5fb7738
45 changed files with 1611 additions and 130 deletions

View file

@ -118,3 +118,15 @@ 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.

View file

@ -42,8 +42,8 @@ without normalization. Failed additions leave IDs and graph contents unchanged.
thread. Empty graphs compile to silence. Sample rate must be finite and positive,
oscillators must be at or below Nyquist, and max_frames is 1..65536. Compilation
allocates and copies topology/parameters, builds a topological schedule and
input adjacency, and allocates one max_frames float buffer per node. The
maximum buffer storage is 64 MiB per plan. Compilation may fail with an
input adjacency, and allocates paired max_frames float buffers per node for stereo. The
maximum buffer storage is 128 MiB per plan (32 MiB for the GUI 64-node limit). Compilation may fail with an
allocation error; output plan is then NULL. Destroy a plan on the control thread.
`xtc_plan_process` uses only prepared state: no allocation/free, sorting,
@ -86,7 +86,7 @@ publication and JACK processing; offline tests alone do not qualify hardware.
Use `xtc_backend.h` on one serialized control thread:
1. Open a config with `XTC_BACKEND_JACK`, a client name, channel_count 1, and
1. Open a config with `XTC_BACKEND_JACK`, a client name, channel_count 1 (or 2 for stereo output), and
max_frames 1..65536. It must accommodate the server's current buffer size.
The actual rate and buffer size are returned through get_status. Open does
not start the server or acquire devices directly. Invalid config returns
@ -134,8 +134,7 @@ JACK notification callbacks were audited for prohibited operations; JACK's own
internal realtime guarantees depend on the installed implementation/server.
Validation commands and results are recorded in BUILD.md and the integration
test README. Current limitations: mono input/output, no parameter ramping or
crossfaded graph changes, no realtime meter bridge, no automatic reconnect,
test README. Current limitations: mono input, no crossfaded graph changes, no automatic reconnect,
no physical-device or long-duration endurance qualification. Phase 3 connects
the FLTK shell to backend controls and status; see GUI.md. Its editors remain
placeholders except the Phase 4 modular editor described below.
@ -250,3 +249,17 @@ 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 stereo mixing and automation
Track settings and automation are copied into immutable source-plan snapshots.
`xtc_audio_plan_render_stereo` and `xtc_midi_plan_render_stereo` pan track audio
and scheduled notes before summing into paired source buffers. Mono wrappers
retain gain/mute/solo/automation but ignore pan. `xtc_plan_process_stereo` and
`xtc_realtime_process_stereo` process both buses through the same graph topology
with one oscillator phase advance, preserving mono APIs and graph publication.
The backend preallocates paired buffers, registers an optional output_right port
and publishes independently atomic output peak snapshots. Callback work remains
bounded and allocation/I/O/lock free. Meter UI reads no active plan memory.
See MIXER.md for exact pan law, automation interpolation, event/source exceptions,
memory/history bounds and verification. Input/recording remain mono.

View file

@ -23,13 +23,15 @@ can briefly block the GUI. Candidate compilation retains the old plan until
success; temporary build memory can include both old and candidate buffers.
The callback renders only overlapping clip ranges from memory. Clips on all
tracks are summed at unity gain into the graph's **Audio clips** source node.
tracks are summed through their mix/automation settings into the graph's
**Audio clips** source node.
Import creates that source and connects it to an existing/new output if no
audio source exists. Subsequent routing is explicit: disconnect it or route it
through gain/meter nodes in Modular Graph. Removing the output silences editor
graphs. This source does not monitor JACK input; unconnected ordinary graph
inputs retain their earlier external-input behavior. Track faders/pan/mute
and individual track routing belong to the mixer phase.
are implemented by the Phase 8 track mixer (see MIXER.md); individual
track graph routing remains deferred.
Linear interpolation converts source sample rate to backend rate. There is no
antialiasing filter, time stretching, looping or fades. Musical durations round

View file

@ -175,3 +175,24 @@ A fresh `build/verify` build passed all targets above with no compiler warnings.
The no-JACK core/GUI suite and Clang ASan/UBSan core/GUI suite also passed; GUI
leak checking uses only the existing fontconfig suppression documented in the
integration README. Physical MIDI and audio devices remain unverified.
## Phase 8 verification
```sh
make all test gui debug test-gui test-gui-jack test-jack-audio test-jack-midi
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
```
`test_mixer` covers stereo gain/pan, mute/solo, sample automation, block
independence, WAV and graph routing, v4 persistence failures and bounded history.
GUI tests add automation mouse edits, mixer keys, history rollback/branching and
save/reopen. The existing live MIDI test now uses two software capture ports and
checks hard-right saved-note playback and output meter snapshots; mono audio
record/play acceptance remains covered. A native Xvfb mouse smoke check confirms
sliders and automation drawing. See MIXER.md for capacities and deferred features.
The fresh Phase 8 verification build, no-JACK core/GUI suite and Clang
ASan/UBSan core/GUI suite passed without compiler warnings. GUI leak checking
retains only the existing fontconfig suppression. ThreadSanitizer also passed
the recorder concurrency and realtime publication tests. Physical devices and
long-running stereo performance remain unverified.

View file

@ -31,7 +31,7 @@ by the GUI. The initially empty graph produces silence.
Use this after server loss or sample-rate changes. A failed close preserves
the handle and reports the failure instead of opening another client.
- **View menu / Ctrl+1, Ctrl+2, Ctrl+3:** switch between sequencer, mixer and
modular graph. Tabs provide the same view switching. The mixer remains a placeholder; graph and sequencer editors are described below.
modular graph. Tabs provide the same view switching. Mixer, graph and sequencer editors are described below.
- **File / Quit / Ctrl+Q** or the window close button: close the backend and
synchronize audio callbacks before hiding/destroying widgets. A failed close
leaves the window open for a retry. New/Open/Save actions are described below. About identifies the development milestone and version.
@ -154,7 +154,8 @@ can block the GUI while reading, but never the audio callback.
See AUDIO_FILES.md for supported WAV formats, memory limits, musical placement,
recording overflow/partial-take recovery, external asset portability and the
software-only acceptance tests. Waveform drawing, input monitoring, recording
latency compensation and per-track mixing are not implemented.
latency compensation and per-track graph inserts are not implemented.
Per-track gain/pan/mute/solo is available in Phase 8 below.
## Phase 7 piano roll
@ -166,3 +167,13 @@ content is retained. Edits stop transport and are blocked during audio recording
Panic/F9 silences held notes; Record continues to mean WAV audio capture.
Modular Graph offers Add instrument for explicit routing. Status includes MIDI
drops. See MIDI.md for voice limits, unsupported messages and live input routing.
## Phase 8 mixer and editing
Mixer (Ctrl+2) now has a selectable track strip, gain/pan controls, mute/solo,
Output L/R peaks and editable gain/pan automation lanes. Mixer edits stop
playback. Click the lane to add/replace points, right-click near a point to
remove, and change First tick to navigate. Ctrl+Z, Ctrl+Shift+Z and Ctrl+Y undo
and redo up to 32 project edits; New/Open resets history. M/S and arrow keys
provide mixer shortcuts. See MIXER.md for exact controls, history rollback,
stereo port routing, automation semantics and prototype limits.

View file

@ -8,7 +8,8 @@ captures audio to WAV: MIDI capture and Standard MIDI File import are deferred.
Add/select a track in Sequencer, choose Add MIDI clip, then Piano roll (Ctrl+4).
Creation adds an instrument-to-output route if no instrument exists. Existing
instruments retain their routes; use Modular Graph to connect them as needed.
All MIDI clips and live input share one mono instrument mix. No hardware ports
Scheduled MIDI clips now apply their track mix/automation before summing to
paired stereo instrument buffers; live input stays centered and unassigned. No hardware ports
are connected automatically. Connect a MIDI source to the backend's `midi_in`
JACK port, and route audio output explicitly to hear it while Play is running.
@ -71,3 +72,5 @@ panic, disconnection and saved/reopened scheduled playback. JACK port discovery
may lag activation under PipeWire; the test waits before connecting.
Physical MIDI devices, bridge/device loss behind an unchanged port, latency and
long-running performance remain unverified. Panic/Stop provides manual recovery.
Phase 8 adds track mixing, automation and project undo/redo; see MIXER.md.

98
docs/MIXER.md Normal file
View file

@ -0,0 +1,98 @@
# Mixer, automation and undo
Phase 8 provides one selectable channel strip per project track. Open Mixer
(Ctrl+2), select a track, and set Gain, Pan, Mute and Solo. Changes stop transport;
Play prepares independent audio/MIDI snapshots with the accepted settings.
No edit changes a plan that the callback is using. Editing is blocked while
recording. Graph edits retain their existing live publication behavior.
Gain is a linear multiplier from 0 to 4. Pan ranges from -1 (left) to +1 (right):
center has unity on both sides and moving away attenuates the opposite side
linearly. This preserves existing centered mono levels; it is not an equal-power
pan law. Mute wins over Solo. If any track is soloed, only soloed, unmuted tracks
play. WAV clips and scheduled MIDI use their containing track's settings before
summing into the audio/instrument graph sources. Live MIDI input and independent
graph oscillators/external input are unassigned sources and bypass track mixing.
Stereo WAV files are still downmixed at import; stereo output comes from panning
those mono sources. Separate per-track graph inserts/sends are deferred.
## Automation lanes
Choose Gain or Pan under Automation. The lane shows four bars (15360 ticks);
First tick moves its viewport across the project. Grid labels are absolute ticks.
Click to add a point or replace the point at that snapped time (1/16 note, 240
ticks). Vertical position maps to gain 0..4 or pan -1..1, top to bottom descending.
Right-click near a point's time to remove it. Removing every point restores the
static control. There are at most 1024 points globally, with one per
track/parameter/tick. Track deletion cascades to its automation.
A nonempty lane replaces that parameter's static value. Between points the C
engine interpolates linearly at each sample, holding the first/last value outside
their range. Preparation converts ticks to integer sample endpoints, sorts and
indexes lanes. When several ticks quantize to one sample at very low sample
rates, the last point wins at that sample. Rendering searches prepared bounded
arrays; it never sorts, allocates, accesses project data, or calls the GUI.
Seeking and block partitioning give the same timeline values. Mute/solo are
static; automation capture, modes, curves and tempo automation are deferred.
## Stereo output and meters
The GUI opens the JACK backend with two audio output ports: `output` (left) and
`output_right`. Input and raw WAV recording remain mono. Connect both outputs
explicitly using an external JACK routing tool. Nothing connects hardware
automatically. Legacy configurations with channel_count=1 and the original
port/route APIs remain supported; output_name(channel) queries either stereo
port. Old mono audio/MIDI render functions apply gain/mute/solo and gain
automation while ignoring pan; the stereo variants apply both lanes.
The graph executes the same topology separately on left and right buses;
oscillators advance once per frame and are centered on both sides. Existing
port metadata and validation describe the mono graph topology. Stereo processing
uses explicit paired buffers, not new port IDs. Output buffers must be distinct;
stereo callers must not alias them with source buffers. Graph-node meter reads
report the maximum of both sides on the processing owner only.
Mixer Output L/R bars show last-block post-graph absolute peaks, polled at 100 ms.
Peaks above unity turn red. Independent lock-free integer snapshots avoid sharing
DSP pointers or float state between callback and GUI; stopped meters show zero.
They are output meters, without per-track RMS, peak hold or loudness analysis.
There is no limiter: finite summed output can exceed unity. GUI polling may miss
brief peaks between refreshes; these bars are not overload history.
## Undo and keyboard workflow
Ctrl+Z undoes; Ctrl+Shift+Z or Ctrl+Y redoes. The C history keeps 32 project edits
plus a baseline, cloning metadata and graph, never audio samples. Common graph,
track, clip, note, mixer, import/relink and automation edits are included. One
accepted gesture is one step; selection, scrolling and graph-node placement are
GUI state and are not undoable. New/Open resets history; Save retains it.
Undo/redo stops and resets transport and resets editor selections/layout. The
candidate is installed before moving the history cursor: validation, asset-plan
or backend failure retains the current document and history position. Missing
assets retain the existing silent/reporting behavior. A new edit after Undo
removes the redo branch; the oldest state is evicted at capacity. Allocation
ID high-water marks survive Undo/branching so abandoned IDs are not reused. History is
session-only and Undo marks the document dirty even if it matches a saved state.
External WAV files are never deleted or restored by Undo; recording itself cannot
be undone while active. An allocation failure retaining a new history snapshot
reports an error; the accepted edit remains in the document.
With mixer focus, M/S toggles mute/solo, Left/Right changes gain by 0.05,
Shift+Left/Right changes pan, and Up/Down selects a track. Native sliders also
support keyboard navigation; Ctrl+1/2/3 switches views, Ctrl+4 opens Piano roll,
F5/F8/Space controls transport and F9 panics, as before. There is no multiselection,
clipboard editing, track rename or automation-point dragging yet.
## Verification and limits
`test_mixer` verifies exact stereo gain/pan, mute/solo precedence, automation
samples and block equivalence, stereo graph phase/routing, WAV mixing, v4 exact
roundtrip/truncations/malformed records, lane capacities and history eviction and
branching. Backend fault injection verifies stereo registration failure, output
names, output separation and atomic meters. GUI tests edit lanes with mouse
events, use mixer keys and exercise undo/redo, failed installation rollback,
branching, save/reopen and history reset. Live JACK MIDI acceptance verifies
saved/reopened hard-right playback and stereo peaks using only software clients.
A native Xvfb mouse smoke check covers sliders and automation drawing.
Physical stereo devices, listening quality, realtime endurance and high-density
project performance remain unverified; no new dependency was introduced.

View file

@ -1,6 +1,6 @@
# Project format v3 (reads v1/v2)
# Project format v4 (reads v1/v2/v3)
Phases 5–7 use a bounded text format implemented in C99 without a parser library.
Phases 5–8 use a bounded text format implemented in C99 without a parser library.
The extension is `.xtc`. File operations run exclusively on the control thread.
The project owns its graph, tracks and silent/audio arrangement regions. Audio samples remain in external WAV
files; MIDI notes are stored inline.
@ -12,7 +12,7 @@ Fields use ASCII spaces. Integers are unsigned decimal, without signs.
Counts determine the number of subsequent records:
```text
XTC_PROJECT 3
XTC_PROJECT 4
APP 0 1 0
TEMPO 120000
IDS 1 1
@ -22,11 +22,13 @@ NODE 1 4 00000000
EDGE 0 1
TRACKS 1
TRACK 0 547261636b
MIX 3f800000 00000000 0 0
CLIPS 1
CLIP 0 0 3840 2880 960 3840 526567696f6e
ASSET -
MIDI 0
NOTES 0 0
AUTOMATION 0
END
```
@ -40,30 +42,37 @@ END
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output, 5 audio source, 6 instrument), and exactly eight
hexadecimal digits encoding the IEEE binary32 parameter bits, independent
of locale and host byte order. Nonfinite values are rejected.
- `EDGE`: source and destination node IDs. Mono port directions are implied
- `EDGE`: source and destination node IDs. Port directions are implied
by node type; endpoints, duplicates and cycles are validated by the graph API.
- `TRACK`: ID and hex-encoded name bytes.
- `TRACK`: ID and hex-encoded name bytes. In v4 each is immediately followed
by `MIX gain_bits pan_bits mute solo`: finite binary32 gain 0..4, pan -1..1
encoded as eight hex digits, mute and solo unsigned 0/1.
- `CLIP`: ID, track ID, start tick, length tick, source offset tick, source
length tick, hex-encoded name bytes. Trimming
preserves source bounds, so a region can re-extend only within its original
length. Overlaps are allowed and audio clips mix through the audio source node.
- `ASSET`: required after each CLIP in v2/v3; "-" means no audio asset, otherwise
- `ASSET`: required after each CLIP in v2/v3/v4; "-" means no audio asset, otherwise
hex-encoded absolute path bytes, <=511 bytes after decoding, no embedded NUL
or ASCII control characters. Each clip directly references a WAV; no sample
data, pointers or DSP state are embedded. Paths can contain spaces.
- `MIDI`: required after each ASSET in v3; 0 for silent/audio regions, 1 for
- `MIDI`: required after each ASSET in v3/v4; 0 for silent/audio regions, 1 for
MIDI clips. MIDI clips must have no asset.
- `NOTES`: next stable note ID and global note count, followed by that many
`NOTE id clip_id start_tick length_tick pitch velocity channel` records.
Notes reference MIDI clips; start/length are source-relative, positive length
within source bounds. Pitch is 0..127, velocity 1..127, channel 0..15.
Note IDs are unique, monotonic uint64 IDs with UINT64_MAX reserved.
- `AUTOMATION`: global point count, after the last NOTE and before END in v4.
Follow with `POINT track_id parameter tick value_bits` for each point.
Parameter 0 is gain (0..4), 1 pan (-1..1); values use finite binary32 hex.
Track must exist; tick <= MAX_TICK; duplicate track/parameter/tick is invalid.
Record order is preserved; rendering sorts a separate prepared copy.
- Names decode to nonempty valid UTF-8 of at most 63 bytes; ASCII control
characters and embedded NULs are forbidden. Hex avoids quoting ambiguity.
Ticks use 960 per quarter note, 3840 per bar. All region endpoints are bounded
by 38400000 ticks (10000 bars). Limits: 64 tracks, 1024 clips, 64 graph nodes,
4096 edges, 4096 notes, 4 MiB input, 1535 bytes per record, 12 tokens per record. The reader
4096 edges, 4096 notes, 1024 automation points, 4 MiB input, 1535 bytes per record, 12 tokens per record. The reader
rejects invalid references, duplicate IDs, inconsistent next IDs, ranges,
truncated records, raw control/non-ASCII bytes, and trailing data.
@ -71,8 +80,10 @@ Unsupported format versions return UNSUPPORTED. Other malformed data returns
FORMAT. The reader explicitly accepts v1 records without ASSET lines, with
node types 1..4, migrating their clips to empty asset paths. Version 2 accepts
node types 1..5 and ASSET records. Both migrate to MIDI=0 and no notes.
Saving always writes v3. Unknown versions are rejected without replacing the destination; older
applications cannot read v3. Migration and maximum-path roundtrips are tested.
Version 3 accepts instrument nodes and MIDI/NOTES records. Versions 1–3 migrate
all tracks to unity gain, centered pan, mute/solo off and no automation.
Saving always writes v4. Unknown versions are rejected without replacing the destination; older
applications cannot read v4. Migration and maximum-path roundtrips are tested.
## Ownership and failures
@ -86,7 +97,8 @@ loss is not guaranteed. Save replaces symlinks themselves and does not preserve
existing file permissions. POSIX I/O is private to the Linux-first persistence
module; public headers contain no POSIX types.
Saved data reproduces the arrangement and graph, including ID allocation state.
Saved data reproduces the arrangement, graph, track mix and automation, including
ID allocation state. Undo history is not saved. See MIXER.md for pan/interpolation.
Graph node positions, timeline zoom/scroll/selection, transport position and DSP
oscillator phase are GUI/session state and are not saved. Asset references are
external absolute paths, not copied on Save As. Metadata loading preserves

View file

@ -91,6 +91,16 @@ See MIDI.md for capacities, immediate note-off, routing and hardware limits.
## Phase 8 — Mixer and editing
Gain/pan, meters, mute/solo, automation, undo/redo, keyboard workflow.
Implemented: selectable per-track channel strips with gain/pan and mute/solo;
stereo graph/JACK output and output peak meters; editable gain/pan automation
lanes with sample interpolation; bounded 32-edit C snapshot undo/redo and mixer
keyboard controls. Format v4 persists mixing/automation and reads v1–v3.
Core, GUI rollback/gesture/history and live stereo software playback tests pass.
See MIXER.md for routing, mono compatibility, pan law, history behavior and limits.
Track inserts/sends, live fader adoption, MIDI capture, per-track meters and
advanced automation remain deferred. No project-file editing is needed for the
implemented composition/mixing workflow.
## Phase 9 — Extensibility and release
Native node API, evaluate plugin hosting, licensing review, packaging,
installation tests, performance and recovery tests.

View file

@ -0,0 +1,17 @@
# 0007 — Track mixing, paired stereo buses and snapshot history
Status: accepted, Phase 8.
Apply gain/pan/mute/solo and gain/pan automation in prepared track sources, then
run paired stereo buses through the existing modular topology. This preserves
mono APIs and graph edits without inventing new graph-port IDs. Center-unity
linear pan preserves previous centered levels. Add optional stereo JACK output
and atomic output peak snapshots; keep input/recording mono.
Use 1024 global automation points, linear sample interpolation, and stopped
preparation for mixer edits. Store settings/points in v4 and migrate v1–v3 to
unity/center/no automation. Use 32 bounded C project snapshots for common undo
edits; commit history movement only after candidate installation succeeds.
Samples and GUI-only state stay outside history. Track graph inserts/sends,
per-track meters, live fader publication, automation modes/capture, nonlinear
curves and equal-power pan are future extensions. See ../MIXER.md.