Phase 32: add MIDI track CLAP, VST2 and VST3 instruments
This commit is contained in:
parent
9e0e4562d7
commit
fe484537f9
41 changed files with 1138 additions and 65 deletions
21
Makefile
21
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
31
docs/MIDI.md
31
docs/MIDI.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
27
docs/decisions/0011-midi-track-instruments.md
Normal file
27
docs/decisions/0011-midi-track-instruments.md
Normal 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.
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
13
src/main.c
13
src/main.c
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,¬e) || !(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 = ∈ data.outputs = &out;
|
||||
data.numSamples = (Steinberg_int32)frames; data.numInputs = p->instrument ? 0 : 1; data.numOutputs = 1; data.inputs = p->instrument ? NULL : ∈ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
2
tests/fixtures/README.md
vendored
2
tests/fixtures/README.md
vendored
|
|
@ -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
41
tests/fixtures/project-v12.xtc
vendored
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
121
tests/integration/test_instruments.c
Normal file
121
tests/integration/test_instruments.c
Normal 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,¬e) == 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,¬e2) == 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; }
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",)),
|
||||
|
|
|
|||
100
tests/plugins/clap_synth_fixture.c
Normal file
100
tests/plugins/clap_synth_fixture.c
Normal 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 ¬es;
|
||||
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};
|
||||
45
tests/plugins/vst2_synth_fixture.c
Normal file
45
tests/plugins/vst2_synth_fixture.c
Normal 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;
|
||||
}
|
||||
88
tests/plugins/vst3_synth_fixture.c
Normal file
88
tests/plugins/vst3_synth_fixture.c
Normal 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; }
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue