Docs file normalization.

This commit is contained in:
Johannes Findeisen 2026-11-06 15:20:20 +01:00
commit 619c2d0808
46 changed files with 3587 additions and 2579 deletions

View file

@ -1,15 +1,17 @@
# Project format v17 (reads v1–v17)
# Project Format v17 (Reads v1–v17)
The project 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/audio arrangement regions. Audio samples remain in external WAV
files; MIDI notes are stored inline.
The project 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/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:
Fields use ASCII spaces. Integers are unsigned decimal, without signs. Counts
determine the number of subsequent records:
```text
XTC_PROJECT 11
@ -55,37 +57,38 @@ END
- `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
- `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.
- `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 the mix routing 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.
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.
`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.
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.
@ -94,20 +97,24 @@ END
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.
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 v15. Unknown versions are rejected without replacing the destination; older
applications without v12 support cannot read current saves. Migration and maximum-path roundtrips are tested.
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 v15. Unknown versions are rejected without replacing the destination;
older applications without v12 support cannot read current saves. Migration and
maximum-path roundtrips are tested.
## Ownership and failures
---
## 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
@ -116,35 +123,42 @@ 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.
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.
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.
## Extension and recovery policy
---
Format v4 was retained. 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.
## Extension and Recovery Policy
## Version 5: plugin metadata
Format v4 was retained. 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.
`XTC_PROJECT 5` was introduced; 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:
---
## Version 5: Plugin Metadata
`XTC_PROJECT 5` was introduced; 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
@ -155,130 +169,153 @@ 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.
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.
---
## Version 6
Version 6 was introduced 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.
Version 6 was introduced 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.
## Version 7 and project folders
---
Version 7 was introduced; 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
## Version 7 and Project Folders
Version 7 was introduced; 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.
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.
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.
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.
`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.
## Version 8: audio clip settings
---
## Version 8: Audio Clip Settings
Version 8 adds audio clip gain/fade settings 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:
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.
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
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`).
## Version 9: mix routing
---
Version 9 adds `ROUTE is_bus bus_id` immediately after each TRACK's MIX record. `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.
## Version 9: Mix Routing
## Version 10: interpolation and controller bindings
Version 9 adds `ROUTE is_bus bus_id` immediately after each TRACK's MIX record.
`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.
Version 10 adds interpolation modes and 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).
---
## Version 10: Interpolation and Controller Bindings
Version 10 adds interpolation modes and 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.
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.
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.
## Compatibility and diagnostic contract
---
## Compatibility and Diagnostic Contract
The current reader accepts versions 1–15 and the writer always emits version 15.
Compatibility diagnostics made no format change; plugin format identity is added 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.
Compatibility diagnostics made no format change; plugin format identity is added
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 |
| --- | --- | --- |
@ -303,41 +340,47 @@ 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.
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.
`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.
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.
`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.
## Version 11: plugin format identity
---
## Version 11: Plugin Format Identity
Immediately after every plugin MODULE record, v11 requires:
@ -345,29 +388,34 @@ Immediately after every plugin MODULE record, v11 requires:
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.
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.
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.
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.
## Version 12: track instruments
---
## Version 12: Track Instruments
After CONTROL_MAPS/MAP records and before END:
@ -389,25 +437,32 @@ modules; preparing playback verifies binary identity and configuration. Deleting
tracks removes bindings; reordering preserves stable track identities. Undo
snapshots include bindings. Public clients must rebuild for the new spec field.
## Version 13 internal devices
---
## Version 13 Internal Devices
Format value 4 identifies dependency-free internal devices at `/xtc/internal`.
Indices 0/1 bind fixed Soft Keys/Sub Bass instruments; 2/3 are Delay/Reverb
effects with three controls and existing automation records. Identity and control
ranges are validated without loading libraries. See [BUILTIN_DEVICES.md](BUILTIN_DEVICES.md).
effects with three controls and existing automation records. Identity and
control ranges are validated without loading libraries. See
[BUILTIN_DEVICES.md](BUILTIN_DEVICES.md).
## Version 14 instrument parameters
---
## Version 14 Instrument Parameters
Each track INSTRUMENT identity is followed by `INSTRUMENT_CONTROLS count`,
`CONTROL port value_bits` records, `INSTRUMENT_AUTOMATION count` and
`PARAM tick port mode value_bits` records. Bounds, duplicate points and metadata
are validated before destination replacement, just as for graph effects.
Earlier bindings have no controls/automation; v13 internal instruments retain
fixed default sounds. Modern external instruments still require zero controls.
are validated before destination replacement, just as for graph effects. Earlier
bindings have no controls/automation; v13 internal instruments retain fixed
default sounds. Modern external instruments still require zero controls.
Instrument tempo follows project tempo. Instrument preset files use this same
schema with exactly one instrument track and no clips; they are metadata only.
## Version 15 sample references
---
## Version 15 Sample References
Each instrument parameter block ends with `SAMPLE hex_path_or_dash`. Only the
internal Sample Player (index 4) may carry a nonempty sample path. Paths within
@ -415,19 +470,23 @@ the project directory are relative on disk and resolve against the project
file's directory; other paths are absolute. Metadata loading does not read WAVs.
Missing/invalid samples remain references and become reported silence during
preparation. Controls describe root key, whole-file loop, attack, release and
gain. Earlier projects have empty sample paths. Rebuild public API consumers
for the additional copied `sample_path` field in `xtc_plugin_spec`.
gain. Earlier projects have empty sample paths. Rebuild public API consumers for
the additional copied `sample_path` field in `xtc_plugin_spec`.
## Version 16: arrangement markers
---
## Version 16: Arrangement Markers
After `LIVE_INSTRUMENT` and before `END`, version 16 writes `MARKERS count`
followed by `MARKER tick hex_name` records. Count is 0..128; ticks must be strictly
increasing and at most MAX_TICK. Names use the existing bounded UTF-8 hex encoding.
Versions 1–15 migrate to an empty marker list. Older builds reject version 16.
Markers are copied by snapshots and history; the tick is their stable key.
followed by `MARKER tick hex_name` records. Count is 0..128; ticks must be
strictly increasing and at most MAX_TICK. Names use the existing bounded UTF-8
hex encoding. Versions 1–15 migrate to an empty marker list. Older builds reject
version 16. Markers are copied by snapshots and history; the tick is their
stable key.
---
## Version 17: per-track insert chains
## Version 17: per-Track Insert Chains
After MARKERS/MARKER and before END, the writer emits a chain for every track,
in dense track order. Earlier versions migrate to empty chains with unity gain.
@ -448,16 +507,16 @@ PARAM tick port step value_bits
Each CHAIN has exactly slot_count INSERT blocks. CONTROLS/PARAMETERS determine
the subsequent CONTROL/PARAM record counts, including zero. Slots are ordered,
at most eight, and accept mono non-instrument effects only. Enabled is 0/1,
wet is finite 0–1, output gain finite 0–4; inputs must be 1. The format values
are LADSPA=0, CLAP=1, VST2=2, VST3=3 and Internal=4. Existing plugin specification
at most eight, and accept mono non-instrument effects only. Enabled is 0/1, wet
is finite 0–1, output gain finite 0–4; inputs must be 1. The format values are
LADSPA=0, CLAP=1, VST2=2, VST3=3 and Internal=4. Existing plugin specification
validation applies, including descriptor/identity/port/automation bounds and
20,000–300,000 milli-BPM. Bus chains must be empty. No sample reference is
permitted on an effect. Duplicate, missing, reordered or excessive chain
records, malformed strings and nonfinite values reject the whole load.
Loading and dependency inspection only validate metadata; no plugin module
is opened. Enabled plugins are verified when a render plan is prepared. Chain
Loading and dependency inspection only validate metadata; no plugin module is
opened. Enabled plugins are verified when a render plan is prepared. Chain
preset files reuse this schema with exactly one non-bus track and no clips or
instrument. Track insert APIs are declared in `xtc_plugin.h`; see
[TRACK_INSERTS.md](TRACK_INSERTS.md) for playback restrictions and workflows.