1
0
Fork 0
forked from hanez/xtc

Phase 6: Add WAV clips, graph playback and bounded recording

This commit is contained in:
Johannes Findeisen 2026-10-27 00:31:03 +01:00
commit 8a37090b81
40 changed files with 1450 additions and 69 deletions

View file

@ -85,3 +85,24 @@ 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.

View file

@ -203,3 +203,38 @@ and delayed by polling. No external JACK transport synchronization is provided.
Silent clip metadata is control-thread data, never traversed by the callback.
Audio/MIDI scheduling will be added in later phases through compiled event/clip
plans rather than direct callback access to the editable project.
## Phase 6 audio source and recording
xtc_audio.h provides control-thread import and immutable preloaded clip plans.
The backend prepares a project only while stopped: build audio candidate,
compile/submit its graph, then replace the old audio plan without a later
fallible step. Preparation failures retain both old plans. Missing/invalid
assets produce explicit issue metadata and silence; OOM/capacity fails.
Graph-only edits still use the existing pending/retired mechanism while running.
Each callback renders the plan into a preallocated mono timeline buffer using
the authoritative sample position. xtc_realtime_process_audio passes that
buffer into the compiled graph; XTC_NODE_AUDIO (output port only) reads it,
independently from external JACK input. The old process APIs remain wrappers
with a silent timeline. Import creates a source/output route if necessary.
No samples or GUI/project arrays are shared with editable state. Clip mixing,
rate conversion and graph processing are bounded; see AUDIO_FILES.md for limits.
A borrowed recorder can be attached/detached only while stopped. Raw JACK input
is copied to its SPSC ring before graph processing. Producer and consumer index
release/acquire operations protect payload ownership; all atomics are checked
always lock-free. The control thread drains to a temporary float32 WAV, then
finalizes only after successful stop/close has synchronized the callback.
An overflowing ring or unavailable input creates a sticky error; later input is
dropped, reported and never silently joined into a continuous-looking take.
A partial overflow take is finalized for manual recovery. File errors retain
the previous destination and remove unfinished temporaries.
GUI recording uses a two-second ring and 100 ms control-thread draining, with
no worker thread. A GUI stall can overflow it. No callback allocation/free, file
I/O, logging, locks or FLTK calls were added. Backend stop failure preserves the
borrowed recorder; the caller must not free it before successful quiescence.
Live software source/capture acceptance and concurrent ring tests pass; physical
recording, latency compensation and disk endurance remain unverified.

109
docs/AUDIO_FILES.md Normal file
View file

