Phase 32: add MIDI track CLAP, VST2 and VST3 instruments

This commit is contained in:
Johannes Findeisen 2026-10-27 17:51:48 +01:00
commit fe484537f9
41 changed files with 1138 additions and 65 deletions

View file

@ -490,3 +490,24 @@ test-gui-modern-plugins:
@echo "FLTK development files are required for modern plugin GUI tests."
@exit 1
endif
$(BUILD_DIR)/clap-synth-fixture.clap: tests/plugins/clap_synth_fixture.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -isystem third_party/clap/include $(LDFLAGS) -fPIC -shared -o $@ $<
$(BUILD_DIR)/vst2-synth-fixture.so: tests/plugins/vst2_synth_fixture.c src/plugin/vst2_abi.h
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -fPIC -shared -o $@ $<
$(BUILD_DIR)/vst3-synth-fixture.so: tests/plugins/vst3_synth_fixture.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -isystem third_party/vst3 $(LDFLAGS) -fPIC -shared -o $@ $<
$(BUILD_DIR)/test_instruments: $(BUILD_DIR)/tests/integration/test_instruments.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=dlopen,--wrap=dlclose
.PHONY: test-instruments
test-instruments: $(BUILD_DIR)/test_instruments $(BUILD_DIR)/clap-synth-fixture.clap $(BUILD_DIR)/vst2-synth-fixture.so $(BUILD_DIR)/vst3-synth-fixture.so
./$(BUILD_DIR)/test_instruments "$(abspath $(BUILD_DIR))/clap-synth-fixture.clap" "$(abspath $(BUILD_DIR))/vst2-synth-fixture.so" "$(abspath $(BUILD_DIR))/vst3-synth-fixture.so"
test: test-instruments
-include $(BUILD_DIR)/tests/integration/test_instruments.d
.PHONY: test-gui-instruments
test-gui-instruments: $(BUILD_DIR)/test_gui $(BUILD_DIR)/clap-synth-fixture.clap $(BUILD_DIR)/vst2-synth-fixture.so $(BUILD_DIR)/vst3-synth-fixture.so
XTC_TEST_SYNTH_CLAP="$(abspath $(BUILD_DIR))/clap-synth-fixture.clap" XTC_TEST_SYNTH_VST2="$(abspath $(BUILD_DIR))/vst2-synth-fixture.so" XTC_TEST_SYNTH_VST3="$(abspath $(BUILD_DIR))/vst3-synth-fixture.so" xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui

View file

@ -22,7 +22,7 @@
plus baseline and at most one reserved metadata snapshot; GUI edits reserve
history before mutation/publication. Media is
never removed by undo/delete. Broad screen-reader/accessibility testing is pending.
- Format v11 reads v1–v10 and uses relative local media references. External absolute
- Format v12 reads v1–v11 and uses relative local media references. External absolute
media/module paths need manual repair after relocation. Saving does not bundle
media; missing media stays referenced, missing plugins reject GUI installation.
Unknown data is rejected rather than stripped. Post-rename directory-sync errors
@ -81,3 +81,34 @@ workers, sidechains or true stereo/multichannel buses are implemented. Plugin
bundles must be opened at their actual Linux shared-library binary. Only original
ABI fixtures are qualified; arbitrary third-party modern plugins remain untested.
See PLUGIN_HOSTING.md and decision 0010 for lifecycle and licensing boundaries.
## Phase 32 instrument support
MIDI tracks can bind CLAP, VST2 and VST3 instruments. In the Track menu, select
**Load instrument plugin** for the selected sequencer track; **Use built-in
instrument** removes the binding. Assignment also selects the track for live
MIDI. **Live MIDI to selected instrument** changes that target; **Live MIDI to
built-in sine** restores the legacy live source. MIDI clip notes always go to
their own track's instrument. Independent instances feed track gain/pan,
mute/solo, bus routing and automation. Bindings/live target save in v12.
Supply the actual trusted Linux binary (including the inner .so of a VST3
bundle). Supported instruments have zero audio inputs, one mono output,
float32 processing, zero latency and one note/event input. Default configuration
only: no custom editor, opaque state/preset restoration, modern parameter
editing/automation, stereo/multichannel instruments or MIDI output. No arbitrary
third-party instrument is qualified by the original ABI fixture tests.
CLAP accepts MIDI or native notes; VST2 uses MIDI events; VST3 uses IEventList
note events. Live CC reaches CLAP MIDI/VST2; native CLAP/VST3 have no controller
mapping (including sustain). Live MIDI goes to exactly one selected target.
Seeks/loops retrigger notes crossing the destination with fresh envelopes;
plugin oscillator/opaque DSP state cannot be reconstructed. Panic delivers
all-off (VST3 held-key note-offs); plugin release tails may remain. Processing
uses preallocated 256-frame chunks and 256 incoming events per instance/chunk;
overflow or processing failure silences the block and schedules reset events.
The existing 32 simultaneous scheduled-note and 4096 stored-note bounds remain.
Do not unload/replace modules during playback; candidate plans are prepared off
the callback and old instances are reclaimed on the control thread. Missing
references remain inspectable without loading; failed GUI preparation retains
the current project. Save a backup for applications limited to v11 or earlier.

View file

@ -136,3 +136,34 @@ Phase 26: CC7/10/64 are defaults, configurable per channel through Audio → MID
controller mapping. Disabled/unmapped CCs have no effect; CC120/123 stay panic.
Decode accepts valid CC0..127 messages within the existing event budget. See
CONTROL.md for the exact bindings, ramp, sustain and automation precedence rules.
## Phase 32 instrument support
MIDI tracks can bind CLAP, VST2 and VST3 instruments. In the Track menu, select
**Load instrument plugin** for the selected sequencer track; **Use built-in
instrument** removes the binding. Assignment also selects the track for live
MIDI. **Live MIDI to selected instrument** changes that target; **Live MIDI to
built-in sine** restores the legacy live source. MIDI clip notes always go to
their own track's instrument. Independent instances feed track gain/pan,
mute/solo, bus routing and automation. Bindings/live target save in v12.
Supply the actual trusted Linux binary (including the inner .so of a VST3
bundle). Supported instruments have zero audio inputs, one mono output,
float32 processing, zero latency and one note/event input. Default configuration
only: no custom editor, opaque state/preset restoration, modern parameter
editing/automation, stereo/multichannel instruments or MIDI output. No arbitrary
third-party instrument is qualified by the original ABI fixture tests.
CLAP accepts MIDI or native notes; VST2 uses MIDI events; VST3 uses IEventList
note events. Live CC reaches CLAP MIDI/VST2; native CLAP/VST3 have no controller
mapping (including sustain). Live MIDI goes to exactly one selected target.
Seeks/loops retrigger notes crossing the destination with fresh envelopes;
plugin oscillator/opaque DSP state cannot be reconstructed. Panic delivers
all-off (VST3 held-key note-offs); plugin release tails may remain. Processing
uses preallocated 256-frame chunks and 256 incoming events per instance/chunk;
overflow or processing failure silences the block and schedules reset events.
The existing 32 simultaneous scheduled-note and 4096 stored-note bounds remain.
Do not unload/replace modules during playback; candidate plans are prepared off
the callback and old instances are reclaimed on the control thread. Missing
references remain inspectable without loading; failed GUI preparation retains
the current project. Save a backup for applications limited to v11 or earlier.

View file

@ -19,7 +19,7 @@ plugin recovery policy, preparation failure/partial cleanup tests, bounded event
and parameter publication, latency/tail handling, callback-safe unload after
retirement, a GUI/main-thread boundary and an explicit crash-containment decision.
Native ABI tests cover some ownership mechanics but do not prove third-party
code safe. Event graph ports and custom-instrument GUI selection remain gaps.
code safe. Event graph ports remain a gap; Phase 32 adds track instrument selection.
Choose one format with concrete user needs after those contracts are specified;
do not introduce a general multi-format framework now. See decision 0008.
@ -113,8 +113,7 @@ Physical listening and plugin CPU/endurance qualification remain pending.
Discovery remains explicit-file only, bounded to 1024 descriptor indices; no
recursive scan, plugin database or new dependency is introduced. Unsupported
layouts are skipped with counts; source/effect roles describe audio ports, not
MIDI instrument capabilities. Built-in MIDI synthesis remains the only GUI MIDI
instrument. LADSPA sources can be identified, edited, replaced and removed in
MIDI instrument capabilities. Phase 32 adds modern track instruments (see below). LADSPA sources can be identified, edited, replaced and removed in
the graph but cannot receive MIDI events.
**Edit → Insert LADSPA on selected wire** replaces one existing connection with
@ -224,7 +223,7 @@ prefers CLAP, then VST2, then VST3, otherwise LADSPA; the C API also provides
The modern subset is **one mono audio input and one mono output, float32, default
configuration, zero reported latency**. Stereo graph playback uses independent
instances. True stereo/multichannel/sidechain buses, instruments, MIDI/events,
instances. For the Phase 31 effect subset, true stereo/multichannel/sidechain buses, instruments, MIDI/events,
custom editors, parameter changes/automation, programs/presets, opaque state,
transport/time information, workers and latency compensation are not implemented
for these three formats. Modern metadata with controls/automation is rejected,
@ -257,3 +256,34 @@ CLAP start/process failures, graph replacement rollback, 50 plan retirements per
format, missing modules, v11 save/reopen and GUI add/browser/undo/reopen. Host
allocation/loading/unloading guards remain active while rendering. This evidence
qualifies the subset above, not arbitrary external plugins or physical audio.
## Phase 32 instrument support
MIDI tracks can bind CLAP, VST2 and VST3 instruments. In the Track menu, select
**Load instrument plugin** for the selected sequencer track; **Use built-in
instrument** removes the binding. Assignment also selects the track for live
MIDI. **Live MIDI to selected instrument** changes that target; **Live MIDI to
built-in sine** restores the legacy live source. MIDI clip notes always go to
their own track's instrument. Independent instances feed track gain/pan,
mute/solo, bus routing and automation. Bindings/live target save in v12.
Supply the actual trusted Linux binary (including the inner .so of a VST3
bundle). Supported instruments have zero audio inputs, one mono output,
float32 processing, zero latency and one note/event input. Default configuration
only: no custom editor, opaque state/preset restoration, modern parameter
editing/automation, stereo/multichannel instruments or MIDI output. No arbitrary
third-party instrument is qualified by the original ABI fixture tests.
CLAP accepts MIDI or native notes; VST2 uses MIDI events; VST3 uses IEventList
note events. Live CC reaches CLAP MIDI/VST2; native CLAP/VST3 have no controller
mapping (including sustain). Live MIDI goes to exactly one selected target.
Seeks/loops retrigger notes crossing the destination with fresh envelopes;
plugin oscillator/opaque DSP state cannot be reconstructed. Panic delivers
all-off (VST3 held-key note-offs); plugin release tails may remain. Processing
uses preallocated 256-frame chunks and 256 incoming events per instance/chunk;
overflow or processing failure silences the block and schedules reset events.
The existing 32 simultaneous scheduled-note and 4096 stored-note bounds remain.
Do not unload/replace modules during playback; candidate plans are prepared off
the callback and old instances are reclaimed on the control thread. Missing
references remain inspectable without loading; failed GUI preparation retains
the current project. Save a backup for applications limited to v11 or earlier.

View file

@ -1,4 +1,4 @@
# Project format v11 (reads v1–v10)
# Project format v12 (reads v1–v11)
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.
@ -104,8 +104,8 @@ 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.
Saving always writes v12. 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
@ -274,7 +274,7 @@ 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.
The current reader accepts versions 1–12 and the writer always emits version 12.
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
@ -367,3 +367,25 @@ publishing. Missing module metadata survives C load/save; GUI preparation retain
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.
## Phase 32: version 12 track instruments
After CONTROL_MAPS/MAP records and before END:
```
TRACK_INSTRUMENTS count
INSTRUMENT track_id format descriptor_index unique_id hex_label
MODULE hex_absolute_binary_path
IDENTITY hex_stable_id_or_dash
LIVE_INSTRUMENT track_id_or_18446744073709551615
END
```
Each binding comprises INSTRUMENT/MODULE/IDENTITY. Formats 1/2/3 mean
CLAP/VST2/VST3. Bindings require distinct existing non-bus tracks and compatible
instrument metadata (zero audio inputs). The live target must have a binding;
UINT64_MAX selects built-in sine. Empty bindings are valid. Older documents
migrate to no bindings and built-in live sine. Parsing/inspection never executes
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.

View file

