369 lines
21 KiB
Markdown
369 lines
21 KiB
Markdown
# Project format v11 (reads v1–v10)
|
||
|
||
Phases 5–31 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.
|
||
|
||
## 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:
|
||
|
||
```text
|
||
XTC_PROJECT 11
|
||
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
|
||
MIX 3f800000 00000000 0 0
|
||
ROUTE 0 18446744073709551615
|
||
CLIPS 1
|
||
CLIP 0 0 3840 2880 960 3840 526567696f6e
|
||
ASSET -
|
||
MIDI 0
|
||
AUDIO 3f800000 0 0
|
||
NOTES 0 0
|
||
AUTOMATION 0
|
||
CONTROL_MAPS 16
|
||
MAP 0 7 10 64
|
||
MAP 1 7 10 64
|
||
MAP 2 7 10 64
|
||
MAP 3 7 10 64
|
||
MAP 4 7 10 64
|
||
MAP 5 7 10 64
|
||
MAP 6 7 10 64
|
||
MAP 7 7 10 64
|
||
MAP 8 7 10 64
|
||
MAP 9 7 10 64
|
||
MAP 10 7 10 64
|
||
MAP 11 7 10 64
|
||
MAP 12 7 10 64
|
||
MAP 13 7 10 64
|
||
MAP 14 7 10 64
|
||
MAP 15 7 10 64
|
||
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, 5 audio source, 6 instrument, 8 LADSPA plugin), 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. Port directions are implied
|
||
by node type; endpoints, duplicates and cycles are validated by the graph API.
|
||
- `TRACK`: ID and hex-encoded name bytes. In v4/v5 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.
|
||
- `ROUTE`: since v9, required after MIX: bus flag and destination bus ID;
|
||
UINT64_MAX denotes master. See Phase 24 constraints below.
|
||
- `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/v4/v5; "-" 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/v4/v5; 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/v5.
|
||
Follow with `POINT track_id parameter tick value_bits step` in v10 (v4–v9
|
||
omit step and migrate to linear). Step is 0 linear or 1 hold for the next segment.
|
||
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, 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.
|
||
|
||
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.
|
||
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 v11. Unknown versions are rejected without replacing the destination; older
|
||
applications without v11 support cannot read current saves. Migration and maximum-path roundtrips are tested.
|
||
|
||
## 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.
|
||
After rename, the parent directory is fsynced. A directory-sync failure reports
|
||
I/O error even though the new complete destination may already be present.
|
||
Storage and filesystem behavior still determine power-loss durability. 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, 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
|
||
absolute in memory; saves use relative paths for media inside the project folder,
|
||
otherwise absolute paths. Media are 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.
|
||
|
||
## Phase 9 extension and recovery policy
|
||
|
||
Phase 9 kept format v4. Native processor descriptors are process-local and have no
|
||
serialized representation; saving a native graph returns UNSUPPORTED and retains
|
||
the existing destination. Unknown file versions/types remain rejected. See
|
||
NATIVE_API.md for the extension boundary and RECOVERY.md for write-failure, killed
|
||
writer and unfinished-recording behavior. Metadata validation is available via
|
||
`xtc-check --validate PROJECT` without a GUI or server; it does not load assets.
|
||
|
||
## Phase 15: version 5 plugin metadata
|
||
|
||
Phase 15 introduced `XTC_PROJECT 5`; its reader also supported versions 1–4. All v4 records and
|
||
order remain the same. Node type 8 is a LADSPA plugin; native pointer-bearing type
|
||
7 is still unsupported in files. Immediately after each type-8 `NODE id 8
|
||
00000000` record come:
|
||
|
||
```text
|
||
PLUGIN unique_id descriptor_index mono_inputs control_count hex_label
|
||
MODULE hex_absolute_module_path
|
||
CONTROL port_index float32_hex_bits
|
||
```
|
||
|
||
There are exactly `control_count` CONTROL records. Label length is 1–63 bytes,
|
||
module path 1–511 bytes (absolute), both hex-encoded with no embedded NUL. Index
|
||
is <1024, mono_inputs 0/1, controls <=16, control port indices <64 strictly
|
||
increasing, and values finite. Unique ID fits uint32. The one output is implicit.
|
||
Structural validation never opens libraries. On graph compilation the host checks
|
||
identity, layout, input control port IDs and rate-adjusted legal ranges against
|
||
the module. Unknown nodes/versions fail without mutating the current project.
|
||
Missing modules remain preserved by metadata load/save; GUI installation rejects
|
||
them with a repair message. No transient DSP state, pointer or plugin binary is
|
||
stored. See PLUGIN_HOSTING.md and decision 0009 for supported processors/limits.
|
||
|
||
## Phase 16: version 6
|
||
|
||
Phase 16 introduced v6 and its reader accepted v1–v6. After each plugin's CONTROL records,
|
||
`PLUGIN_AUTOMATION count` is followed by exactly count (<=32) records:
|
||
`PARAM tick port mode float32_hex_bits`. Mode is 0 linear or 1 step. Duplicate
|
||
port/tick pairs, nonfinite values, unknown control ports and ticks beyond the
|
||
project limit are rejected. Loaded plugin tempo comes from TEMPO. Range and
|
||
discrete-mode compatibility are checked on control-thread compilation. Versions
|
||
1–5 migrate with empty plugin lanes. See CONTROL.md for timing/ordering.
|
||
|
||
## Phase 18: version 7 and project folders
|
||
|
||
Phase 18 introduced v7; its readers migrated v1–v6. Record ordering and field bounds
|
||
are otherwise unchanged from v6. v7 ASSET may contain a relative path, interpreted
|
||
against the actual containing directory of the named project file (resolved on
|
||
the control thread), never the launch working directory. Opening a symlink to a project uses the
|
||
resolved target project directory; Save replaces the named entry as before. After loading, all core
|
||
asset references are absolute, including missing media. Media inside the save
|
||
folder are written relative; references outside remain absolute. Save As resolves
|
||
against its new folder and preserves media identity rather than rebasing it by
|
||
accident. Copy/move a project folder together with its referenced media to retain
|
||
portability. Saving does not copy assets: import/record into that folder first.
|
||
Plugin binaries remain trusted absolute module references, not bundled resources.
|
||
|
||
Empty assets still use `-`. Paths exclude embedded NUL/control bytes and must fit
|
||
511 bytes after resolution; overly long resolved references fail transactionally.
|
||
Relative references can name outside the folder using ordinary POSIX semantics;
|
||
this is not a sandbox. Symlinks follow filesystem behavior, so portability requires
|
||
moving their targets too. Old formats continue to require absolute media paths.
|
||
No unknown version/node/record is silently discarded; unsupported versions/types
|
||
or malformed/trailing data reject the candidate and retain the current project.
|
||
Parsing never opens media/plugins or constructs an audio engine.
|
||
|
||
Save validates the complete model, writes/fsyncs a same-folder temporary, closes
|
||
it, atomically renames it, then fsyncs the containing directory. Failures before
|
||
rename retain the old destination. A directory sync failure *after* rename reports
|
||
I/O error but the new complete file may already be present: inspect/reopen it;
|
||
there is no rollback across that durability boundary. Filesystem/hardware power
|
||
loss guarantees still depend on the underlying storage. Recording destinations
|
||
have their separate no-clobber policy; ordinary project Save intentionally replaces
|
||
the selected document. In-memory models and the existing destination are preserved
|
||
on load failure. No automatic unknown-field stripping or media deletion occurs.
|
||
|
||
`make test-portability test test-plugins test-recovery test-recording` covers checked-in
|
||
v1–v10 fixtures (including missing v5/v6 plugins), current exact roundtrips, moved-folder
|
||
playback from a different cwd, missing media retention, unknown records, every
|
||
truncation and deterministic byte mutations, malformed plugin metadata, interrupted
|
||
writers and write-limit failures. ASan/UBSan/static analysis additionally audit the
|
||
bounded parser. This is regression/fault-injection coverage, not exhaustive fuzzing
|
||
or a claim of power-cut qualification.
|
||
|
||
## Phase 22: version 8 audio clip settings
|
||
|
||
Phase 22 introduced v8 and reads v1–v7 with unity clip gain and no explicit
|
||
clip fades. After **every** clip's `MIDI` record (including silent and MIDI
|
||
clips), v8 requires:
|
||
|
||
```text
|
||
AUDIO gain_float32_hex_bits fade_in_ticks fade_out_ticks
|
||
```
|
||
|
||
Gain is finite, linear and within 0..4. Fade durations are unsigned decimal,
|
||
0..source_length_tick; zero disables that explicit fade. MIDI clips require
|
||
gain exactly 1 and both fades zero. Invalid or missing AUDIO records reject the
|
||
whole candidate. All v7 media-folder rules and atomic-save guarantees remain.
|
||
Older applications reject v8 rather than silently stripping clip settings.
|
||
Rebuild C/C++ consumers against the extended `xtc_clip_info` public struct;
|
||
this development release does not promise an unchanged struct binary layout.
|
||
|
||
Fades follow the audible trimmed clip boundaries, in current-tempo ticks, with
|
||
sample endpoints defined in AUDIO_FILES.md. Duplicating retains settings and
|
||
source references; splitting keeps the original ID on the left, gives the right
|
||
a fresh ID/source offset, retains outer explicit fades and clears inner ones.
|
||
No waveform cache or source audio is serialized. Checked-in v1–v7 migrations,
|
||
v8 exact roundtrips, malformed new metadata and byte-mutation tests cover the
|
||
format change (`make test test-portability test-audio-editing`).
|
||
|
||
## Phase 24: version 9 mix routing
|
||
|
||
Phase 24 introduced v9. Immediately after each TRACK's MIX record it writes
|
||
`ROUTE is_bus bus_id`. `is_bus` is 0 or 1; `bus_id` is an existing bus track ID,
|
||
or decimal UINT64_MAX (18446744073709551615) for master. Buses must route to
|
||
master and cannot contain clips. Invalid destinations, flags and nested routing
|
||
are rejected. v1–v8 migrate all tracks to ordinary direct-to-master tracks.
|
||
Bus controls and automation use existing MIX and POINT records. Older readers
|
||
reject v9 rather than discard routing. The public `xtc_track_info` grew fields;
|
||
API consumers must rebuild. Fixtures cover v1–v8 migration and tests exercise
|
||
v9 exact roundtrips and malformed routing.
|
||
|
||
## Phase 26: version 10 interpolation and controller bindings
|
||
|
||
Phase 26 introduced v10 with migration from v1–v9. Track/bus automation points are
|
||
`POINT track_id parameter tick value_bits step`, where step is 0 linear or 1
|
||
hold for the segment starting there. v1–v9 points migrate to step=0. LADSPA
|
||
`PARAM` encoding remains unchanged (already has interpolation mode).
|
||
|
||
Immediately after all POINT records, before END, v10 requires `CONTROL_MAPS 16`
|
||
and exactly 16 records in ascending channel order:
|
||
`MAP channel volume_CC pan_CC sustain_CC`. Channels are 0..15. Controllers are
|
||
0..127 or 128 (disabled); enabled numbers must be distinct within a row and may
|
||
not be 120 or 123. Duplicate/missing/out-of-order channels, invalid CCs, reserved
|
||
bindings and invalid interpolation modes reject the complete candidate. v1–v9
|
||
supply default 7/10/64 bindings on every channel. No event history/current live
|
||
controller values are serialized. Older readers reject v10.
|
||
|
||
Fixtures cover v1–v10 loading and migration. Exact roundtrips, malformed metadata, step/linear
|
||
renders and remapped live events cover the new writer/reader. Public clients
|
||
must rebuild and initialize the appended `xtc_automation_point.step` field.
|
||
|
||
## Phase 28 compatibility and diagnostic contract
|
||
|
||
The current reader accepts versions 1–11 and the writer always emits version 11.
|
||
Phase 28 made no format change; Phase 31 adds plugin format identity in v11. Saving an older document upgrades it;
|
||
retain an original backup if an older application must still open it. APP is
|
||
informational; compatibility depends on the format version, not the producer's
|
||
application version. A future incompatible schema requires a new format number.
|
||
Public C/C++ consumers must rebuild when public structs change.
|
||
|
||
| Version | Added representation | Defaults when loading earlier versions |
|
||
| --- | --- | --- |
|
||
| 1 | Graph, tracks, silent regions | Baseline |
|
||
| 2 | External WAV references | Empty asset references |
|
||
| 3 | MIDI clips and notes | Non-MIDI clips, no notes |
|
||
| 4 | Track mix and automation | Unity gain, center pan, mute/solo off, no points |
|
||
| 5 | LADSPA metadata | No plugin nodes |
|
||
| 6 | Plugin automation | Empty plugin lanes |
|
||
| 7 | Relative media references | Earlier media paths must be absolute |
|
||
| 8 | Clip gain and fades | Unity gain, zero explicit fades |
|
||
| 9 | Bus routing | Ordinary tracks routed directly to master |
|
||
| 10 | Track interpolation and controller bindings | Linear points; CC 7/10/64 on every channel |
|
||
|
||
The grammar has no optional extension namespace. Unknown records, extra fields,
|
||
node types or trailing data cannot be preserved by this model and are rejected
|
||
rather than silently removed. Supported older records migrate in a separate
|
||
candidate and pass complete model validation before adoption. Failed parsing,
|
||
reference validation or close retains the existing destination model. GUI Open
|
||
also prepares the candidate before replacing the live document; missing or
|
||
incompatible plugins retain the current document and identify the module.
|
||
Missing media references remain available for relinking and render silently.
|
||
|
||
`xtc_project_load_diagnostic` adds a copied result, parsed format version (zero
|
||
if unavailable), expected record/stage and message. Syntax locations are 1-based;
|
||
line zero denotes I/O, size, path resolution or whole-model validation. MODEL
|
||
errors do not identify the original line of a cross-reference. The original
|
||
`xtc_project_load` remains available with identical transactional semantics.
|
||
|
||
`xtc-check --validate PROJECT` checks bounded metadata without loading dependencies.
|
||
`xtc-check --inspect PROJECT` additionally lists resolved media and module paths,
|
||
plugin labels, unique IDs and descriptor indices. It checks readable regular-file
|
||
availability only. Exit status is 0 when references are available, 2 when valid
|
||
metadata has missing/unreadable references, and 1 for invalid metadata or I/O.
|
||
Neither command executes plugin code, decodes WAV data, connects JACK or verifies
|
||
hardware. A readable file can still be an invalid WAV or incompatible plugin;
|
||
actual preparation performs those checks. Paths and labels are escaped for
|
||
terminal output. There is no search-path substitution or automatic plugin relink.
|
||
|
||
Portability currently targets Linux/POSIX. The text representation is independent
|
||
of byte order and locale and requires IEEE binary32 floats. IDs used by the graph
|
||
must fit the receiving host's size_t; a 64-bit file is not guaranteed to fit a
|
||
32-bit build. Paths follow POSIX semantics; Windows drive letters and separators
|
||
are not translated. Relative media can travel with their project folder, while
|
||
absolute references and symlink targets must be restored separately. LADSPA
|
||
modules are absolute installed references: match architecture, ABI, descriptor
|
||
identity, ports and legal controls on the destination. Plugin binaries and state
|
||
are not bundled. JACK availability, sample rate, block limits and device routing
|
||
are runtime dependencies; loading metadata does not restore device connections.
|
||
|
||
`make test` includes v1–v10 migration, malformed/truncated/mutated/oversized input,
|
||
retained-state diagnostics, CLI dependency inspection, and deterministic save
|
||
faults at flush, file sync, close, rename and directory sync. Pre-rename faults
|
||
preserve the old bytes and remove the temporary. After rename, a sync failure
|
||
reports I/O while leaving a complete new document; inspect it before retrying.
|
||
Fault wrappers are linked into tests only. These tests complement killed-writer
|
||
coverage in `make test-recovery`; they do not qualify power-cut behavior.
|
||
|
||
## Phase 31: version 11 plugin format identity
|
||
|
||
Immediately after every plugin MODULE record, v11 requires:
|
||
|
||
```text
|
||
FORMAT format_number hex_identity_or_dash
|
||
```
|
||
|
||
0 = LADSPA, 1 = CLAP, 2 = VST2, 3 = VST3. Identity is `-` for LADSPA/VST2;
|
||
CLAP uses its stable plugin ID (1–127 non-NUL bytes); VST3 uses the 32-character lowercase
|
||
hex class ID string, itself hex-encoded like other byte strings. PLUGIN's unique
|
||
ID remains the VST2/LADSPA integer ID (zero for CLAP/VST3), descriptor index and
|
||
label. MODULE is still the actual absolute binary path. Versions 1–10 migrate
|
||
to format 0 and empty extra identity. Older readers reject v11. The public plugin
|
||
struct has grown; C/C++ consumers must rebuild and initialize it to zero.
|
||
|
||
Modern adapters accept one-input mono effects, default configuration only, with
|
||
zero controls and automation points. Unsupported modern control metadata is
|
||
rejected, not discarded. State/editor/preset changes are not exposed, so no opaque
|
||
state is serialized. Reloading recreates plugin defaults, which can change after
|
||
a plugin update or with its external configuration. Matching ID/name/layout is
|
||
not a binary hash or sound-reproducibility guarantee. See PLUGIN_HOSTING.md.
|
||
|
||
Metadata validation and inspection never initialize modules. Actual graph
|
||
preparation verifies format, descriptor, stable identity, name and layout before
|
||
publishing. Missing module metadata survives C load/save; GUI preparation retains
|
||
the existing document on failure. No automatic path search or plugin substitution
|
||
occurs. Current and historical migration fixtures, malformed fields, identity
|
||
mismatch, missing-module and exact-render roundtrips cover this extension.
|