3.6 KiB
Project format v1
Phase 5 uses 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.
Schema
Records occur in this exact order, separated by LF, including the final record. Fields use ASCII spaces. Integers are unsigned decimal, without signs. Counts determine the number of subsequent records:
XTC_PROJECT 1
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 2 2 1
NODE 0 1 3e800000
NODE 1 4 00000000
EDGE 0 1
TRACKS 1
TRACK 0 547261636b
CLIPS 1
CLIP 0 0 3840 2880 960 3840 526567696f6e
END
APP: producer major/minor/patch; informational, never executed.TEMPO: constant milli-BPM (20000..300000, default 120000), fixed 4/4.IDS: next track ID and next clip ID. IDs are uint64, monotonic and never reused; UINT64_MAX is reserved. Track and clip ID namespaces are separate.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 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 preserves source bounds, so a region can re-extend only within its original length. Overlaps are allowed; no audio mixing or scheduling is implied.- 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 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.
Ownership and failures
Load builds and validates a separate candidate, closes the input, then replaces the destination. Any failure retains the complete previous project. Saving validates the project, creates a mode-0600 temporary in the destination directory, writes, flushes, fsyncs, closes and renames over the destination. Failures before rename retain the previous destination and remove the temporary. The parent directory is not fsynced: survival of the rename through sudden power 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. 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.