@ -1,6 +1,6 @@
# Persistence and failure recovery
The project format is v11. Readers migrate v1–v10, validate bounded input
The project format is v12. Readers migrate v1–v11, validate bounded input
and atomically install a candidate only after complete success. Existing tests
cover exact migration/roundtrip, truncations, malformed references, counts,
IDs, nonfinite values and failed GUI/backend installation. Native processors
@ -87,7 +87,7 @@ rename and directory-sync errors. Pre-rename failures retain the previous bytes
and clean temporary files. Post-rename sync errors leave a complete new file and
return failure; there is no rollback after replacement. Load diagnostics identify
the expected record and syntax line, or line zero for whole-model/I/O stages.
Unknown versions report the supported range (1–11); unknown records/fields are
Unknown versions report the supported range (1–12); unknown records/fields are
rejected without partially changing the current project.
Modern CLAP/VST2/VST3 module identity is preserved in v11 metadata even when the
@ -96,3 +96,34 @@ CLAP restart and VST2 I/O-change requests fail/silence subsequent blocks; stop
and rebuild/reload rather than attempting callback-time reconfiguration. Modern
instances restart at default configuration; no edited opaque state is exposed
or saved by the initial adapters. Trusted plugin crashes cannot be contained.
## Phase 32 instrument support
MIDI tracks can bind CLAP, VST2 and VST3 instruments. In the Track menu, select
**Load instrument plugin** for the selected sequencer track; **Use built-in
instrument** removes the binding. Assignment also selects the track for live
MIDI. **Live MIDI to selected instrument** changes that target; **Live MIDI to
built-in sine** restores the legacy live source. MIDI clip notes always go to
their own track's instrument. Independent instances feed track gain/pan,
mute/solo, bus routing and automation. Bindings/live target save in v12.
Supply the actual trusted Linux binary (including the inner .so of a VST3
bundle). Supported instruments have zero audio inputs, one mono output,
float32 processing, zero latency and one note/event input. Default configuration
only: no custom editor, opaque state/preset restoration, modern parameter
editing/automation, stereo/multichannel instruments or MIDI output. No arbitrary
third-party instrument is qualified by the original ABI fixture tests.
CLAP accepts MIDI or native notes; VST2 uses MIDI events; VST3 uses IEventList
note events. Live CC reaches CLAP MIDI/VST2; native CLAP/VST3 have no controller
mapping (including sustain). Live MIDI goes to exactly one selected target.
Seeks/loops retrigger notes crossing the destination with fresh envelopes;
plugin oscillator/opaque DSP state cannot be reconstructed. Panic delivers
all-off (VST3 held-key note-offs); plugin release tails may remain. Processing
uses preallocated 256-frame chunks and 256 incoming events per instance/chunk;
overflow or processing failure silences the block and schedules reset events.
The existing 32 simultaneous scheduled-note and 4096 stored-note bounds remain.
Do not unload/replace modules during playback; candidate plans are prepared off
the callback and old instances are reclaimed on the control thread. Missing
references remain inspectable without loading; failed GUI preparation retains
the current project. Save a backup for applications limited to v11 or earlier.

View file

@ -15,7 +15,7 @@ undo/redo and a generic LADSPA editor. JACK is the only live audio/MIDI backend;
core validation and offline rendering work without it. Native/LADSPA processors
are trusted in-process code. No proprietary energyXT resources are included.
Project format v11 reads versions 1–11 transactionally. Syntax diagnostics identify
Project format v12 reads versions 1–12 transactionally. Syntax diagnostics identify
records and lines. `xtc-check --inspect` lists dependency paths without executing
plugins. Phase 29 adds timing/CPU/RSS reports, repeatable stress runs and virtual
endurance. Phase 30 adds configurable install directories, shared install/remove
@ -24,7 +24,7 @@ inventory, installed MIDI examples and source archive checksum sidecars.
## Upgrading and removal
Back up documents and external media before upgrading. Opening an older project
does not rewrite it; saving upgrades it to v11, which older readers may reject.
does not rewrite it; saving upgrades it to v12, which older readers may reject.
Use Save As for a separate migrated copy. Moving a project folder preserves media
stored inside it via relative references. Absolute media and plugin module paths
must be restored separately. There is no bundled plugin/media migration or saved
@ -57,3 +57,7 @@ GUI discovery/insertion/replacement and v11 format identity. Pinned MIT CLAP and
BSD-3-Clause VST3 C headers ship with notices; VST2 uses original compatibility
declarations. Modern instruments, parameters/state/editors and multichannel
layouts remain unsupported. No third-party modern plugin has been qualified.
Phase 32 adds CLAP/VST2/VST3 mono instruments bound to MIDI tracks, sample-offset
notes, a saved live target, track mixing, GUI assignment/undo and v12 persistence.
See PLUGIN_HOSTING.md for compatibility and controller/state limits.

View file

@ -901,6 +901,36 @@ These require separate future work; they are not claimed as implemented by loadi
---
## Phase 32 — MIDI Track Plugin Instruments
### Goal
Play CLAP, VST2 and VST3 synthesizer/instrument plugins from MIDI clips and live MIDI.
### Tasks and acceptance criteria
- [x] Discover and initialize compatible zero-input, mono float32, zero-latency instruments.
- [x] Bind independent instances to tracks; preserve built-in sine for unassigned tracks.
- [x] Deliver sample-offset notes and bounded event queues without callback allocations.
- [x] Apply track/bus mixing and provide an explicit saved live-MIDI target.
- [x] Handle discontinuity note chase, panic, teardown and failed preparation.
- [x] Save bindings in v12; migrate v1–v11 and retain missing module references.
- [x] Add GUI load/clear/live-target commands with undo and candidate preparation.
- [x] Test original fixtures for all three formats and document the supported subset.
### Implementation status
Implemented with track-owned prepared instances and 256-frame processing chunks.
Only original ABI fixtures are qualified; arbitrary third-party instruments and
physical MIDI/audio hardware remain unverified. Mono/default configuration only;
no custom editors, opaque state or modern parameter automation. Native-note CLAP
and VST3 carry notes only; CLAP MIDI and VST2 also receive live controllers.
Seeking retriggers held scheduled notes with fresh envelopes; plugin release tails
can remain after note-off/panic. See PLUGIN_HOSTING.md and decision 0011.
Validation: normal/minimal core and GUI builds, core/instrument/GUI fixtures,
JACK/workflow, reliability, ASan/UBSan core and GUI, static analysis, staged
installation, reproducible source releases and relocatable binary packages pass.
---
## Recommended Execution Order
Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification.

View file

@ -166,5 +166,36 @@ CLAP/VST2/VST3 effect subset. Select the actual module binary (`.clap` or `.so`,
including a VST3 bundle's inner binary). Modern effects require one mono input
and output, float32 and zero latency; they use default configuration. Instruments,
modern parameter editing/automation, opaque state and custom editors are not
implemented. Projects now save v11 and read v1–v11. See PLUGIN_HOSTING.md for the
implemented. Projects now save v12 and read v1–v12. See PLUGIN_HOSTING.md for the
full contract and test/qualification limits.
## Phase 32 instrument support
MIDI tracks can bind CLAP, VST2 and VST3 instruments. In the Track menu, select
**Load instrument plugin** for the selected sequencer track; **Use built-in
instrument** removes the binding. Assignment also selects the track for live
MIDI. **Live MIDI to selected instrument** changes that target; **Live MIDI to
built-in sine** restores the legacy live source. MIDI clip notes always go to
their own track's instrument. Independent instances feed track gain/pan,
mute/solo, bus routing and automation. Bindings/live target save in v12.
Supply the actual trusted Linux binary (including the inner .so of a VST3
bundle). Supported instruments have zero audio inputs, one mono output,
float32 processing, zero latency and one note/event input. Default configuration
only: no custom editor, opaque state/preset restoration, modern parameter
editing/automation, stereo/multichannel instruments or MIDI output. No arbitrary
third-party instrument is qualified by the original ABI fixture tests.
CLAP accepts MIDI or native notes; VST2 uses MIDI events; VST3 uses IEventList
note events. Live CC reaches CLAP MIDI/VST2; native CLAP/VST3 have no controller
mapping (including sustain). Live MIDI goes to exactly one selected target.
Seeks/loops retrigger notes crossing the destination with fresh envelopes;
plugin oscillator/opaque DSP state cannot be reconstructed. Panic delivers
all-off (VST3 held-key note-offs); plugin release tails may remain. Processing
uses preallocated 256-frame chunks and 256 incoming events per instance/chunk;
overflow or processing failure silences the block and schedules reset events.
The existing 32 simultaneous scheduled-note and 4096 stored-note bounds remain.
Do not unload/replace modules during playback; candidate plans are prepared off
the callback and old instances are reclaimed on the control thread. Missing
references remain inspectable without loading; failed GUI preparation retains
the current project. Save a backup for applications limited to v11 or earlier.

View file

@ -0,0 +1,27 @@
# 0011 — Prepared MIDI track instruments
Accepted for Phase 32. Extend the modern C99 adapters with zero-audio-input,
mono-output instruments and one event input. Keep graph effects separate from
track bindings. Each assigned track owns one prepared instance; the existing
instrument source sums their mixed output and the sine output of unassigned
tracks. A stable saved live target prevents accidental broadcast/double playback.
Bindings are copied metadata in the project/history model, serialized in v12.
Loading metadata never executes modules. GUI edits prepare a candidate before
publication, preserving the previous project on missing/invalid binaries. Existing
quiescent plan retirement owns destruction; no module operation belongs in audio.
Preallocate 256-frame audio scratch and 256 incoming events per chunk. Stable
insertion orders reset, scheduled off/on and live events without callback sort or
allocation. VST3 translates into a bounded 2304-event IEventList (2048 possible
held keys plus 256 incoming events); panic expands held keys to note-offs. CLAP
prefers MIDI where supported, otherwise native note events; VST2 uses MIDI ABI
messages. Native CLAP and VST3 currently omit controller mapping. Existing
32 simultaneous scheduled notes and 4096 stored notes remain validation bounds.
Discontinuities send all-off and chase spanning notes with fresh note-ons.
Panic suppresses already-started scheduled notes. Neither operation promises
opaque DSP reconstruction or removal of release tails. Overflow/process errors
silence the block and arm reset for the following render. Hosts cannot guarantee
that third-party processing is realtime safe. Qualification covers original ABI
fixtures; arbitrary third-party plugins, physical audio and MIDI remain unverified.

View file

@ -21,6 +21,7 @@ typedef struct {
xtc_plugin_format format;
char identifier[128]; /* CLAP stable ID / VST3 class hex; empty for LADSPA/VST2. */
unsigned int index, unique_id, inputs, control_count;
unsigned int instrument; /* Modern zero-input note instrument; track binding only. */
unsigned int ports[XTC_PLUGIN_CONTROLS];
float values[XTC_PLUGIN_CONTROLS];
unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */
@ -69,6 +70,14 @@ void xtc_plugin_destroy(xtc_plugin *plugin); /* control, after quiescence */
/* Single audio owner. No loading/allocation/I/O in host process; trusted run()
* must obey realtime rules. No MIDI; max one mono input and one mono output.
* Transport reset retains LADSPA delay history (no realtime reset contract). */
/* Instruments: sorted bounded MIDI, mono output, single audio owner. */
xtc_result xtc_plugin_process_midi(xtc_plugin *plugin, const xtc_midi_event *events, size_t count, float *output, size_t frames);
/* Metadata-only track assignment. NULL restores built-in sine. Assignment ensures
* an instrument-source graph route transactionally. Missing modules stay saved. */
xtc_result xtc_project_set_instrument(xtc_project *project, uint64_t track, const xtc_plugin_spec *spec);
xtc_result xtc_project_get_instrument(const xtc_project *project, uint64_t track, xtc_plugin_spec *out);
xtc_result xtc_project_set_live_instrument(xtc_project *project, uint64_t track);
xtc_result xtc_project_get_live_instrument(const xtc_project *project, uint64_t *track);
xtc_result xtc_plugin_process_at(xtc_plugin *plugin, const float *input, float *output, size_t frames, uint64_t position);
xtc_result xtc_plugin_process(xtc_plugin *plugin, const float *input, float *output, size_t frames);
xtc_result xtc_graph_add_plugin(xtc_graph *graph, const xtc_plugin_spec *spec, size_t *id);

View file

@ -7,7 +7,7 @@
extern "C" {
#endif
#define XTC_PROJECT_FORMAT_VERSION 11U
#define XTC_PROJECT_FORMAT_VERSION 12U
#define XTC_PROJECT_MAX_TRACKS 64U
#define XTC_PROJECT_MAX_CLIPS 1024U
#define XTC_PROJECT_NAME_BYTES 64U

View file

@ -568,6 +568,7 @@ xtc_result xtc_graph_add_plugin(xtc_graph *g, const xtc_plugin_spec *s, size_t *
{
xtc_result r = xtc_plugin_spec_validate(s);
if (r != XTC_OK) return r;
if (s->instrument) return XTC_ERR_UNSUPPORTED;
r = add_node(g,XTC_NODE_PLUGIN,0,id);
if (r == XTC_OK) g->nodes[g->node_count-1].plugin = *s;
return r;
@ -589,6 +590,7 @@ xtc_result xtc_graph_set_plugin(xtc_graph *g, size_t id, const xtc_plugin_spec *
i = node_index(g,id);
if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
if (g->nodes[i].type != XTC_NODE_PLUGIN) return XTC_ERR_INVALID_ARGUMENT;
if (s->instrument) return XTC_ERR_UNSUPPORTED;
/* Parameter edits cannot replace identity/layout or invalidate wiring. */
if (s->format != g->nodes[i].plugin.format || strcmp(s->identifier,g->nodes[i].plugin.identifier) ||
strcmp(s->path,g->nodes[i].plugin.path) || strcmp(s->label,g->nodes[i].plugin.label) ||
@ -605,6 +607,7 @@ xtc_result xtc_graph_replace_plugin(xtc_graph *g, size_t id, const xtc_plugin_sp
i = node_index(g,id);
if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
if (g->nodes[i].type != XTC_NODE_PLUGIN) return XTC_ERR_INVALID_ARGUMENT;
if (s->instrument) return XTC_ERR_UNSUPPORTED;
if (g->nodes[i].plugin.inputs != s->inputs) return XTC_ERR_UNSUPPORTED;
g->nodes[i].plugin = *s;
return XTC_OK;

View file

@ -22,7 +22,7 @@ int main(int argc, char **argv)
}
if (argc == 2 && !std::strcmp(argv[1], "--diagnose")) {
unsigned int features = xtc_build_features();
std::printf("xtc %s; project formats 1..%u; JACK %s; LADSPA %s; CLAP/VST2/VST3 mono effects compiled\nFor limits and recovery commands run xtc-check --diagnose/--help.\n",
std::printf("xtc %s; project formats 1..%u; JACK %s; LADSPA %s; CLAP/VST2/VST3 mono effects/instruments compiled\nFor limits and recovery commands run xtc-check --diagnose/--help.\n",
XTC_VERSION_STRING,XTC_PROJECT_FORMAT_VERSION,features & XTC_FEATURE_JACK ? "compiled" : "disabled",
features & XTC_FEATURE_LADSPA ? "compiled" : "disabled");
return EXIT_SUCCESS;

View file

@ -88,6 +88,10 @@ MainWindow::MainWindow(bool prompt_unsaved)
menu->add("Audio/Panic", FL_F + 9, panic_callback, this);
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
menu->add("Audio/Record WAV", FL_SHIFT + FL_F + 5, audio_file_callback, this);
menu->add("Track/Load instrument plugin", 0, instrument_callback, this);
menu->add("Track/Use built-in instrument", 0, instrument_callback, this);
menu->add("Track/Live MIDI to selected instrument", 0, instrument_callback, this);
menu->add("Track/Live MIDI to built-in sine", 0, instrument_callback, this);
menu->add("File/Load plugin", 0, plugin_callback, this);
menu->add("Edit/Replace selected plugin", 0, plugin_callback, this);
menu->add("Edit/Insert plugin on selected wire", 0, plugin_callback, this);
@ -1424,6 +1428,53 @@ void MainWindow::history_callback(Fl_Widget *widget, void *context)
window->report_error("Undo/redo failed", r);
}
xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsigned int index)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
xtc_project *candidate = nullptr;
xtc_plugin_spec spec{};
xtc_result r = path ? xtc_plugin_probe(path,index,48000,&spec) : XTC_OK;
if (r == XTC_OK && path && !spec.instrument) return XTC_ERR_UNSUPPORTED;
if (r == XTC_OK) r = xtc_project_clone(project_.get(),&candidate);
if (r == XTC_OK) r = xtc_project_set_instrument(candidate,track,path ? &spec : nullptr);
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
if (r == XTC_OK) r = install_project(candidate,false);
if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty();
return r;
}
xtc_result MainWindow::select_live_instrument(uint64_t track)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
xtc_project *candidate = nullptr;
xtc_result r = xtc_project_clone(project_.get(),&candidate);
if (r == XTC_OK) r = xtc_project_set_live_instrument(candidate,track);
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
if (r == XTC_OK) r = install_project(candidate,false);
if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty();
return r;
}
void MainWindow::instrument_callback(Fl_Widget *widget, void *context)
{
auto *w = static_cast<MainWindow *>(context);
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
const char *label = menu->mvalue()->label();
uint64_t track = w->sequencer_->selected_track();
xtc_result r;
if (std::strcmp(label,"Use built-in instrument") == 0) r = w->assign_instrument(track,nullptr,0);
else if (std::strcmp(label,"Live MIDI to selected instrument") == 0) r = w->select_live_instrument(track);
else if (std::strcmp(label,"Live MIDI to built-in sine") == 0) r = w->select_live_instrument(UINT64_MAX);
else {
Fl_Native_File_Chooser chooser;
chooser.title("Choose trusted mono instrument binary for selected track");
chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*");
if (chooser.show() != 0 || !chooser.filename()) return;
unsigned int index = 0;
if (!xtc_choose_plugin(chooser.filename(),48000,index,true)) return;
r = w->assign_instrument(track,chooser.filename(),index);
}
if (r != XTC_OK) w->report_error("Instrument assignment failed; select a non-bus track and compatible mono instrument",r);
}
xtc_result MainWindow::set_control_map(unsigned int channel, const unsigned int controllers[3])
{
if (recorder_) return XTC_ERR_INVALID_STATE;

View file

@ -22,6 +22,9 @@ class Fl_Menu_Bar;
class MainWindow : public Fl_Double_Window {
public:
/* Control-thread plugin commands, transactional model adoption. */
xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index);
xtc_result select_live_instrument(uint64_t track);
static void instrument_callback(Fl_Widget *, void *);
xtc_result set_control_map(unsigned int channel, const unsigned int controllers[3]);
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false);

View file

@ -22,7 +22,7 @@ struct Dialog : Fl_Double_Window {
bool run() { callback(cancel,this); end(); show(); while (shown()) Fl::wait(); return accepted; }
};
}
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index)
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index, bool instrument)
{
Dialog dialog(760,450,"Plugin descriptors");
auto *heading = new Fl_Box(15,10,730,40);
@ -36,6 +36,7 @@ bool xtc_choose_plugin(const char *path, double rate, unsigned int &index)
xtc_result r = xtc_plugin_probe(path,i,rate,&spec);
if (r == XTC_ERR_NOT_FOUND) break;
if (r != XTC_OK) { ++rejected; continue; }
if (!!spec.instrument != instrument) { ++rejected; continue; }
if (xtc_plugin_describe(&spec,rate,&details) != XTC_OK) { ++rejected; continue; }
std::string row = std::to_string(i) + ": " + details.name + " (" + spec.label + ") — " + details.maker +
(spec.inputs ? " [effect]" : " [source]");

View file

@ -3,7 +3,7 @@
#include "xtc/xtc_plugin.h"
/* Modal, control-thread-only views. They own copied metadata and widget state,
* never engine/module handles. Cancel/close leaves caller configuration intact. */
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index);
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index, bool instrument = false);
bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details,
xtc_plugin_spec &edited);
#endif