@ -0,0 +1,109 @@
# Audio clips and recording (Phase 6)
## Import and playback
File/Import WAV (Ctrl+I) loads a WAV into the selected track at the playhead.
With no selected track it creates an Audio track. Add region still creates a
silent placeholder. Imported clips use the existing move/trim controls.
Import stops playback, validates a candidate project, and commits only after
successful backend preparation (or locally when disconnected).
Supported input: little-endian RIFF/WAVE, PCM16/24/32 and IEEE float32, one or
two channels, 1..768000 Hz. Stereo is averaged to mono. Compressed WAV, RF64,
WAVE_FORMAT_EXTENSIBLE, PCM8, other containers and multichannel files are
unsupported. Nonfinite float data and malformed/truncated files are rejected.
There is no new dependency: the bounded C decoder/writer is covered by known
sample, truncation and invalid-codec tests. See decision 0005.
Audio is preloaded before playback: maximum 64 distinct paths per plan,
16777216 mono frames per asset (64 MiB), 67108864 total frames (256 MiB).
Import rejects files over 256 MiB or over 4096 chunks. No streaming decoder,
waveform cache, peak drawing or background load worker exists yet. Preloading
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.
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.
Linear interpolation converts source sample rate to backend rate. There is no
antialiasing filter, time stretching, looping or fades. Musical durations round
up to a tick on import; playback boundaries and trims use floor tick-to-sample
conversion. A rounded tail pads with silence past the source, and a recording's
placement can differ by less than one tick from the capture start sample.
Changing tempo changes the musical span, not the source playback rate.
Arrange edits stop playback. Play preloads and adopts the current arrangement;
Stop pauses the sample clock. Graph-only edits can still be submitted during
playback. Graph recompilation can reset oscillator phase as before. Playback
does not stop automatically at the arrangement end.
## Assets and recovery
Version 2 projects store external absolute canonical paths, not sample data.
Save the project AND keep its referenced WAV files. Save As changes only the
metadata path; it does not copy assets. Moving/deleting a WAV makes its regions
silent, preserves their IDs/positions and reports the path and unique issue
count on Open/Play. An already loaded plan keeps playing even if its file is
removed; the next preparation detects it. Unreadable, malformed and unsupported
assets are also reported, rather than substituted with unrelated files.
Capacity/OOM is a preparation failure and retains the old plans.
Select the affected clip, then File/Relink selected audio to choose a replacement.
Relinking retains its original timeline/source bounds. The replacement is
validated on the next Play; a shorter replacement pads beyond its end with
silence. Saving retains unresolved references for later repair. Missing assets
are also checked on Open without a working backend, using a temporary plan.
Automatic asset search, content hashes and portable project bundles are deferred.
## Recording
Record (or Audio/Record WAV, Shift+F5) opens a destination chooser, stops playback,
prepares the project, opens a temporary mono float32 WAV and starts JACK.
Connect a source to xtc's JACK input using an external patchbay; no physical or
software route is created automatically. Without a route the take contains
silence. Recording captures raw mono JACK input, not the output/effect mix.
A preallocated two-second SPSC ring transfers complete callback blocks to the
GUI/control thread, which drains it every 100 ms. The producer uses verified
lock-free atomics and bounded copies only. It never allocates, logs, takes a
mutex, opens/writes files or calls FLTK. A slow disk/blocked GUI can overflow
the ring; capture then stops accepting input with a sticky, visible capacity
error rather than hiding gaps or compressing time.
Stop first synchronizes the callback, detaches the recorder, drains remaining
samples, writes the final WAV header, flushes/fsyncs/closes and renames the
same-directory temporary over the destination. A successful nonempty take is
imported at its capture position on the selected track (or a new Recording
track), then the document becomes unsaved. Save the project after Stop. Seeking
back to the take and pressing Play auditions it. Recording continues existing
arrangement playback, but there is no automatic input monitoring, punch, loop,
latency compensation, multitrack arming or overdub alignment compensation.
Graph/arrangement edits, project file actions, reconnect and close are blocked
until Stop finishes the take. Deactivation failure retains the recorder for a
safe retry. On overflow a finalized partial WAV is retained for manual recovery
and is not automatically added. On import/preparation failure the completed
take remains at its chosen path with an explicit recovery message. I/O errors
are reported; an unfinished temporary is removed on destruction. Recordings
are RIFF-limited to 1073741814 frames, but the smaller playback/import asset
limit still applies: longer takes may need external splitting before import.
There is no crash-recovery journal. The file is mode 0600; parent-directory
fsync and preservation of previous permissions are not implemented.
## Verification
make test includes deterministic WAV/graph samples, resampling/overlap/trim,
malformed inputs, missing/relinked assets, SPSC concurrency/overflow/cleanup,
and record -> save/reopen -> exact playback through the backend callback.
make test-gui covers the workflow and rollback with a fake backend.
make test-gui-jack records/reopens a silent take using real JACK callbacks.
make test-jack-audio routes only two software clients: known source -> recorded
WAV -> saved/reopened project -> graph playback -> capture assertion.
Physical input/output, device latency, slow-disk endurance and server loss
during a real recording remain unverified.

View file

@ -128,3 +128,29 @@ and region, moved it one bar and zoomed the canvas. No dependencies were added.
Phase 6 will add audio assets and recording. Silent arrangement regions, fixed
tempo/4/4, no undo, no persisted GUI layout, and unverified physical-device
playback/endurance remain explicit limits.
## Phase 6 verification
```sh
make all test gui debug test-gui test-gui-jack test-jack-audio
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
make BUILD_DIR=build/tsan CC=cc CFLAGS='-O1 -g -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude -fsanitize=thread' LDFLAGS='-fsanitize=thread' build/tsan/test_audio build/tsan/test_realtime
./build/tsan/test_audio
./build/tsan/test_realtime
```
No new dependency is required for WAV import, recording or playback. New audio
sources remain C99; only GUI controls use C++/FLTK. test_audio covers decoder
samples, truncations/nonfinite/unsupported input, resampling, trim/overlap,
missing assets and concurrent recording. test_backend records known callback
input, saves/reopens and checks exact graph output. Project tests cover v1
migration and 1024 maximum-length asset paths in a valid v2 file.
The live test-jack-audio target connects only its software clients and checks
record -> WAV -> saved/reopened project -> graph playback. GUI/JACK tests also
record, finalize, save and reopen a take without hardware routing. Clang
ASan/UBSan passes the core and GUI tests (GUI retains the existing fontconfig-only
LSan suppression). ThreadSanitizer passes the concurrent recorder and existing
realtime handoff tests. Hardware capture/playback, disk endurance and real server
loss during recording remain unverified. See AUDIO_FILES.md.

