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

@ -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