View file

@ -62,6 +62,17 @@ static int inspect(const xtc_project *project, uint64_t version)
if (!available) ++missing;
}
}
for (i = 0; i < tracks; ++i) {
xtc_track_info track; xtc_plugin_spec spec;
xtc_project_get_track(project,i,&track);
if (xtc_project_get_instrument(project,track.id,&spec) == XTC_OK) {
int available = readable_file(spec.path);
printf("%s instrument track %" PRIu64 " descriptor %u ",xtc_plugin_format_name(spec.format),track.id,spec.index);
quoted(spec.identifier); putchar(' '); quoted(spec.label);
printf(": %s ",available ? "available" : "missing/unreadable"); quoted(spec.path); putchar('\n');
if (!available) ++missing;
}
}
printf("Dependency references missing/unreadable: %zu.\n",missing);
puts("File availability only: WAV contents, plugin identity/initialization, JACK and hardware not checked; no plugin code executed.");
return missing ? 2 : EXIT_SUCCESS;
@ -87,7 +98,7 @@ int main(int argc, char **argv)
}
if (argc == 2 && !strcmp(argv[1], "--diagnose")) {
unsigned int features = xtc_build_features();
printf("xtc %s; %zu-bit process\nProject formats: 1..%u\nJACK compiled: %s\nLADSPA compiled: %s\nCLAP/VST2/VST3: compiled (mono effects, default configuration)\n"
printf("xtc %s; %zu-bit process\nProject formats: 1..%u\nJACK compiled: %s\nLADSPA compiled: %s\nCLAP/VST2/VST3: compiled (mono effects/instruments, default configuration)\n"
"Limits: 256 graph nodes (64/project), %u tracks, %u clips, %u notes, 65536 frames/block\n"
"No server, device or plugin probed. Runtime errors appear in GUI status/stderr.\n",
XTC_VERSION_STRING,sizeof(void *)*CHAR_BIT,XTC_PROJECT_FORMAT_VERSION,

View file

@ -1,5 +1,6 @@
#include "xtc/xtc_midi.h"
#include "xtc/xtc_dsp.h"
#include "xtc/xtc_plugin.h"
#include "../mixer/mix_internal.h"
#include <math.h>
#include <stdlib.h>
@ -33,6 +34,11 @@ struct xtc_midi_plan {
unsigned char control_map[16][3];
xtc_mix_snapshot mix;
span notes[XTC_PROJECT_MAX_NOTES];
xtc_plugin *plugins[XTC_PROJECT_MAX_TRACKS];
size_t live_track;
unsigned int plugin_reset;
float scratch[256];
xtc_midi_event queue[256];
size_t count;
voice voices[XTC_MIDI_VOICES];
double step[128];
@ -96,6 +102,7 @@ void xtc_midi_plan_reset(xtc_midi_plan *p)
memset(p->cutoff, 0, sizeof(p->cutoff));
p->next_position = UINT64_MAX;
p->age = 0;
p->plugin_reset = 1;
{ unsigned int c; for (c = 0; c < 16; ++c) {
p->sustain[c] = 0;
(void)xtc_gain_ramp_reset(p->volume+c,1);
@ -189,6 +196,15 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
}
}
free(points);
p->live_track = SIZE_MAX;
for (i = 0; i < p->mix.count; ++i) {
xtc_plugin_spec spec;
if (xtc_project_get_instrument(project,p->mix.tracks[i].id,&spec) == XTC_OK) {
xtc_result r = xtc_plugin_create(&spec,rate,256,&p->plugins[i]);
if (r != XTC_OK) { xtc_midi_plan_destroy(p); return r; }
}
if (project->live_instrument == p->mix.tracks[i].id) p->live_track = i;
}
memcpy(p->control_map,project->control_map,sizeof(p->control_map));
p->controller_frames = rate / 200U;
if (!p->controller_frames) p->controller_frames = 1;
@ -204,6 +220,9 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x
*/
void xtc_midi_plan_destroy(xtc_midi_plan *p)
{
size_t i;
if (!p) return;
for (i = 0; i < XTC_PROJECT_MAX_TRACKS; ++i) xtc_plugin_destroy(p->plugins[i]);
free(p);
}
@ -238,6 +257,7 @@ void xtc_midi_plan_panic(xtc_midi_plan *p, uint64_t position)
for (c = 0; c < 16; ++c)
channel_off(p, c, position);
p->next_position = position;
p->plugin_reset = 1;
}
/**
@ -324,7 +344,7 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ
uint64_t begin = position > s->start ? position : s->start;
uint64_t end = position + frames < s->end ? position + frames : s->end;
double step = p->step[s->pitch];
if (step == 0.0 || s->start < p->cutoff[s->channel] || end <= begin)
if ((s->track < p->mix.count && p->plugins[s->track]) || step == 0.0 || s->start < p->cutoff[s->channel] || end <= begin)
continue;
for (j = (size_t)(begin - position); j < (size_t)(end - position); ++j) {
double phase = fmod((double)(position + j - s->start + s->age) * step, 1.0);
@ -371,6 +391,64 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ
}
}
/* Stable insertion keeps resets, scheduled offs/ons and live events ordered.
* All storage belongs to the prepared plan; callback work is strictly bounded. */
static int enqueue(xtc_midi_plan *p, size_t *count, xtc_midi_event e)
{
size_t i = *count;
if (i == 256) return 0;
while (i && p->queue[i-1].offset > e.offset) { p->queue[i] = p->queue[i-1]; --i; }
p->queue[i] = e; ++*count; return 1;
}
static xtc_result instruments(xtc_midi_plan *p, uint64_t position, const xtc_midi_event *events,
size_t count, float *out, float *right, size_t frames)
{
size_t track, offset, i, j;
for (track = 0; track < p->mix.count; ++track) {
if (!p->plugins[track]) continue;
for (offset = 0; offset < frames; offset += 256) {
size_t n = frames-offset < 256 ? frames-offset : 256, queued = 0;
uint64_t begin = position+offset, end = begin+n;
xtc_result r;
if (!offset && p->plugin_reset) {
unsigned int c;
for (c = 0; c < 16; ++c)
if (!enqueue(p,&queued,(xtc_midi_event){0,XTC_MIDI_ALL_OFF,c,0,0})) return XTC_ERR_CAPACITY;
}
/* End before start at equal offsets, including retriggered keys. */
for (i = 0; i < p->count; ++i) {
const span *s = p->notes+i;
if (s->track == track && s->end >= begin && s->end < end)
if (!enqueue(p,&queued,(xtc_midi_event){(unsigned int)(s->end-begin),XTC_MIDI_NOTE_OFF,s->channel,s->pitch,0})) return XTC_ERR_CAPACITY;
}
for (i = 0; i < p->count; ++i) {
const span *s = p->notes+i;
int chase = !offset && p->plugin_reset && s->start < begin && s->end > begin;
if (s->track == track && s->start >= p->cutoff[s->channel] &&
((s->start >= begin && s->start < end) || chase))
if (!enqueue(p,&queued,(xtc_midi_event){chase ? 0U : (unsigned int)(s->start-begin),XTC_MIDI_NOTE_ON,s->channel,s->pitch,s->velocity})) return XTC_ERR_CAPACITY;
}
if (track == p->live_track) for (i = 0; i < count; ++i) {
xtc_midi_event e = events[i];
if (e.offset >= offset && e.offset < offset+n) {
e.offset -= (unsigned int)offset;
if (!enqueue(p,&queued,e)) return XTC_ERR_CAPACITY;
}
}
r = xtc_plugin_process_midi(p->plugins[track],p->queue,queued,p->scratch,n);
if (r != XTC_OK) return r;
for (j = 0; j < n; ++j) {
float sample = p->scratch[j];
if (p->monitor) xtc_mix_monitor(&p->mix,p->monitor,p->monitor_stride,track,begin+j,offset+j,sample,right != NULL);
out[offset+j] += sample*xtc_mix_level(&p->mix,track,begin+j,right ? 0U : 2U);
if (right) right[offset+j] += sample*xtc_mix_level(&p->mix,track,begin+j,1);
}
}
}
p->plugin_reset = 0;
return XTC_OK;
}
/**
* @brief Renders scheduled and live MIDI with sample-offset events and track gain and pan.
*
@ -408,10 +486,18 @@ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, cons
for (i = 0; i < count; ++i) {
size_t next = events[i].offset;
segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, next - cursor);
event(p, events + i, position + next);
if (p->live_track == SIZE_MAX) event(p, events + i, position + next);
cursor = next;
}
segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, frames - cursor);
{
xtc_result r = instruments(p,position,events,count,out,right,frames);
if (r != XTC_OK) {
memset(out,0,frames*sizeof(*out));
if (right) memset(right,0,frames*sizeof(*right));
xtc_midi_plan_reset(p); return r;
}
}
for (i = 0; i < frames; ++i) {
out[i] = xtc_dsp_finite(out[i]);
if (right) right[i] = xtc_dsp_finite(right[i]);

View file

@ -2,6 +2,27 @@
#include "../graph/graph_internal.h"
#include <string.h>
xtc_result xtc_project_ensure_midi_route(xtc_project *c)
{
size_t i, source = SIZE_MAX, output = SIZE_MAX;
xtc_result r = XTC_OK;
for (i = 0; i < c->graph->node_count; ++i) {
xtc_node *n = c->graph->nodes + i;
if (n->type == XTC_NODE_INSTRUMENT)
source = n->id;
if (n->type == XTC_NODE_OUTPUT)
output = n->id;
}
if (source == SIZE_MAX) {
r = xtc_graph_add_instrument(c->graph, &source);
if (r == XTC_OK && output == SIZE_MAX)
r = xtc_graph_add_output(c->graph, &output);
if (r == XTC_OK)
r = xtc_graph_connect(c->graph, source, output);
}
return r;
}
/**
* @brief Adds a MIDI clip and ensures an instrument output route exists.
*
@ -16,7 +37,6 @@ xtc_result xtc_project_add_midi_clip(xtc_project *p, uint64_t track, uint64_t st
{
xtc_project *c = NULL;
xtc_result r;
size_t i, source = SIZE_MAX, output = SIZE_MAX;
uint64_t id;
if (!p || !out)
return XTC_ERR_INVALID_ARGUMENT;
@ -25,20 +45,7 @@ xtc_result xtc_project_add_midi_clip(xtc_project *p, uint64_t track, uint64_t st
r = xtc_project_add_clip(c, track, "MIDI", start, length, &id);
if (r == XTC_OK) {
c->clips[c->clip_count - 1].midi = 1;
for (i = 0; i < c->graph->node_count; ++i) {
xtc_node *n = c->graph->nodes + i;
if (n->type == XTC_NODE_INSTRUMENT)
source = n->id;
if (n->type == XTC_NODE_OUTPUT)
output = n->id;
}
if (source == SIZE_MAX) {
r = xtc_graph_add_instrument(c->graph, &source);
if (r == XTC_OK && output == SIZE_MAX)
r = xtc_graph_add_output(c->graph, &output);
if (r == XTC_OK)
r = xtc_graph_connect(c->graph, source, output);
}
r = xtc_project_ensure_midi_route(c);
}
if (r == XTC_OK) {
xtc_graph *old = p->graph;

View file

@ -443,6 +443,29 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
}
}
}
if (format_version >= 12U) {
size_t bindings;
READ("TRACK_INSTRUMENTS",2U); NUM(1U,value);
if (value > project->track_count) return XTC_ERR_FORMAT;
bindings = (size_t)value;
for (i = 0; i < bindings; ++i) {
uint64_t track; size_t slot; xtc_plugin_spec *s;
READ("INSTRUMENT",6U); NUM(1U,track);
for (slot = 0; slot < project->track_count && project->tracks[slot].id != track; ++slot) {}
if (slot == project->track_count || project->tracks[slot].is_bus || project->instruments[slot].instrument) return XTC_ERR_FORMAT;
s = project->instruments+slot; s->instrument = 1; s->tempo = project->tempo;
NUM(2U,value); if (value < XTC_PLUGIN_CLAP || value > XTC_PLUGIN_VST3) return XTC_ERR_UNSUPPORTED;
s->format = (xtc_plugin_format)value;
NUM(3U,value); if (value >= 1024) return XTC_ERR_FORMAT; s->index = (unsigned int)value;
NUM(4U,value); if (value > UINT32_MAX) return XTC_ERR_FORMAT; s->unique_id = (unsigned int)value;
if (!decode_bytes(input->tokens[5],s->label,sizeof(s->label))) return XTC_ERR_FORMAT;
READ("MODULE",2U); if (!decode_bytes(input->tokens[1],s->path,sizeof(s->path))) return XTC_ERR_FORMAT;
READ("IDENTITY",2U);
if (strcmp(input->tokens[1],"-") && !decode_bytes(input->tokens[1],s->identifier,sizeof(s->identifier))) return XTC_ERR_FORMAT;
if (xtc_plugin_spec_validate(s) != XTC_OK) return XTC_ERR_FORMAT;
}
READ("LIVE_INSTRUMENT",2U); NUM(1U,project->live_instrument);
}
READ("END", 1U);
if (fgetc(input->file) != EOF)
return XTC_ERR_FORMAT;
@ -590,6 +613,17 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch
for (i = 0; i < 16; ++i)
if (fprintf(file,"MAP %zu %u %u %u\n",i,(unsigned int)project->control_map[i][0],
(unsigned int)project->control_map[i][1],(unsigned int)project->control_map[i][2]) < 0) return XTC_ERR_IO;
{ size_t bindings = 0;
for (i = 0; i < project->track_count; ++i) if (project->instruments[i].instrument) ++bindings;
if (fprintf(file,"TRACK_INSTRUMENTS %zu\n",bindings) < 0) return XTC_ERR_IO;
for (i = 0; i < project->track_count; ++i) if (project->instruments[i].instrument) {
const xtc_plugin_spec *s = project->instruments+i;
if (fprintf(file,"INSTRUMENT %" PRIu64 " %u %u %u ",project->tracks[i].id,(unsigned int)s->format,s->index,s->unique_id) < 0 ||
!write_name(file,s->label) || fputs("MODULE ",file) == EOF || !write_name(file,s->path) || fputs("IDENTITY ",file) == EOF ||
(s->identifier[0] ? !write_name(file,s->identifier) : fputs("-\n",file) == EOF)) return XTC_ERR_IO;
}
if (fprintf(file,"LIVE_INSTRUMENT %" PRIu64 "\n",project->live_instrument) < 0) return XTC_ERR_IO;
}
return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK;
}

View file

@ -42,7 +42,7 @@ xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double rate, xtc_plugin
if (s->format == XTC_PLUGIN_LADSPA) return xtc_ladspa_describe(s,rate,out);
r = xtc_plugin_probe_format(s->format,s->path,s->index,rate,&actual);
if (r != XTC_OK) return r;
if (actual.unique_id != s->unique_id || strcmp(actual.identifier,s->identifier) || strcmp(actual.label,s->label)) return XTC_ERR_UNSUPPORTED;
if (actual.instrument != s->instrument || actual.unique_id != s->unique_id || strcmp(actual.identifier,s->identifier) || strcmp(actual.label,s->label)) return XTC_ERR_UNSUPPORTED;
memset(out,0,sizeof(*out)); snprintf(out->name,sizeof(out->name),"%s",s->label);
snprintf(out->maker,sizeof(out->maker),"Not exposed by this adapter");
snprintf(out->copyright,sizeof(out->copyright),"See plugin license");
@ -61,7 +61,7 @@ xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frame
if (s->format == XTC_PLUGIN_LADSPA) r = xtc_ladspa_create(s,rate,frames,&p->ladspa);
else {
r = xtc_modern_open(s->format,s->path,s->index,rate,frames,&p->modern,&actual);
if (r == XTC_OK && (actual.unique_id != s->unique_id || actual.inputs != s->inputs ||
if (r == XTC_OK && (actual.instrument != s->instrument || actual.unique_id != s->unique_id || actual.inputs != s->inputs ||
strcmp(actual.identifier,s->identifier) || strcmp(actual.label,s->label))) r = XTC_ERR_UNSUPPORTED;
}
if (r != XTC_OK) { xtc_plugin_destroy(p); return r; }
@ -75,6 +75,8 @@ xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *in, float *out, siz
if (r == XTC_OK) p->next = position <= UINT64_MAX-frames ? position+frames : 0;
return r;
}
xtc_result xtc_plugin_process_midi(xtc_plugin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames)
{ return p && p->modern ? xtc_modern_process_midi(p->modern,events,count,out,frames) : XTC_ERR_UNSUPPORTED; }
xtc_result xtc_plugin_process(xtc_plugin *p, const float *in, float *out, size_t frames)
{ return xtc_plugin_process_at(p,in,out,frames,p ? p->next : 0); }
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])

View file

@ -27,7 +27,7 @@ xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
if (s->format < XTC_PLUGIN_LADSPA || s->format > XTC_PLUGIN_VST3 ||
!terminated(s->identifier,sizeof(s->identifier))) return XTC_ERR_INVALID_ARGUMENT;
if (s->format != XTC_PLUGIN_LADSPA) {
if (s->inputs != 1 || s->control_count || s->point_count ||
if (s->instrument > 1 || s->inputs != (s->instrument ? 0U : 1U) || s->control_count || s->point_count ||
((s->format == XTC_PLUGIN_CLAP || s->format == XTC_PLUGIN_VST3) && !s->identifier[0]))
return XTC_ERR_UNSUPPORTED;
if (s->format == XTC_PLUGIN_VST2 && s->identifier[0]) return XTC_ERR_INVALID_ARGUMENT;
@ -37,7 +37,7 @@ xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
if (!((s->identifier[i] >= '0' && s->identifier[i] <= '9') ||
(s->identifier[i] >= 'a' && s->identifier[i] <= 'f'))) return XTC_ERR_INVALID_ARGUMENT;
}
} else if (s->identifier[0]) return XTC_ERR_INVALID_ARGUMENT;
} else if (s->instrument || s->identifier[0]) return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < s->control_count; ++i)
if (s->ports[i] >= 64 || (i && s->ports[i] <= s->ports[i-1]) || !isfinite(s->values[i]))
return XTC_ERR_INVALID_ARGUMENT;

View file

@ -24,6 +24,8 @@ typedef struct module {
struct module *next;
} module;
static module *modules;
typedef union { clap_event_header_t header; clap_event_note_t note; clap_event_midi_t midi; } clap_note;
typedef struct { Steinberg_Vst_IEventList api; struct xtc_modern *owner; } vst_events;
struct xtc_modern {
module *module;
xtc_plugin_format format;
@ -38,10 +40,19 @@ struct xtc_modern {
double rate;
pthread_t control_thread;
int initialized, active, started;
unsigned int instrument, clap_midi;
size_t event_count, clap_count;
clap_note clap_events[XTC_MIDI_MAX_EVENTS];
xtc_v2_midi v2_events[XTC_MIDI_MAX_EVENTS];
struct Steinberg_Vst_Event vst_events[2304];
size_t vst_count;
unsigned char keys[16][128];
vst_events event_list;
unsigned int callback, restart;
struct xtc_modern *next;
};
static xtc_modern *instances;
static Steinberg_Vst_IEventListVtbl event_vtable;
static __thread int in_audio;
static __thread xtc_modern *constructing;
@ -106,9 +117,10 @@ void xtc_plugin_idle(void)
for (p = instances; p; p = p->next)
if (p->clap && __atomic_exchange_n(&p->callback,0U,__ATOMIC_ACQ_REL)) p->clap->on_main_thread(p->clap);
}
static uint32_t event_count(const clap_input_events_t *list) { (void)list; return 0; }
static uint32_t event_count(const clap_input_events_t *list)
{ const xtc_modern *p = list->ctx; return (uint32_t)p->clap_count; }
static const clap_event_header_t *event_get(const clap_input_events_t *list, uint32_t index)
{ (void)list; (void)index; return NULL; }
{ const xtc_modern *p = list->ctx; return index < p->clap_count ? &p->clap_events[index].header : NULL; }
static bool event_push(const clap_output_events_t *list, const clap_event_header_t *event)
{ (void)list; (void)event; return false; }
static xtc_result open_clap(xtc_modern *p, unsigned int index, xtc_plugin_spec *s)
@ -130,11 +142,18 @@ static xtc_result open_clap(xtc_modern *p, unsigned int index, xtc_plugin_spec *
return XTC_ERR_UNSUPPORTED;
if (!p->clap->init(p->clap)) return XTC_ERR_INVALID_STATE;
ports = p->clap->get_extension(p->clap,CLAP_EXT_AUDIO_PORTS);
if (!ports || !ports->count || !ports->get || ports->count(p->clap,true) != 1 || ports->count(p->clap,false) != 1 ||
!ports->get(p->clap,0,true,&info) || info.channel_count != 1 ||
if (!ports || !ports->count || !ports->get || ports->count(p->clap,false) != 1 ||
!ports->get(p->clap,0,false,&info) || info.channel_count != 1) return XTC_ERR_UNSUPPORTED;
{ const clap_plugin_note_ports_t *notes = p->clap->get_extension(p->clap,CLAP_EXT_NOTE_PORTS);
if (notes && (!notes->count || notes->count(p->clap,true) || notes->count(p->clap,false))) return XTC_ERR_UNSUPPORTED; }
{ uint32_t inputs = ports->count(p->clap,true);
const clap_plugin_note_ports_t *notes = p->clap->get_extension(p->clap,CLAP_EXT_NOTE_PORTS);
if (inputs == 0 && notes && notes->count && notes->get && notes->count(p->clap,true) == 1 && !notes->count(p->clap,false)) {
clap_note_port_info_t note;
if (!notes->get(p->clap,0,true,&note) || !(note.supported_dialects & (CLAP_NOTE_DIALECT_CLAP|CLAP_NOTE_DIALECT_MIDI))) return XTC_ERR_UNSUPPORTED;
p->instrument = s->instrument = 1; s->inputs = 0;
p->clap_midi = (note.supported_dialects & CLAP_NOTE_DIALECT_MIDI) != 0;
} else if (inputs != 1 || !ports->get(p->clap,0,true,&info) || info.channel_count != 1 ||
(notes && (!notes->count || notes->count(p->clap,true) || notes->count(p->clap,false)))) return XTC_ERR_UNSUPPORTED;
}
if (!p->clap->activate(p->clap,p->rate,1,(uint32_t)p->frames)) return XTC_ERR_INVALID_STATE;
p->active = 1;
{ const clap_plugin_latency_t *latency = p->clap->get_extension(p->clap,CLAP_EXT_LATENCY);
@ -147,6 +166,8 @@ static intptr_t v2_host(xtc_v2_effect *effect, int32_t opcode, int32_t index, in
(void)index; (void)value; (void)option;
switch (opcode) {
case 1: return 2400; /* host API version */
case 6: return 1; /* Legacy MIDI delivery request. */
case 37: return ptr && (!strcmp(ptr,"sendVstEvents") || !strcmp(ptr,"sendVstMidiEvent")) ? 1 : 0;
case 13: if (p) __atomic_store_n(&p->restart,1U,__ATOMIC_RELEASE); return 0;
case 16: return p ? (intptr_t)p->rate : 48000;
case 17: return p ? (intptr_t)p->frames : 256;
@ -167,10 +188,11 @@ static xtc_result open_v2(xtc_modern *p, unsigned int index, xtc_plugin_spec *s)
if (!p->v2 || p->v2->magic != XTC_V2_MAGIC || !p->v2->dispatch) return XTC_ERR_UNSUPPORTED;
p->v2->user = p;
p->v2->dispatch(p->v2,0,0,0,NULL,0); p->initialized = 1;
if (p->v2->inputs != 1 || p->v2->outputs != 1 || !p->v2->replace || !(p->v2->flags & 16) ||
(p->v2->flags & 256) || p->v2->initial_delay) return XTC_ERR_UNSUPPORTED;
if (p->v2->outputs != 1 || !p->v2->replace || !(p->v2->flags & 16) || p->v2->initial_delay ||
((p->v2->flags & 256) ? p->v2->inputs != 0 : p->v2->inputs != 1)) return XTC_ERR_UNSUPPORTED;
p->v2->dispatch(p->v2,45,0,0,name,0); name[63] = 0;
if (!name[0]) return XTC_ERR_UNSUPPORTED;
if (p->v2->flags & 256) { p->instrument = s->instrument = 1; s->inputs = 0; }
snprintf(s->label,sizeof(s->label),"%s",name); s->unique_id = (unsigned int)p->v2->unique_id;
p->v2->dispatch(p->v2,10,0,0,NULL,(float)p->rate);
p->v2->dispatch(p->v2,11,0,(intptr_t)p->frames,NULL,0);
@ -222,11 +244,17 @@ static xtc_result open_v3(xtc_modern *p, unsigned int index, xtc_plugin_spec *s)
if (!p->processor->lpVtbl || !p->processor->lpVtbl->release || !p->processor->lpVtbl->canProcessSampleSize ||
!p->processor->lpVtbl->setBusArrangements || !p->processor->lpVtbl->setupProcessing || !p->processor->lpVtbl->getLatencySamples ||
!p->processor->lpVtbl->setProcessing || !p->processor->lpVtbl->process) return XTC_ERR_UNSUPPORTED;
if (p->component->lpVtbl->getBusCount(p->component,0,0) != 1 || p->component->lpVtbl->getBusCount(p->component,0,1) != 1 ||
p->component->lpVtbl->getBusCount(p->component,1,0) || p->component->lpVtbl->getBusCount(p->component,1,1) ||
p->processor->lpVtbl->canProcessSampleSize(p->processor,0) != 0 ||
p->processor->lpVtbl->setBusArrangements(p->processor,&mono,1,&mono,1) != 0) return XTC_ERR_UNSUPPORTED;
for (i = 0; i < 2; ++i) {
{ Steinberg_int32 inputs = p->component->lpVtbl->getBusCount(p->component,0,0);
Steinberg_int32 event_inputs = p->component->lpVtbl->getBusCount(p->component,1,0);
if (inputs == 0 && event_inputs == 1) { p->instrument = s->instrument = 1; s->inputs = 0; }
else if (inputs != 1 || event_inputs != 0) return XTC_ERR_UNSUPPORTED;
if (p->component->lpVtbl->getBusCount(p->component,0,1) != 1 ||
p->component->lpVtbl->getBusCount(p->component,1,1) ||
p->processor->lpVtbl->canProcessSampleSize(p->processor,0) != 0 ||
p->processor->lpVtbl->setBusArrangements(p->processor,inputs ? &mono : NULL,inputs,&mono,1) != 0) return XTC_ERR_UNSUPPORTED;
if (event_inputs && p->component->lpVtbl->activateBus(p->component,1,0,0,1) != 0) return XTC_ERR_UNSUPPORTED;
}
for (i = p->instrument ? 1U : 0U; i < 2; ++i) {
struct Steinberg_Vst_BusInfo bus;
if (p->component->lpVtbl->getBusInfo(p->component,0,(Steinberg_int32)i,0,&bus) != 0 || bus.channelCount != 1 ||
p->component->lpVtbl->activateBus(p->component,0,(Steinberg_int32)i,0,1) != 0) return XTC_ERR_UNSUPPORTED;
@ -244,6 +272,7 @@ xtc_result xtc_modern_open(xtc_plugin_format format, const char *path, unsigned
p = calloc(1,sizeof(*p));
if (!p) return XTC_ERR_OUT_OF_MEMORY;
p->format = format; p->frames = frames; p->rate = rate; p->control_thread = pthread_self();
p->event_list.api.lpVtbl = &event_vtable; p->event_list.owner = p;
p->input = calloc(frames,sizeof(float)); p->output = calloc(frames,sizeof(float));
if (!p->input || !p->output) { xtc_modern_destroy(p); return XTC_ERR_OUT_OF_MEMORY; }
r = module_open(format,path,&p->module);
@ -291,25 +320,109 @@ xtc_result xtc_modern_process(xtc_modern *p, const float *input, float *output,
memset(p->output,0,frames*sizeof(float));
in_audio = 1;
if (p->clap) {
clap_input_events_t events = {NULL,event_count,event_get}; clap_output_events_t sink = {NULL,event_push};
clap_input_events_t events = {p,event_count,event_get}; clap_output_events_t sink = {NULL,event_push};
clap_audio_buffer_t in = {&p->input,NULL,1,0,0}, out = {&p->output,NULL,1,0,0};
clap_process_t block = {-1,(uint32_t)frames,NULL,&in,&out,1,1,&events,&sink};
clap_process_t block = {-1,(uint32_t)frames,NULL,p->instrument ? NULL : &in,&out,p->instrument ? 0U : 1U,1,&events,&sink};
if (!p->started) { p->started = p->clap->start_processing(p->clap); if (!p->started) ok = 0; }
if (ok) {
clap_process_status status = p->clap->process(p->clap,&block);
ok = status >= CLAP_PROCESS_CONTINUE && status <= CLAP_PROCESS_SLEEP;
}
} else if (p->v2) p->v2->replace(p->v2,&p->input,&p->output,(int32_t)frames);
} else if (p->v2) {
if (p->event_count) {
xtc_v2_events events; memset(&events,0,sizeof(events)); events.count = (int32_t)p->event_count;
for (i = 0; i < p->event_count; ++i) events.events[i] = p->v2_events+i;
p->v2->dispatch(p->v2,25,0,0,&events,0);
}
p->v2->replace(p->v2,p->instrument ? NULL : &p->input,&p->output,(int32_t)frames);
}
else {
struct Steinberg_Vst_AudioBusBuffers in = {0}, out = {0};
struct Steinberg_Vst_ProcessData data = {0};
in.numChannels = out.numChannels = 1;
in.Steinberg_Vst_AudioBusBuffers_channelBuffers32 = &p->input;
out.Steinberg_Vst_AudioBusBuffers_channelBuffers32 = &p->output;
data.numSamples = (Steinberg_int32)frames; data.numInputs = data.numOutputs = 1; data.inputs = &in; data.outputs = &out;
data.numSamples = (Steinberg_int32)frames; data.numInputs = p->instrument ? 0 : 1; data.numOutputs = 1; data.inputs = p->instrument ? NULL : &in; data.outputs = &out;
if (p->instrument) data.inputEvents = &p->event_list.api;
ok = p->processor->lpVtbl->process(p->processor,&data) == 0;
}
in_audio = 0;
for (i = 0; i < frames; ++i) output[i] = ok ? xtc_dsp_finite(p->output[i]) : 0;
return ok ? XTC_OK : XTC_ERR_INVALID_STATE;
}
static Steinberg_tresult list_query(void *self, const Steinberg_TUID iid, void **out)
{
if (!out || !iid) return Steinberg_kInvalidArgument;
*out = NULL;
if (!memcmp(iid,Steinberg_Vst_IEventList_iid,16) || !memcmp(iid,Steinberg_FUnknown_iid,16)) { *out = self; return 0; }
return Steinberg_kNoInterface;
}
static Steinberg_int32 list_count(void *self) { return (Steinberg_int32)((vst_events *)self)->owner->vst_count; }
static Steinberg_tresult list_get(void *self, Steinberg_int32 index, struct Steinberg_Vst_Event *out)
{
xtc_modern *p = ((vst_events *)self)->owner;
if (!out || index < 0 || (size_t)index >= p->vst_count) return Steinberg_kInvalidArgument;
*out = p->vst_events[index]; return 0;
}
static Steinberg_tresult list_add(void *self, struct Steinberg_Vst_Event *event)
{ (void)self; (void)event; return Steinberg_kNotImplemented; }
static Steinberg_Vst_IEventListVtbl event_vtable = {list_query,host_ref,host_ref,list_count,list_get,list_add};
static void vst_note(xtc_modern *p, unsigned int offset, unsigned int channel, unsigned int pitch, unsigned int velocity, int on)
{
struct Steinberg_Vst_Event *v = p->vst_events+p->vst_count++;
memset(v,0,sizeof(*v)); v->sampleOffset = (Steinberg_int32)offset; v->type = on ? 0 : 1;
if (on) {
v->Steinberg_Vst_Event_noteOn.channel = (Steinberg_int16)channel;
v->Steinberg_Vst_Event_noteOn.pitch = (Steinberg_int16)pitch;
v->Steinberg_Vst_Event_noteOn.velocity = (float)velocity/127;
v->Steinberg_Vst_Event_noteOn.noteId = -1;
} else {
v->Steinberg_Vst_Event_noteOff.channel = (Steinberg_int16)channel;
v->Steinberg_Vst_Event_noteOff.pitch = (Steinberg_int16)pitch;
v->Steinberg_Vst_Event_noteOff.noteId = -1;
}
p->keys[channel][pitch] = (unsigned char)on;
}
xtc_result xtc_modern_process_midi(xtc_modern *p, const xtc_midi_event *events, size_t count, float *output, size_t frames)
{
size_t i; xtc_result r;
if (!p || !p->instrument || !output || (count && !events) || count > XTC_MIDI_MAX_EVENTS || frames > p->frames) return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < count; ++i) {
const xtc_midi_event *e = events+i;
if (e->offset >= frames || (i && e->offset < events[i-1].offset) || e->channel > 15 || e->pitch > 127 || e->velocity > 127 ||
e->type < XTC_MIDI_NOTE_ON || e->type > XTC_MIDI_CONTROL || (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT;
}
p->event_count = count; p->vst_count = 0; p->clap_count = 0;
p->event_list.api.lpVtbl = &event_vtable; p->event_list.owner = p;
for (i = 0; i < count; ++i) {
const xtc_midi_event *e = events+i;
unsigned char status = (unsigned char)((e->type == XTC_MIDI_NOTE_ON ? 144 : e->type == XTC_MIDI_NOTE_OFF ? 128 : 176) | e->channel);
unsigned char key = (unsigned char)(e->type == XTC_MIDI_ALL_OFF ? 120 : e->pitch);
unsigned char value = (unsigned char)(e->type == XTC_MIDI_ALL_OFF ? 0 : e->velocity);
clap_note *c = p->clap_events+p->clap_count; memset(c,0,sizeof(*c));
if (p->clap_midi) {
c->midi.header = (clap_event_header_t){sizeof(c->midi),e->offset,CLAP_CORE_EVENT_SPACE_ID,CLAP_EVENT_MIDI,0};
c->midi.data[0] = status; c->midi.data[1] = key; c->midi.data[2] = value;
} else {
c->note.header = (clap_event_header_t){sizeof(c->note),e->offset,CLAP_CORE_EVENT_SPACE_ID,
e->type == XTC_MIDI_NOTE_ON ? CLAP_EVENT_NOTE_ON : e->type == XTC_MIDI_NOTE_OFF ? CLAP_EVENT_NOTE_OFF : CLAP_EVENT_NOTE_CHOKE,0};
c->note.note_id = -1; c->note.port_index = 0; c->note.channel = (int16_t)e->channel;
c->note.key = e->type == XTC_MIDI_ALL_OFF ? -1 : (int16_t)e->pitch; c->note.velocity = (double)e->velocity/127;
/* No generic controller mapping in native-note CLAP or VST3. */
if (e->type == XTC_MIDI_CONTROL) c->note.header.type = UINT16_MAX;
}
if (p->clap_midi || e->type != XTC_MIDI_CONTROL) ++p->clap_count;
memset(p->v2_events+i,0,sizeof(*p->v2_events));
p->v2_events[i].type = 1; p->v2_events[i].bytes = sizeof(xtc_v2_midi); p->v2_events[i].offset = (int32_t)e->offset;
p->v2_events[i].data[0] = status; p->v2_events[i].data[1] = key; p->v2_events[i].data[2] = value;
if (e->type == XTC_MIDI_NOTE_ON || e->type == XTC_MIDI_NOTE_OFF)
vst_note(p,e->offset,e->channel,e->pitch,e->velocity,e->type == XTC_MIDI_NOTE_ON);
else if (e->type == XTC_MIDI_ALL_OFF) {
unsigned int k;
for (k = 0; k < 128; ++k) if (p->keys[e->channel][k]) vst_note(p,e->offset,e->channel,k,0,0);
}
}
r = xtc_modern_process(p,NULL,output,frames);
p->event_count = 0; p->clap_count = 0; p->vst_count = 0; return r;
}

View file

@ -4,5 +4,6 @@
typedef struct xtc_modern xtc_modern;
xtc_result xtc_modern_open(xtc_plugin_format format, const char *path, unsigned int index, double rate, size_t frames, xtc_modern **out, xtc_plugin_spec *spec);
xtc_result xtc_modern_process(xtc_modern *plugin, const float *input, float *output, size_t frames);
xtc_result xtc_modern_process_midi(xtc_modern *plugin, const xtc_midi_event *events, size_t count, float *output, size_t frames);
void xtc_modern_destroy(xtc_modern *plugin);
#endif

View file

@ -22,5 +22,11 @@ struct xtc_v2_effect {
void (*replace64)(xtc_v2_effect *, double **, double **, int32_t);
char future[56];
};
typedef struct {
int32_t type, bytes, offset, flags, note_length, note_offset;
unsigned char data[4];
char detune, off_velocity, reserved1, reserved2;
} xtc_v2_midi;
typedef struct { int32_t count; intptr_t reserved; xtc_v2_midi *events[256]; } xtc_v2_events;
#define XTC_V2_MAGIC INT32_C(0x56737450)
#endif

View file

@ -119,6 +119,7 @@ xtc_result xtc_project_create(xtc_project **out_project)
}
project->graph->require_output_nodes = 1;
project->tempo = 120000U;
project->live_instrument = UINT64_MAX;
{ size_t channel;
for (channel = 0; channel < 16; ++channel) {
project->control_map[channel][0] = 7;
@ -331,6 +332,10 @@ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id)
project->clip_count = count;
for (i = 0; i < project->track_count; ++i)
if (project->tracks[i].bus_id == id) project->tracks[i].bus_id = UINT64_MAX;
if (project->live_instrument == id) project->live_instrument = UINT64_MAX;
memmove(project->instruments+index,project->instruments+index+1,
(project->track_count-index-1)*sizeof(*project->instruments));
memset(project->instruments+project->track_count-1,0,sizeof(*project->instruments));
memmove(project->tracks + index, project->tracks + index + 1U,
(project->track_count - index - 1U) * sizeof(*project->tracks));
--project->track_count;
@ -530,7 +535,13 @@ xtc_result xtc_project_validate(const xtc_project *project)
for (k = 0; k < j; ++k)
if (cc != 128 && cc == project->control_map[i][k]) return XTC_ERR_FORMAT;
}
if (project->live_instrument != UINT64_MAX) {
size_t target = track_index(project,project->live_instrument);
if (target == SIZE_MAX || !project->instruments[target].instrument) return XTC_ERR_FORMAT;
}
for (i = 0U; i < project->track_count; ++i) {
if (project->instruments[i].instrument && (project->tracks[i].is_bus ||
xtc_plugin_spec_validate(project->instruments+i) != XTC_OK)) return XTC_ERR_FORMAT;
if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name))
return XTC_ERR_FORMAT;
{
@ -674,6 +685,10 @@ xtc_result xtc_project_move_track(xtc_project *p, uint64_t id, size_t destinatio
size_t i; xtc_track_info track;
if (!p || destination >= p->track_count) return XTC_ERR_INVALID_ARGUMENT;
i = track_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
{ xtc_plugin_spec instrument = p->instruments[i];
if (i < destination) memmove(p->instruments+i,p->instruments+i+1,(destination-i)*sizeof(instrument));
else if (i > destination) memmove(p->instruments+destination+1,p->instruments+destination,(i-destination)*sizeof(instrument));
p->instruments[destination] = instrument; }
track = p->tracks[i];
if (i < destination) memmove(p->tracks+i,p->tracks+i+1,(destination-i)*sizeof(track));
else if (i > destination) memmove(p->tracks+destination+1,p->tracks+destination,(i-destination)*sizeof(track));
@ -718,3 +733,38 @@ xtc_result xtc_project_split_audio(xtc_project *p, uint64_t id, uint64_t tick, u
*out = right.id;
return XTC_OK;
}
xtc_result xtc_project_set_instrument(xtc_project *p, uint64_t track, const xtc_plugin_spec *spec)
{
size_t i; xtc_project *candidate = NULL; xtc_result r;
if (!p) return XTC_ERR_INVALID_ARGUMENT;
i = track_index(p,track);
if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
if (p->tracks[i].is_bus) return XTC_ERR_INVALID_STATE;
if (spec && (!spec->instrument || xtc_plugin_spec_validate(spec) != XTC_OK)) return XTC_ERR_UNSUPPORTED;
r = xtc_project_clone(p,&candidate);
if (r == XTC_OK && spec) r = xtc_project_ensure_midi_route(candidate);
if (r == XTC_OK) {
if (spec) { candidate->instruments[i] = *spec; candidate->live_instrument = track; }
else { memset(candidate->instruments+i,0,sizeof(*candidate->instruments));
if (candidate->live_instrument == track) candidate->live_instrument = UINT64_MAX; }
{ xtc_graph *old = p->graph; *p = *candidate; candidate->graph = old; }
}
xtc_project_destroy(candidate); return r;
}
xtc_result xtc_project_get_instrument(const xtc_project *p, uint64_t track, xtc_plugin_spec *out)
{
size_t i;
if (!p || !out) return XTC_ERR_INVALID_ARGUMENT;
i = track_index(p,track); if (i == SIZE_MAX || !p->instruments[i].instrument) return XTC_ERR_NOT_FOUND;
*out = p->instruments[i]; return XTC_OK;
}
xtc_result xtc_project_set_live_instrument(xtc_project *p, uint64_t track)
{
xtc_plugin_spec s;
if (!p) return XTC_ERR_INVALID_ARGUMENT;
if (track != UINT64_MAX && xtc_project_get_instrument(p,track,&s) != XTC_OK) return XTC_ERR_NOT_FOUND;
p->live_instrument = track; return XTC_OK;
}
xtc_result xtc_project_get_live_instrument(const xtc_project *p, uint64_t *track)
{ if (!p || !track) return XTC_ERR_INVALID_ARGUMENT; *track = p->live_instrument; return XTC_OK; }

View file

@ -4,9 +4,12 @@
#include "xtc/xtc_project.h"
#include "xtc/xtc_midi.h"
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_plugin.h"
struct xtc_project {
xtc_graph *graph;
uint64_t live_instrument;
xtc_plugin_spec instruments[XTC_PROJECT_MAX_TRACKS];
uint64_t next_track_id, next_clip_id;
unsigned int tempo;
unsigned char control_map[16][3];
@ -26,6 +29,7 @@ struct xtc_project {
* @param name NUL-terminated name.
* @return Nonzero for a valid name, zero otherwise.
*/
xtc_result xtc_project_ensure_midi_route(xtc_project *project);
int xtc_project_valid_name(const char *name);
/**
* @brief Validates project IDs, timeline ranges, names, graph, notes, and automation.

View file

@ -3,7 +3,7 @@
Store small, redistributable fixtures here. Document their origin and license.
Do not commit large recordings or assets with unclear licensing.
`project-v1.xtc` through `project-v11.xtc` are hand-authored xtc metadata fixtures
`project-v1.xtc` through `project-v12.xtc` are hand-authored xtc metadata fixtures
created for this repository, under the project license. They contain no audio
or third-party resources. The v5/v6 plugin fixtures intentionally reference a
missing module to exercise metadata migration without loading code.

41
tests/fixtures/project-v12.xtc vendored Normal file
View file

@ -0,0 +1,41 @@
XTC_PROJECT 12
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 2 2 1
NODE 0 1 3f000000
NODE 1 4 00000000
EDGE 0 1
TRACKS 1
TRACK 0 4b657973
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 0 960 0 960 5665727365
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 1 1
NOTE 0 0 0 480 69 100 0
AUTOMATION 1
POINT 0 0 0 3f800000 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
TRACK_INSTRUMENTS 0
LIVE_INSTRUMENT 18446744073709551615
END

View file

@ -827,6 +827,27 @@ int main(int argc, char **argv)
unlink("/tmp/xtc-gui-modern-test.xtc"); window.do_callback();
}
}
for (const char *variable : {"XTC_TEST_SYNTH_CLAP","XTC_TEST_SYNTH_VST2","XTC_TEST_SYNTH_VST3"}) {
if (const char *module = std::getenv(variable)) {
reset(); MainWindow window(false); auto *sequence = timeline(&window);
CHECK(sequence->add_track() == XTC_OK && sequence->add_midi_clip() == XTC_OK);
uint64_t track = sequence->selected_track(); xtc_plugin_spec spec{};
CHECK(window.assign_instrument(track,module,0) == XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_OK && spec.instrument);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_ERR_NOT_FOUND);
CHECK(window.undo(true) == XTC_OK);
CHECK(window.select_live_instrument(UINT64_MAX) == XTC_OK);
CHECK(window.select_live_instrument(track) == XTC_OK);
const xtc_project *before = sequence->project();
CHECK(window.assign_instrument(track,"/absent/instrument.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(window.save_project("/tmp/xtc-gui-instrument.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-gui-instrument.xtc") == XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_OK && spec.instrument);
CHECK(window.assign_instrument(track,nullptr,0) == XTC_OK);
unlink("/tmp/xtc-gui-instrument.xtc"); window.do_callback();
}
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;

View file

@ -54,7 +54,7 @@ def main():
run([str(binaries / 'xtc-check'), '--inspect', str(example)])
assert (resources / 'xtc/examples/README.md').is_file()
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True)
assert 'Project formats: 1..11' in diagnose and 'JACK compiled:' in diagnose
assert 'Project formats: 1..12' in diagnose and 'JACK compiled:' in diagnose
# Replacing a file symlink must not overwrite an unrelated target.
protected = stage / 'unrelated-target'
protected.write_text('keep unrelated target\n')

View file

@ -0,0 +1,121 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_mixer.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <unistd.h>
#include <stdlib.h>
static int audio;
void *__real_malloc(size_t);
void *__real_calloc(size_t,size_t);
void *__real_realloc(void *,size_t);
void __real_free(void *);
void *__real_dlopen(const char *,int);
int __real_dlclose(void *);
void *__wrap_malloc(size_t n) { assert(!audio); return __real_malloc(n); }
void *__wrap_calloc(size_t n,size_t s) { assert(!audio); return __real_calloc(n,s); }
void *__wrap_realloc(void *p,size_t n) { assert(!audio); return __real_realloc(p,n); }
void __wrap_free(void *p) { assert(!audio); __real_free(p); }
void *__wrap_dlopen(const char *p,int mode) { assert(!audio); return __real_dlopen(p,mode); }
int __wrap_dlclose(void *p) { assert(!audio); return __real_dlclose(p); }
static xtc_result render(xtc_midi_plan *plan, uint64_t position, const xtc_midi_event *events, size_t count, float *out, size_t frames)
{
xtc_result r; audio = 1;
r = xtc_midi_plan_render(plan,position,events,count,out,frames);
audio = 0; return r;
}
static void check(const char *path)
{
xtc_project *p = NULL, *q = NULL;
xtc_midi_plan *plan = NULL;
xtc_plugin_spec spec, actual;
xtc_plugin *plugin = NULL;
uint64_t track, clip, note, start, end, live;
float out[600]; size_t i;
char file[256];
xtc_midi_event events[] = {{3,XTC_MIDI_NOTE_ON,0,60,127},{19,XTC_MIDI_NOTE_OFF,0,60,0}};
assert(xtc_plugin_probe(path,0,48000,&spec) == XTC_OK && spec.instrument && !spec.inputs);
assert(xtc_plugin_create(&spec,48000,256,&plugin) == XTC_OK);
assert(xtc_plugin_process_midi(plugin,events,2,out,31) == XTC_OK);
for (i = 0; i < 31; ++i) assert(out[i] == (i >= 3 && i < 19 ? 1.0f : 0));
xtc_plugin_destroy(plugin);
assert(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
assert(xtc_project_add_track(p,"Synth",&track) == XTC_OK);
assert(xtc_project_add_midi_clip(p,track,0,960,&clip) == XTC_OK);
assert(xtc_project_add_note(p,clip,2,10,60,127,0,&note) == XTC_OK);
assert(xtc_project_tick_to_sample(p,2,48000,&start) == XTC_OK);
assert(xtc_project_tick_to_sample(p,12,48000,&end) == XTC_OK);
assert(xtc_project_set_instrument(p,track,&spec) == XTC_OK);
assert(xtc_project_get_live_instrument(p,&live) == XTC_OK && live == track);
assert(xtc_midi_plan_create(p,48000,&plan) == XTC_OK);
assert(render(plan,0,NULL,0,out,600) == XTC_OK);
for (i = 0; i < 600; ++i) assert(fabsf(out[i]-(i >= start && i < end ? 1.0f : 0)) < .000001f);
/* Seek chases notes, panic suppresses them until the next discontinuity. */
assert(render(plan,start+1,NULL,0,out,1) == XTC_OK && out[0] == 1);
xtc_midi_plan_panic(plan,start+2);
assert(render(plan,start+2,NULL,0,out,1) == XTC_OK && out[0] == 0);
assert(render(plan,1000,events,2,out,31) == XTC_OK);
for (i = 0; i < 31; ++i) assert(out[i] == (i >= 3 && i < 19 ? 1.0f : 0));
xtc_midi_plan_destroy(plan);
snprintf(file,sizeof(file),"/tmp/xtc-instrument-%ld.xtc",(long)getpid());
assert(xtc_project_save(p,file) == XTC_OK);
assert(xtc_project_load(q,file) == XTC_OK);
assert(xtc_project_get_instrument(q,track,&actual) == XTC_OK && actual.instrument && !strcmp(actual.path,path));
assert(xtc_project_get_live_instrument(q,&live) == XTC_OK && live == track);
assert(xtc_midi_plan_create(q,48000,&plan) == XTC_OK);
xtc_midi_plan_destroy(plan);
assert(xtc_project_set_instrument(q,track,NULL) == XTC_OK);
assert(xtc_project_get_instrument(q,track,&actual) == XTC_ERR_NOT_FOUND);
assert(xtc_project_get_live_instrument(q,&live) == XTC_OK && live == UINT64_MAX);
/* Independent instances sum through track gains, and stable IDs survive reorder. */
{
uint64_t second, clip2, note2, bus;
assert(xtc_project_add_track(p,"Second",&second) == XTC_OK);
assert(xtc_project_add_midi_clip(p,second,0,960,&clip2) == XTC_OK);
assert(xtc_project_add_note(p,clip2,2,10,62,127,1,&note2) == XTC_OK);
assert(xtc_project_set_instrument(p,second,&spec) == XTC_OK);
assert(xtc_project_set_mix(p,second,.25f,0,0,0) == XTC_OK);
assert(xtc_project_move_track(p,second,0) == XTC_OK);
assert(xtc_project_get_instrument(p,track,&actual) == XTC_OK);
assert(xtc_midi_plan_create(p,48000,&plan) == XTC_OK);
assert(render(plan,0,NULL,0,out,600) == XTC_OK);
assert(fabsf(out[start]-1.25f) < .000001f);
xtc_midi_plan_destroy(plan);
assert(xtc_project_add_bus(p,"Synth bus",&bus) == XTC_OK);
assert(xtc_project_set_instrument(p,bus,&spec) == XTC_ERR_INVALID_STATE);
assert(xtc_project_route_track(p,second,bus) == XTC_OK);
assert(xtc_project_set_mix(p,bus,.5f,0,0,0) == XTC_OK);
assert(xtc_midi_plan_create(p,48000,&plan) == XTC_OK);
assert(render(plan,0,NULL,0,out,600) == XTC_OK && fabsf(out[start]-1.125f) < .000001f);
xtc_midi_plan_destroy(plan);
assert(xtc_project_set_mix(p,second,.25f,0,1,0) == XTC_OK);
assert(xtc_midi_plan_create(p,48000,&plan) == XTC_OK);
assert(render(plan,0,NULL,0,out,600) == XTC_OK && out[start] == 1);
xtc_midi_plan_destroy(plan);
assert(xtc_project_remove_track(p,second) == XTC_OK);
assert(xtc_project_get_live_instrument(p,&live) == XTC_OK && live == UINT64_MAX);
}
/* Exhausting a chunk's event budget fails silent and recovers on next render. */
{
xtc_midi_event many[256];
assert(xtc_project_set_live_instrument(p,track) == XTC_OK);
for (i = 0; i < 256; ++i) many[i] = (xtc_midi_event){0,XTC_MIDI_NOTE_ON,0,60,127};
assert(xtc_midi_plan_create(p,48000,&plan) == XTC_OK);
assert(render(plan,1000,many,256,out,31) == XTC_ERR_CAPACITY);
for (i = 0; i < 31; ++i) assert(out[i] == 0);
assert(render(plan,1031,events,2,out,31) == XTC_OK);
xtc_midi_plan_destroy(plan);
}
/* Missing modules remain model data; playback preparation fails cleanly. */
actual = spec; strcpy(actual.path,"/absent/xtc-instrument.so");
assert(xtc_project_set_instrument(q,track,&actual) == XTC_OK);
assert(xtc_project_save(q,file) == XTC_OK && xtc_project_load(p,file) == XTC_OK);
plan = NULL;
assert(xtc_midi_plan_create(p,48000,&plan) == XTC_ERR_NOT_FOUND && !plan);
xtc_project_destroy(q); xtc_project_destroy(p); unlink(file);
printf("%s instrument offsets, scheduling, live routing, seek/panic and v12 reload passed.\n",xtc_plugin_format_name(spec.format));
}
int main(int argc, char **argv)
{ int i; assert(argc == 4); for (i = 1; i < argc; ++i) check(argv[i]); assert(setenv("XTC_TEST_NATIVE_NOTES","1",1) == 0); check(argv[1]); return 0; }

View file

@ -19,7 +19,7 @@ int main(void)
assert(mkdtemp(root));
assert(xtc_project_create(&p) == XTC_OK);
snprintf(file,sizeof(file),"%s/migration.xtc",root);
for (version = 1; version <= 11; ++version) {
for (version = 1; version <= 12; ++version) {
xtc_track_info t; xtc_clip_info c; size_t notes, points;
snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version);
assert(xtc_project_load(p,old) == XTC_OK);
@ -89,6 +89,6 @@ int main(void)
assert(xtc_project_load(p,file) == XTC_ERR_FORMAT);
{ xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } }
xtc_project_destroy(p); unlink(file); rmdir(moved);
puts("v1-v11 migration, current folder relocation, cwd independence and missing-media preservation passed.");
puts("v1-v12 migration, current folder relocation, cwd independence and missing-media preservation passed.");
return 0;
}

View file

@ -65,8 +65,26 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
if number == 3:
project.write_text(modern.replace(identity.encode().hex(), b"bad-class-id".hex()))
check("--validate", project, 1)
# V12 track bindings retain references without executing module contents.
for number, identity in ((1, "org.xtc.instrument"), (2, ""),
(3, "0123456789abcdef0123456789abcdef")):
binding = (f"INSTRUMENT 0 {number} 0 42 {b'synth'.hex()}\n"
f"MODULE {str(module).encode().hex()}\n"
f"IDENTITY {identity.encode().hex() if identity else '-'}\n")
document = base.replace("XTC_PROJECT 10", "XTC_PROJECT 12").replace(
"END\n", "TRACK_INSTRUMENTS 1\n" + binding + "LIVE_INSTRUMENT 0\nEND\n")
project.write_text(document)
check("--inspect", project, 0, "instrument track 0", "no plugin code executed")
for malformed in (
document.replace("TRACK_INSTRUMENTS 1", "TRACK_INSTRUMENTS 2").replace(binding,binding+binding),
document.replace("INSTRUMENT 0 ", "INSTRUMENT 99 "),
document.replace("LIVE_INSTRUMENT 0", "LIVE_INSTRUMENT 99"),
document.replace(f"INSTRUMENT 0 {number}", "INSTRUMENT 0 0"),
):
project.write_text(malformed)
check("--validate", project, 1)
for text, messages in [
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..11")),
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..12")),
("XTC_PROJECT 10\n", ("line 2", "APP")),
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),

View file

@ -0,0 +1,100 @@
#include <clap/clap.h>
#include <clap/ext/thread-check.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
static int loaded, count;
static const char *features[] = {CLAP_PLUGIN_FEATURE_INSTRUMENT,NULL};
static const clap_plugin_descriptor_t descriptors[] = {
{CLAP_VERSION,"xtc.fixture.gain","CLAP gain","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.stereo","Stereo","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.init","Init failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.activate","Activate failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.start","Start failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.process","Process failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.restart","Restart request","xtc","","","","1","",features}
};
typedef struct { clap_plugin_t plugin; const clap_host_t *host; unsigned int index; int active, started; float level; } instance;
static bool init(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(loaded); return p->index != 2; }
static void destroy(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(!p->active && !p->started); --count; free(p); }
static bool activate(const clap_plugin_t *plugin, double rate, uint32_t min, uint32_t max)
{ instance *p = plugin->plugin_data; assert(rate > 0 && min && max >= min && !p->active); if (p->index == 3) return false; p->active = 1; return true; }
static void deactivate(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(p->active && !p->started); p->active = 0; }
static bool start(const clap_plugin_t *plugin)
{
instance *p = plugin->plugin_data;
const clap_host_thread_check_t *check = p->host->get_extension(p->host,CLAP_EXT_THREAD_CHECK);
assert(p->active && !p->started && check->is_audio_thread(p->host));
if (p->index == 4) return false;
p->started = 1; return true;
}
static void stop(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(p->started); p->started = 0; }
static void reset(const clap_plugin_t *plugin) { (void)plugin; }
static clap_process_status process(const clap_plugin_t *plugin, const clap_process_t *block)
{
instance *p = plugin->plugin_data; uint32_t i;
uint32_t j = 0, event_total = block->in_events->size(block->in_events);
assert(p->started && block->audio_inputs_count == 0 && block->audio_outputs_count == 1);
if (p->index == 5) return CLAP_PROCESS_ERROR;
for (i = 0; i < block->frames_count; ++i) {
while (j < event_total) {
const clap_event_header_t *h = block->in_events->get(block->in_events,j);
const clap_event_midi_t *m = (const clap_event_midi_t *)h;
if (h->time != i) break;
++j;
if (h->type != CLAP_EVENT_MIDI) {
const clap_event_note_t *note = (const clap_event_note_t *)h;
assert(h->type == CLAP_EVENT_NOTE_ON || h->type == CLAP_EVENT_NOTE_OFF || h->type == CLAP_EVENT_NOTE_CHOKE);
p->level = h->type == CLAP_EVENT_NOTE_ON ? (float)note->velocity : 0;
continue;
}
if ((m->data[0]&240) == 144) p->level = (float)m->data[2]/127;
else if ((m->data[0]&240) == 128 || ((m->data[0]&240) == 176 && m->data[1] == 120)) p->level = 0;
}
block->audio_outputs[0].data32[0][i] = p->level;
}
p->host->request_callback(p->host);
if (p->index == 6) p->host->request_restart(p->host);
return CLAP_PROCESS_CONTINUE;
}
static uint32_t port_count(const clap_plugin_t *plugin, bool input) { (void)plugin; return input ? 0 : 1; }
static bool port_get(const clap_plugin_t *plugin, uint32_t index, bool input, clap_audio_port_info_t *info)
{ instance *p = plugin->plugin_data; (void)input; if (index) return false; memset(info,0,sizeof(*info)); info->channel_count = p->index == 1 ? 2 : 1; return true; }
static uint32_t latency(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(p->active); return 0; }
static uint32_t note_count(const clap_plugin_t *p, bool input) { (void)p; return input ? 1 : 0; }
static bool note_get(const clap_plugin_t *p, uint32_t index, bool input, clap_note_port_info_t *info)
{ (void)p; if (index || !input) return false; memset(info,0,sizeof(*info)); info->supported_dialects = info->preferred_dialect = getenv("XTC_TEST_NATIVE_NOTES") ? CLAP_NOTE_DIALECT_CLAP : CLAP_NOTE_DIALECT_MIDI; return true; }
static const void *extension(const clap_plugin_t *plugin, const char *id)
{
static const clap_plugin_audio_ports_t ports = {port_count,port_get};
static const clap_plugin_latency_t delay = {latency}; (void)plugin;
static const clap_plugin_note_ports_t notes = {note_count,note_get};
if (!strcmp(id,CLAP_EXT_NOTE_PORTS)) return &notes;
if (!strcmp(id,CLAP_EXT_AUDIO_PORTS)) return &ports;
return !strcmp(id,CLAP_EXT_LATENCY) ? &delay : NULL;
}
static void on_main(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; const clap_host_thread_check_t *check = p->host->get_extension(p->host,CLAP_EXT_THREAD_CHECK); assert(check->is_main_thread(p->host)); }
static uint32_t plugin_count(const clap_plugin_factory_t *factory) { (void)factory; return 7; }
static const clap_plugin_descriptor_t *descriptor(const clap_plugin_factory_t *factory, uint32_t index)
{ (void)factory; return index < 7 ? descriptors+index : NULL; }
static const clap_plugin_t *create(const clap_plugin_factory_t *factory, const clap_host_t *host, const char *id)
{
unsigned int i; instance *p; (void)factory;
for (i = 0; i < 7 && strcmp(id,descriptors[i].id); ++i) {}
if (i == 7) return NULL;
p = calloc(1,sizeof(*p)); if (!p) return NULL;
p->host = host; p->index = i;
p->plugin = (clap_plugin_t){descriptors+i,p,init,destroy,activate,deactivate,start,stop,reset,process,extension,on_main};
++count; return &p->plugin;
}
static bool entry_init(const char *path) { (void)path; assert(!loaded); loaded = 1; return true; }
static void entry_exit(void) { assert(loaded && !count); loaded = 0; }
static const void *factory(const char *id)
{ static const clap_plugin_factory_t f = {plugin_count,descriptor,create}; return !strcmp(id,CLAP_PLUGIN_FACTORY_ID) ? &f : NULL; }
CLAP_EXPORT const clap_plugin_entry_t clap_entry = {CLAP_VERSION,entry_init,entry_exit,factory};

View file

@ -0,0 +1,45 @@
/* Original MIT ABI fixture; not built from or linked to a proprietary SDK. */
#include "../../src/plugin/vst2_abi.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
typedef struct { xtc_v2_effect effect; int open, active, started; float level; xtc_v2_midi events[256]; int count; } instance;
static intptr_t dispatch(xtc_v2_effect *e, int32_t op, int32_t index, intptr_t value, void *ptr, float option)
{
instance *p = (instance *)e; (void)index; (void)option;
switch (op) {
case 0: assert(!p->open); p->open = 1; break;
case 1: assert(p->open && !p->active && !p->started); free(p); break;
case 12: p->active = value != 0; break;
case 25: { xtc_v2_events *events = ptr; int i; assert(events->count <= 256); p->count = events->count; for (i = 0; i < p->count; ++i) p->events[i] = *events->events[i]; break; }
case 45: strcpy(ptr,"VST2 gain"); return 1;
case 71: assert(p->active); p->started = 1; break;
case 72: p->started = 0; break;
default: break;
}
return 0;
}
static void process(xtc_v2_effect *e, float **input, float **output, int32_t frames)
{ instance *p = (instance *)e; int32_t i, j = 0; assert(p->started && !input);
for (i = 0; i < frames; ++i) {
while (j < p->count && p->events[j].offset == i) {
unsigned char *m = p->events[j++].data;
if ((m[0]&240) == 144) p->level = (float)m[2]/127;
else if ((m[0]&240) == 128 || ((m[0]&240) == 176 && m[1] == 120)) p->level = 0;
}
output[0][i] = p->level;
} p->count = 0;
}
xtc_v2_effect *VSTPluginMain(xtc_v2_host host);
xtc_v2_effect *VSTPluginMain(xtc_v2_host host)
{
instance *p; assert(host(NULL,1,0,0,NULL,0) == 2400);
p = calloc(1,sizeof(*p)); if (!p) return NULL;
p->effect.magic = XTC_V2_MAGIC; p->effect.dispatch = dispatch;
p->effect.inputs = 0; p->effect.outputs = 1;
if (getenv("XTC_VST2_FIXTURE_LAYOUT")) p->effect.inputs = 2;
p->effect.flags = 16|256;
p->effect.unique_id = 0x58544332; p->effect.replace = process;
return &p->effect;
}

View file

@ -0,0 +1,88 @@
#include <stdbool.h>
#include <vst3_c_api.h>
#include <assert.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
static int loaded, instances;
static const Steinberg_TUID cid = SMTG_INLINE_UID(0x58544333,0x12345678,0x12345678,0x12345678);
typedef struct { Steinberg_Vst_IComponent component; Steinberg_Vst_IAudioProcessor processor; unsigned int refs; int initialized, active, started; float level; } instance;
static instance *from_processor(void *p) { return (instance *)((char *)p - offsetof(instance,processor)); }
static Steinberg_tresult cq(void *self, const Steinberg_TUID iid, void **out)
{
instance *p = self; *out = NULL;
if (!memcmp(iid,Steinberg_Vst_IAudioProcessor_iid,16)) *out = &p->processor;
else if (!memcmp(iid,Steinberg_Vst_IComponent_iid,16) || !memcmp(iid,Steinberg_FUnknown_iid,16)) *out = &p->component;
if (!*out) return -1;
++p->refs; return 0;
}
static Steinberg_uint32 add(void *self) { return ++((instance *)self)->refs; }
static Steinberg_uint32 release(void *self)
{ instance *p = self; unsigned int refs = --p->refs; if (!refs) { assert(!p->initialized); --instances; free(p); } return refs; }
static Steinberg_tresult pq(void *self, const Steinberg_TUID iid, void **out) { return cq(from_processor(self),iid,out); }
static Steinberg_uint32 pa(void *self) { return add(from_processor(self)); }
static Steinberg_uint32 pr(void *self) { return release(from_processor(self)); }
static Steinberg_tresult init(void *self, Steinberg_FUnknown *host)
{ instance *p = self; assert(loaded && host && !p->initialized); if (getenv("XTC_VST3_FIXTURE_INIT")) return -1; p->initialized = 1; return 0; }
static Steinberg_tresult terminate(void *self) { instance *p = self; assert(p->initialized && !p->active); p->initialized = 0; return 0; }
static Steinberg_tresult controller(void *self, Steinberg_TUID id) { (void)self; (void)id; return -1; }
static Steinberg_tresult mode(void *self, Steinberg_Vst_IoMode m) { (void)self; (void)m; return 0; }
static Steinberg_int32 buses(void *self, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir) { (void)self; (void)dir; return type == 0 ? (dir == 1 ? 1 : 0) : (dir == 0 ? 1 : 0); }
static Steinberg_tresult bus_info(void *self, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, struct Steinberg_Vst_BusInfo *info)
{ (void)self; (void)dir; assert(type == 0 && index == 0); memset(info,0,sizeof(*info)); info->channelCount = 1; return 0; }
static Steinberg_tresult routing(void *self, struct Steinberg_Vst_RoutingInfo *in, struct Steinberg_Vst_RoutingInfo *out)
{ (void)self; (void)in; (void)out; return -1; }
static Steinberg_tresult activate_bus(void *self, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, Steinberg_TBool state)
{ (void)self; (void)type; (void)dir; (void)index; (void)state; return 0; }
static Steinberg_tresult active(void *self, Steinberg_TBool state) { instance *p = self; assert(!p->started); p->active = state; return 0; }
static Steinberg_tresult state(void *self, Steinberg_IBStream *stream) { (void)self; (void)stream; return -1; }
static Steinberg_tresult arrangements(void *self, Steinberg_Vst_SpeakerArrangement *in, Steinberg_int32 ni, Steinberg_Vst_SpeakerArrangement *out, Steinberg_int32 no)
{ (void)self; return ni == 0 && !in && no == 1 && out[0] == Steinberg_Vst_SpeakerArr_kMono ? 0 : -1; }
static Steinberg_tresult arrangement(void *self, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, Steinberg_Vst_SpeakerArrangement *out)
{ (void)self; (void)dir; (void)index; *out = Steinberg_Vst_SpeakerArr_kMono; return 0; }
static Steinberg_tresult sample_size(void *self, Steinberg_int32 size) { (void)self; return size == 0 ? 0 : -1; }
static Steinberg_uint32 latency(void *self) { (void)self; return 0; }
static Steinberg_tresult setup(void *self, struct Steinberg_Vst_ProcessSetup *data) { (void)self; return data->maxSamplesPerBlock > 0 && data->sampleRate > 0 ? 0 : -1; }
static Steinberg_tresult processing(void *self, Steinberg_TBool value) { instance *p = from_processor(self); assert(p->active); p->started = value; return 0; }
static Steinberg_tresult process(void *self, struct Steinberg_Vst_ProcessData *data)
{ instance *p = from_processor(self); int32_t i, j = 0;
Steinberg_Vst_IEventList *events = data->inputEvents;
int32_t count = events->lpVtbl->getEventCount(events);
assert(p->started && data->numInputs == 0);
for (i = 0; i < data->numSamples; ++i) {
while (j < count) {
struct Steinberg_Vst_Event event;
assert(events->lpVtbl->getEvent(events,j,&event) == 0);
if (event.sampleOffset != i) break;
++j;
if (event.type == 0) p->level = event.Steinberg_Vst_Event_noteOn.velocity;
else if (event.type == 1) p->level = 0;
}
data->outputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32[0][i] = p->level;
} return 0;
}
static Steinberg_Vst_IComponentVtbl component_vtable = {cq,add,release,init,terminate,controller,mode,buses,bus_info,routing,activate_bus,active,state,state};
static Steinberg_Vst_IAudioProcessorVtbl processor_vtable = {pq,pa,pr,arrangements,arrangement,sample_size,latency,setup,processing,process,latency};
static Steinberg_tresult fq(void *self, const Steinberg_TUID iid, void **out) { (void)iid; *out = self; return 0; }
static Steinberg_uint32 fr(void *self) { (void)self; return 1; }
static Steinberg_tresult factory_info(void *self, struct Steinberg_PFactoryInfo *info) { (void)self; memset(info,0,sizeof(*info)); return 0; }
static Steinberg_int32 classes(void *self) { (void)self; return 1; }
static Steinberg_tresult class_info(void *self, Steinberg_int32 index, struct Steinberg_PClassInfo *info)
{ (void)self; if (index) return -1; memset(info,0,sizeof(*info)); memcpy(info->cid,cid,16); strcpy(info->category,"Audio Module Class"); strcpy(info->name,"VST3 gain"); return 0; }
static Steinberg_tresult create(void *self, Steinberg_FIDString id, Steinberg_FIDString iid, void **out)
{
instance *p; (void)self; *out = NULL;
if (memcmp(id,cid,16) || memcmp(iid,Steinberg_Vst_IComponent_iid,16)) return -1;
p = calloc(1,sizeof(*p)); if (!p) return -1;
p->component.lpVtbl = &component_vtable; p->processor.lpVtbl = &processor_vtable; p->refs = 1;
++instances; *out = p; return 0;
}
static Steinberg_IPluginFactoryVtbl factory_vtable = {fq,fr,fr,factory_info,classes,class_info,create};
static Steinberg_IPluginFactory factory = {&factory_vtable};
bool ModuleEntry(void *handle);
bool ModuleExit(void);
Steinberg_IPluginFactory *GetPluginFactory(void);
bool ModuleEntry(void *handle) { assert(handle && !loaded); loaded = 1; return true; }
bool ModuleExit(void) { assert(loaded && !instances); loaded = 0; return true; }
Steinberg_IPluginFactory *GetPluginFactory(void) { assert(loaded); return &factory; }

View file

@ -197,7 +197,7 @@ int main(void)
fputc('\n',legacy); continue;
}
if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) &&
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6) && strncmp(line,"CONTROL_MAPS ",13) && strncmp(line,"MAP ",4))
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6) && strncmp(line,"CONTROL_MAPS ",13) && strncmp(line,"MAP ",4) && strncmp(line,"TRACK_INSTRUMENTS ",18) && strncmp(line,"LIVE_INSTRUMENT ",16))
assert(fputs(line, legacy) >= 0);
}
assert(fclose(f) == 0 && fclose(legacy) == 0);

View file

@ -68,7 +68,7 @@ int main(void)
assert(xtc_project_add_track(old,"Old",&id) == XTC_OK && xtc_project_add_track(next,"New",&id) == XTC_OK);
assert(xtc_project_save(old,song) == XTC_OK); size = bytes(song,before);
assert(xtc_project_load_diagnostic(loaded,song,&diagnostic) == XTC_OK && diagnostic.result == XTC_OK &&
diagnostic.format_version == 11 && !diagnostic.line);
diagnostic.format_version == 12 && !diagnostic.line);
for (mode = 1; mode <= 5; ++mode) {
fault = mode;
assert(xtc_project_save(next,song) == XTC_ERR_IO);
@ -81,7 +81,7 @@ int main(void)
}
write_text(bad,"XTC_PROJECT 999\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 &&
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..11"));
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..12"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
!strcmp(diagnostic.record,"TEMPO"));