View file

@ -26,7 +26,7 @@ from an audio callback. No server is started and no audio ports are connected
by the GUI. The initially empty graph produces silence.
- **Play / F5:** start the engine. **Stop / F8:** deactivate it. **Space:** toggle.
These advance/pause the sample-counted playhead; regions are currently silent.
These advance/pause the sample-counted playhead; imported audio clips play through their graph source.
- **Reconnect / F6:** close the old client and open a fresh stopped client.
Use this after server loss or sample-rate changes. A failed close preserves
the handle and reports the failure instead of opening another client.
@ -123,10 +123,35 @@ ranges from 24 to 384 pixels/quarter. Names, tempo and 4/4 meter are drawn.
A ruler click seeks while stopped (or positions the cursor when disconnected).
The 100 ms GUI refresh converts the backend's actual sample counter to ticks;
it never integrates wall-clock time. Play resumes and Stop pauses. Reconnect,
New and Open reset position. Regions contain no audio or MIDI and cannot
produce sound yet; modular graph processing continues as before. The C model
New and Open reset position. Add region creates silent metadata; imported WAV clips play through the
audio source node (Phase 6 below). MIDI content is not implemented. The C model
supports a constant tempo; a tempo map and scheduler are future work.
Tests cover C-model/persistence boundaries, GUI move/trim gestures and zoom,
save/reopen, failed load/submission rollback, sample-derived cursor updates,
and live JACK playhead advance/pause.
## Phase 6 audio workflow
Use File/Import WAV (Ctrl+I) to add an audio clip at the playhead on the selected
track, or on a new Audio track. Record (Shift+F5) chooses a WAV destination and
captures JACK input until Stop finalizes and adds the take. No input routes are
created automatically. Save after Stop; the project references the external WAV.
Import creates an Audio clips graph source and output route if the source is
absent. Modular Graph offers Add audio source for explicit routing through gain
or meter nodes. All tracks currently share one mono timeline mix. Arrangement
edits stop playback; graph edits remain live outside recording. During recording,
edits, file actions, reconnect and close require Stop first.
Open/Play reports missing, unreadable or unsupported assets, preserving their
regions as silence. The backend status shows the unique issue count. Select a
clip and use File/Relink selected audio to choose another path; Play validates
the replacement. Playback uses preloaded immutable samples; imports and loads
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.

View file

@ -1,9 +1,9 @@
# Project format v1
# Project format v2 (reads v1)
Phase 5 uses a bounded text format implemented in C99 without a parser library.
Phases 5/6 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 arrangement regions; audio files,
recording and MIDI content arrive in subsequent phases.
The project owns its graph, tracks and silent/audio arrangement regions. Audio samples remain in external WAV
files; MIDI content is a subsequent phase.
## Schema
@ -12,7 +12,7 @@ Fields use ASCII spaces. Integers are unsigned decimal, without signs.
Counts determine the number of subsequent records:
```text
XTC_PROJECT 1
XTC_PROJECT 2
APP 0 1 0
TEMPO 120000
IDS 1 1
@ -24,6 +24,7 @@ TRACKS 1
TRACK 0 547261636b
CLIPS 1
CLIP 0 0 3840 2880 960 3840 526567696f6e
ASSET -
END
```
@ -34,28 +35,34 @@ END
- `GRAPH`: output-node policy (0/1), next node ID, node count, edge count.
Nodes retain stable IDs (limited to the host's size_t range). No pointers or
compiled state are serialized.
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output), and exactly eight
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output, 5 audio source), 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
by node type; endpoints, duplicates and cycles are validated by the graph API.
- `TRACK`: ID and hex-encoded name bytes.
- `CLIP`: ID, track ID, start tick, length tick, source offset tick, source
length tick, hex-encoded name bytes. Regions are silent metadata. Trimming
length tick, hex-encoded name bytes. Trimming
preserves source bounds, so a region can re-extend only within its original
length. Overlaps are allowed; no audio mixing or scheduling is implied.
length. Overlaps are allowed and audio clips mix through the audio source node.
- `ASSET`: required after each CLIP in v2; "-" means silent, 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.
- 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, 1 MiB input, 511 bytes per record, 12 tokens per record. The reader
4096 edges, 2 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.
Unsupported format versions return UNSUPPORTED. Other malformed data returns
FORMAT. No implicit migration is attempted; future revisions require explicit
migration code and roundtrip tests.
FORMAT. The reader explicitly accepts v1 records without ASSET lines, with
node types 1..4, migrating their clips to empty asset paths. Saving always writes
v2. Unknown versions are rejected without replacing the destination; older
applications cannot read v2. Migration and maximum-path roundtrips are tested.
## Ownership and failures
@ -71,5 +78,8 @@ module; public headers contain no POSIX types.
Saved data reproduces the arrangement and graph, including ID allocation state.
Graph node positions, timeline zoom/scroll/selection, transport position and DSP
oscillator phase are GUI/session state and are not saved. There are no external
asset references yet; missing-asset recovery belongs to Phase 6.
oscillator phase are GUI/session state and are not saved. Asset references are
external absolute paths, not copied on Save As. Metadata loading preserves
missing references; audio-plan preparation reports unreadable/invalid assets
and renders them silently. Relinking replaces a selected clip's path without
changing its IDs or arrangement. See AUDIO_FILES.md for recovery and limitations.

View file

@ -66,6 +66,16 @@ no undo, asset playback, MIDI or persisted GUI layout are explicit limits.
Audio import/playback, recording through bounded buffers, missing-asset
handling, no file I/O in callbacks.
Implemented: bounded PCM16/24/32 and float32 WAV import (mono/stereo downmix),
preloaded overlap-summed clip plans with linear rate conversion, and an audio
source node using ordinary graph routing. A lock-free SPSC ring transfers JACK
input to a control-thread WAV writer; Stop finalizes and adds successful takes.
Version 2 metadata stores external asset references and reads v1. Missing assets
remain silent with reporting and manual relinking. Core, callback, GUI and live
software record/save/reopen/play acceptance tests pass. See AUDIO_FILES.md for
memory/format limits, overflow recovery and deferred physical-device validation.
No new dependency or callback I/O was introduced.
## Phase 7 — MIDI and instrument
MIDI input/events/clips, simple internal instrument, note-off/panic handling,
then a basic piano roll.

View file

@ -0,0 +1,26 @@
# 0005: Preloaded WAV clips and control-thread recording consumer
Status: accepted for Phase 6.
Use a small bounded C99 WAV decoder/writer for PCM16/24/32 and IEEE float32,
mono/stereo, instead of adding libsndfile in this milestone. This avoids a new
build/runtime/license dependency, keeps the supported format surface explicit,
and enables malformed-input tests. It also limits format coverage; a future
codec library needs its own dependency and licensing review.
Compile musical clip ranges and decoded, shared-per-path mono buffers while
stopped. The callback renders the timeline into a preallocated buffer consumed
by an explicit graph source node, preserving graph routing rather than mixing
outside the graph. The old snapshot remains until candidate preparation succeeds.
All track clips currently share one source; per-track routing/mixing is deferred.
Use a bounded SPSC ring for callback input capture, with release/acquire index
publication and a GUI/control-thread consumer. This adds no worker-thread
dependency; a two-second ring tolerates normal 100 ms polling but exposes
overflow if the GUI or disk stalls. Sticky errors prevent silent time compression.
Finalization requires callback quiescence. Future long-session/background I/O
work can reuse the ring contract with a dedicated consumer.
Store external absolute paths in version 2 metadata and retain version 1 loads.
Missing/unreadable assets are reported silence, with explicit relinking; portable
bundles and content-addressed assets are deferred. See AUDIO_FILES.md for limits.