From 8e09f589443ce91d9e895fc4556aae95ac10e87c Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 18:13:49 +0100 Subject: [PATCH] Added VST SDK support. --- Makefile | 32 ++++- README.md | 7 + docs/PLUGIN_HOSTING.md | 13 +- docs/ROADMAP.md | 77 +++++++++++ docs/STEINBERG.md | 57 ++++++++ examples/README.md | 5 + examples/steinberg/CMakeLists.txt | 52 ++++++++ examples/steinberg/README.md | 91 +++++++++++++ examples/steinberg/asio_devices.cpp | 81 ++++++++++++ examples/steinberg/render.c | 43 ++++++ examples/steinberg/sdk_plugins.cpp | 132 +++++++++++++++++++ include/xtc/xtc_plugin.h | 5 +- src/main.c | 15 ++- src/plugin/host.c | 14 +- src/plugin/modern.c | 29 +++- src/plugin/modern.h | 4 +- src/plugin/module_path.c | 40 ++++++ src/plugin/module_path.h | 6 + tests/integration/test_modern_plugins.c | 26 ++++ tests/integration/test_project_inspection.py | 13 ++ tests/integration/test_steinberg_sdk.c | 59 +++++++++ tests/plugins/vst3_fixture.c | 9 ++ tests/plugins/vst3_synth_fixture.c | 1 + tests/unit/test_plugin_module_path.c | 57 ++++++++ third_party/README.md | 5 + 25 files changed, 850 insertions(+), 23 deletions(-) create mode 100644 docs/STEINBERG.md create mode 100644 examples/steinberg/CMakeLists.txt create mode 100644 examples/steinberg/README.md create mode 100644 examples/steinberg/asio_devices.cpp create mode 100644 examples/steinberg/render.c create mode 100644 examples/steinberg/sdk_plugins.cpp create mode 100644 src/plugin/module_path.c create mode 100644 src/plugin/module_path.h create mode 100644 tests/integration/test_steinberg_sdk.c create mode 100644 tests/unit/test_plugin_module_path.c diff --git a/Makefile b/Makefile index 35ae36d..aba210f 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude LDFLAGS ?= -CORE_SOURCES := src/midi/file.c src/midi/output.c src/project/crossfade.c src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c +CORE_SOURCES := src/plugin/module_path.c src/midi/file.c src/midi/output.c src/project/crossfade.c src/control/tempo.c src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes) ifeq ($(JACK_AVAILABLE),yes) JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack) @@ -616,3 +616,33 @@ test-configuration-startup: gui .PHONY: test-gui-midi-labels test-gui-midi-labels: $(BUILD_DIR)/test_gui XTC_TEST_MIDI_LABELS_ONLY=1 xvfb-run -a -s "-screen 0 1440x960x24" ./$(BUILD_DIR)/test_gui + +# Optional external Steinberg SDK qualification. Normal builds never fetch SDKs. +VST3_SDK_ROOT ?= +STEINBERG_BUILD_DIR ?= $(BUILD_DIR)/steinberg-sdk +.PHONY: steinberg-sdk test-steinberg-sdk test-plugin-module-path +steinberg-sdk: + @test -n "$(VST3_SDK_ROOT)" || { echo "Set VST3_SDK_ROOT to an official recursive SDK checkout."; exit 1; } + cmake -S examples/steinberg -B "$(STEINBERG_BUILD_DIR)" -DXTC_VST3_SDK_ROOT="$(abspath $(VST3_SDK_ROOT))" -DCMAKE_BUILD_TYPE=Release + cmake --build "$(STEINBERG_BUILD_DIR)" --target xtc-sdk-examples validator --parallel 4 + +$(BUILD_DIR)/test_plugin_module_path: $(BUILD_DIR)/tests/unit/test_plugin_module_path.o $(BUILD_DIR)/libxtc.a + $(CC) $(LDFLAGS) -o $@ $^ -lm -ldl + +test-plugin-module-path: $(BUILD_DIR)/test_plugin_module_path + ./$(BUILD_DIR)/test_plugin_module_path + +test: test-plugin-module-path + +$(BUILD_DIR)/test_steinberg_sdk: $(BUILD_DIR)/tests/integration/test_steinberg_sdk.o $(BUILD_DIR)/libxtc.a + $(CC) $(LDFLAGS) -o $@ $^ -lm -ldl + +$(BUILD_DIR)/steinberg-render: $(BUILD_DIR)/examples/steinberg/render.o $(BUILD_DIR)/libxtc.a + $(CC) $(LDFLAGS) -o $@ $^ -lm -ldl + +# Linux bundle convention; native Windows SDK examples use CMake directly. +test-steinberg-sdk: steinberg-sdk $(BUILD_DIR)/test_steinberg_sdk $(BUILD_DIR)/steinberg-render + "$(STEINBERG_BUILD_DIR)/bin/Release/validator" "$(abspath $(STEINBERG_BUILD_DIR))/VST3/Release/xtc-sdk-examples.vst3" + ./$(BUILD_DIR)/test_steinberg_sdk "$(abspath $(STEINBERG_BUILD_DIR))/VST3/Release/xtc-sdk-examples.vst3" + +-include $(BUILD_DIR)/tests/unit/test_plugin_module_path.d $(BUILD_DIR)/tests/integration/test_steinberg_sdk.d $(BUILD_DIR)/examples/steinberg/render.d diff --git a/README.md b/README.md index 6511301..6dc8d13 100644 --- a/README.md +++ b/README.md @@ -194,3 +194,10 @@ JACK/PipeWire input/output choices, channel filtering and optional thru. Use Application preferences load from **`~/.xtc.conf`** at startup and save on Apply and clean exit. See [configuration and MIDI workflows](docs/MIDI_WORKFLOWS.md) for file support, routing and persistence details. + + +[Steinberg SDK integration (Phase 106)](docs/STEINBERG.md) adds conventional Linux +VST3 bundle loading, transport context, official-SDK gain/synth examples and +validator checks. Run `make VST3_SDK_ROOT=/path/to/vst3sdk test-steinberg-sdk` +for optional SDK qualification. A Windows ASIO device diagnostic is provided; +a native ASIO playback/capture backend remains planned. diff --git a/docs/PLUGIN_HOSTING.md b/docs/PLUGIN_HOSTING.md index 7fc6626..0fad541 100644 --- a/docs/PLUGIN_HOSTING.md +++ b/docs/PLUGIN_HOSTING.md @@ -233,9 +233,10 @@ parameters. Choose the actual binary: `.clap` for CLAP, a VST2 `.so` exporting `VSTPluginMain`, or a VST3 bundle's `Contents/-linux/.so` -exporting `ModuleEntry`, `ModuleExit` and `GetPluginFactory`. The chooser does -not resolve bundle directories or recursively scan plugin folders. On Linux this -avoids executing unrelated plugins just to discover one selected module. +exporting `ModuleEntry`, `ModuleExit` and `GetPluginFactory`. The C API also +accepts conventional `.vst3` bundle paths and resolves the matching architecture +and bundle-stem binary. The file chooser may require selecting the inner binary; +no recursive plugin-folder scan is performed. Auto-detection prefers CLAP, then VST2, then VST3, otherwise LADSPA; the C API also provides `xtc_plugin_probe_format` for an explicit format. @@ -312,3 +313,9 @@ 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. + + +Phase 106 adds [official SDK examples and qualification](STEINBERG.md), including +VST3 process context (sample position, rate, tempo and musical position), an +SDK-built gain/synth pair and a WAV render example. Native Windows ASIO remains +in progress; the optional ASIO SDK example currently provides device diagnostics. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6c6ad76..4c04a22 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -3363,6 +3363,83 @@ from software-port tests. No dependencies or project-format change were added. See [MIDI_WORKFLOWS.md](MIDI_WORKFLOWS.md), [PREFERENCES.md](PREFERENCES.md) and [the gallery](../screenshots/README.md) for reproduction and exact limits. +--- + +## Phase 106 — Steinberg VST SDK and ASIO Integration + +### Goal + +Use the official Steinberg SDKs to qualify and extend xtc's VST hosting and +prepare native Windows ASIO audio I/O, with runnable examples. Keep the C99 +engine and public C API; SDK-specific C++ belongs in optional tools/examples or +an isolated platform bridge. This is a staged phase, **in progress**, not a +claim that every VST extension or ASIO driver is supported. + +### Tasks and acceptance criteria + +- [x] Add an opt-in external VST3 SDK CMake build, official validator target, + and original SDK-built gain effect and MIDI instrument examples. Ordinary + builds must remain offline and SDK-independent. +- [x] Load conventional Linux VST3 bundles as well as inner binaries, preserving + the chosen bundle path in plugin metadata and existing module ownership. +- [x] Supply VST3 process context with sample position, sample rate, tempo and + musical position for effect and instrument processing, including seeks. +- [x] Provide a headless C example rendering the SDK synth through the gain + effect to a no-clobber WAV, and automated SDK/xtc interoperability checks. +- [ ] Qualify the ASIO SDK device/channel/format/latency diagnostic example on + native Windows with a real installed driver. Explicit driver selection; + no automatic installation, sample-rate changes or device routing. +- [ ] Add VST3 edit-controller lifecycle/connection handling, normalized + parameters, sample-accurate input automation and parameter feedback to + xtc's existing controls and undo paths; test with SDK and real plugins. +- [ ] Add bounded component/controller opaque-state persistence and transactional + reload, with relocation/missing-module and corrupt-state tests. +- [ ] Extend the graph/host contract for native stereo, multiple buses, + sidechains, latency compensation, tails and float64 negotiation; retain + explicit rejection until each layout is supported and tested. +- [ ] Embed native VST3 editors with Linux run-loop/resize ownership, close and + reopen tests, and main/audio-thread separation. Extend note expression, + MIDI mapping and process-context requirements without inventing timing. +- [ ] Extend legacy VST2 interoperability (parameters/state/editor/transport) + against user-supplied licensed plugins. The discontinued VST2 SDK is not + supplied by VST3 and must not be presented as a downloadable dependency. +- [ ] Introduce a native Windows ASIO backend behind the C backend interface: + explicit driver/device selection, buffer negotiation, integer/float sample + conversion, double buffers/time-info callbacks, channel mapping, latency, + reset/resync/rate-change recovery and callback quiescence. First establish + a Windows build of the C engine; do not pretend ASIO is a Linux backend. +- [ ] Qualify ASIO start/stop/reopen, playback/capture, MIDI coexistence, format + conversion and failure cleanup with deterministic driver tests and real + Windows hardware loopback; record SDK, OS, driver and hardware versions. +- [ ] Audit SDK licenses/notices and packaging for each enabled integration; + update examples, user documentation and the compatibility matrix after + each completed slice. No third-party plugin redistribution implied. + +### Implementation status + +The first slice adds optional SDK builds/examples, Linux bundle resolution and +VST3 transport context. See [STEINBERG.md](STEINBERG.md) and +[examples/steinberg](../examples/steinberg/README.md) for reproduction and exact +limits. The optional ASIO diagnostic source uses a separately supplied SDK; +Windows compilation and hardware validation remain pending. The application +still uses JACK/PipeWire on Linux. Controller/editor/opaque-state, richer bus +layouts and the native Windows backend remain open acceptance criteria above. + +### Validation of the initial slice + +On 2026-11-06, `make BUILD_DIR=build/steinberg-host all test` passed with +warnings enabled. The optional official-SDK target passed Steinberg validator +**94/94** plus xtc gain/synth, sample-offset and bundle-path save/reload checks +against SDK revision `3cdf9ca5d1f5b1b21e0a86832aa4abe55607bd96` (3.8.1). +Focused Clang ASan/UBSan module-path, modern-host and SDK interoperability tests +passed; static analysis of path resolution and the host facade reported no +findings. Independent WAV checks confirmed float32/mono/48 kHz, 96000 frames, +note-off silence and no-clobber destination preservation. Metadata inspection +accepts readable bundle binaries without executing them. Linux ASIO-option +configuration correctly rejects the unsupported platform; no Windows build or +ASIO hardware result is claimed. + + --- ## Recommended Execution Order diff --git a/docs/STEINBERG.md b/docs/STEINBERG.md new file mode 100644 index 0000000..3d8dbd7 --- /dev/null +++ b/docs/STEINBERG.md @@ -0,0 +1,57 @@ +# Steinberg SDK integration (Phase 106) + +The VST3 host uses Steinberg's pinned generated C ABI in the C99 core. Optional +SDK examples now also build against the full, separately supplied official C++ +SDK and run Steinberg's validator. This adds an independent interoperability +check without adding a C++ runtime or network dependency to libxtc. + +## Working integration + +- Linux `.vst3` bundles resolve to + `Contents/-linux/.so`. Direct `.so` paths still + work. The original chosen path remains the saved identity reference. This + supports conventional bundles, not arbitrary renamed modules or Windows DLLs. +- VST3 processing receives sample rate, absolute sample position, tempo and + musical position (`samples / rate * BPM / 60`). Position follows the existing + effect/instrument `_at` APIs, including seek. Tempo is the prepared plugin + spec's milli-BPM value; reprepare when it changes. Processing is marked playing + because this API represents active rendering, including offline rendering. + Loop, recording, wall-clock, time signature and continuous-time flags are not + asserted: the current plugin API does not carry that information. Positions + above `INT64_MAX` reject before processing, matching VST3's signed sample clock. +- Original SDK-built examples expose a mono half-gain effect and a mono, + 16-channel sine instrument. The instrument handles note-on/off at their exact + block offsets; the host translates all-notes-off to held-note releases. +- The C render example demonstrates the actual xtc plugin facade and WAV writer, + including no-clobber output, without requiring JACK or a GUI. + +See [the example instructions](../examples/steinberg/README.md) for SDK versions, +build commands, validation and ASIO diagnostics. + +## ASIO and VST2 scope + +ASIO is native driver I/O, separate from VST plugin hosting. The optional Windows +diagnostic enumerates drivers and inspects an explicitly selected driver's +channels, sample types, current sample rate, buffer bounds and reported latency. +It does not create/start audio buffers and is **not an xtc ASIO backend**. +The current xtc engine/backend still targets Linux/POSIX and JACK/PipeWire. +Windows portability, a C API platform bridge and native-driver qualification +are remaining Phase 106 work, not simulated by Linux tests. + +VST2 support retains the existing original minimal ABI declarations. VST3's SDK +does not include the discontinued VST2 SDK. No VST2 SDK or user plugin is copied, +fetched or licensed by these build targets. + +## Remaining work + +The application VST3 adapter still accepts one mono input (or no audio input for +instruments), one mono output, float32 and zero reported latency. Its two mono +instances are not a native stereo plugin instance. It uses plugin defaults; +controller parameters, opaque-state persistence and custom editor windows remain +unimplemented in xtc. The SDK examples include an edit-controller class for +standard SDK-host compatibility; xtc does not instantiate it yet. The examples +have no editable parameters or opaque state, so they do not prove those paths. +No VST extension support or general third-party compatibility is inferred from +the validator passing these examples. + +The complete acceptance checklist is in [Phase 106](ROADMAP.md#phase-106--steinberg-vst-sdk-and-asio-integration). diff --git a/examples/README.md b/examples/README.md index b4402ed..38d65f4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,3 +12,8 @@ listening acceptance. It never starts a backend or plays audio. `projects/midi-demo.xtc` is an original media/plugin-free MIDI example installed under `share/xtc/examples`. See projects/README.md for opening and playback. + +[Steinberg SDK examples](steinberg/README.md) build an official-SDK VST3 gain +and MIDI synth, qualify them with Steinberg's validator and the xtc C host, and +render a WAV offline. An optional Windows ASIO device diagnostic is also provided; +it is not yet a qualified audio backend. diff --git a/examples/steinberg/CMakeLists.txt b/examples/steinberg/CMakeLists.txt new file mode 100644 index 0000000..3c31f11 --- /dev/null +++ b/examples/steinberg/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.25) +project(xtc_steinberg_examples VERSION 1.0.0 LANGUAGES C CXX) +set(XTC_VST3_SDK_ROOT "" CACHE PATH "External official VST3 SDK checkout (with submodules)") +option(XTC_BUILD_ASIO_EXAMPLE "Build Windows ASIO device diagnostics" OFF) +set(XTC_ASIO_SDK_ROOT "" CACHE PATH "External ASIO SDK root") + +if(XTC_VST3_SDK_ROOT) + if(NOT EXISTS "${XTC_VST3_SDK_ROOT}/public.sdk/source/vst/vstaudioeffect.h") + message(FATAL_ERROR "XTC_VST3_SDK_ROOT needs the official SDK and its submodules") + endif() + # Never install/link example plugins into the user's plugin directories. + set(SMTG_CREATE_PLUGIN_LINK OFF CACHE BOOL "" FORCE) + set(SMTG_RUN_VST_VALIDATOR OFF CACHE BOOL "" FORCE) + set(SMTG_ENABLE_VSTGUI_SUPPORT OFF CACHE BOOL "" FORCE) + set(SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF CACHE BOOL "" FORCE) + set(SMTG_ENABLE_VST3_HOSTING_EXAMPLES ON CACHE BOOL "" FORCE) + add_subdirectory("${XTC_VST3_SDK_ROOT}" sdk) + set(public_sdk_SOURCE_DIR "${XTC_VST3_SDK_ROOT}/public.sdk") + smtg_add_vst3plugin(xtc-sdk-examples sdk_plugins.cpp) + target_link_libraries(xtc-sdk-examples PRIVATE sdk) + target_compile_features(xtc-sdk-examples PRIVATE cxx_std_17) + target_include_directories(xtc-sdk-examples PRIVATE "${XTC_VST3_SDK_ROOT}") + if(MSVC) + target_compile_options(xtc-sdk-examples PRIVATE /W4) + else() + target_compile_options(xtc-sdk-examples PRIVATE -Wall -Wextra -Wpedantic) + endif() +endif() + +if(XTC_BUILD_ASIO_EXAMPLE) + if(NOT WIN32) + message(FATAL_ERROR "The ASIO example needs Windows and an installed ASIO driver; Linux uses JACK/PipeWire") + endif() + foreach(source common/asio.cpp host/asiodrivers.cpp host/pc/asiolist.cpp) + if(NOT EXISTS "${XTC_ASIO_SDK_ROOT}/${source}") + message(FATAL_ERROR "XTC_ASIO_SDK_ROOT is missing ${source}") + endif() + endforeach() + add_executable(xtc-asio-devices asio_devices.cpp + "${XTC_ASIO_SDK_ROOT}/common/asio.cpp" + "${XTC_ASIO_SDK_ROOT}/host/asiodrivers.cpp" + "${XTC_ASIO_SDK_ROOT}/host/pc/asiolist.cpp") + target_include_directories(xtc-asio-devices PRIVATE + "${XTC_ASIO_SDK_ROOT}/common" "${XTC_ASIO_SDK_ROOT}/host" + "${XTC_ASIO_SDK_ROOT}/host/pc") + target_compile_features(xtc-asio-devices PRIVATE cxx_std_17) + target_link_libraries(xtc-asio-devices PRIVATE ole32 advapi32 uuid user32) +endif() + +if(NOT XTC_VST3_SDK_ROOT AND NOT XTC_BUILD_ASIO_EXAMPLE) + message(FATAL_ERROR "Set XTC_VST3_SDK_ROOT or enable XTC_BUILD_ASIO_EXAMPLE with XTC_ASIO_SDK_ROOT") +endif() diff --git a/examples/steinberg/README.md b/examples/steinberg/README.md new file mode 100644 index 0000000..b10d606 --- /dev/null +++ b/examples/steinberg/README.md @@ -0,0 +1,91 @@ +# Official Steinberg SDK examples + +These are optional integration examples. Normal `make` / `make test` uses the +existing pinned C ABI headers and never downloads SDKs. Original example source +is MIT-licensed like xtc. SDK code is compiled from an external checkout, not +vendored or installed by these targets. C++ is confined to the SDK examples; +`render.c` and the xtc host remain C99. + +## VST3 effect and instrument (Linux) + +Requirements: the normal C toolchain, C++17, CMake >= 3.25, and an official VST3 +SDK checkout with its submodules. SDK hosting utilities can require additional +platform development libraries (the inspected SDK discovers X11/GTK). Dependencies +are never installed automatically. Obtain the SDK explicitly, for example: + +```sh +git clone --recursive https://github.com/steinbergmedia/vst3sdk.git /your/path/vst3sdk +``` + +Qualified SDK revision: `3cdf9ca5d1f5b1b21e0a86832aa4abe55607bd96` +(3.8.1; MIT license). To reproduce this revision, checkout it and run +`git submodule update --init --recursive`. Review the license of the actual SDK +revision used; this example neither bundles SDK sources nor grants plugin or +trademark distribution rights. + +From the xtc repository root: + +```sh +make VST3_SDK_ROOT=/your/path/vst3sdk test-steinberg-sdk +``` + +This builds the original `xtc-sdk-examples.vst3` bundle using Steinberg's factory, +AudioEffect, controller and platform-entry implementations; runs the official +validator; and runs xtc's gain/MIDI interoperability checks. Build output stays +under `build/steinberg-sdk`; automatic plugin-directory links and installation +are disabled. SDK source may emit its own compiler warnings. + +The bundle contains: + +| Factory index | Class | Behavior | +| --- | --- | --- | +| 0 | xtc SDK Half Gain | mono float32 input multiplied by 0.5 | +| 1 | xtc SDK Sine | mono MIDI synth; 16 channels; note velocity; note-on/off offsets | +| 2 | xtc SDK Controller | non-audio class; no editable parameters | + +Both audio classes are zero-latency, stateless, default-configuration examples. +These make good host compatibility checks; they are not production instruments. +The sine example uses immediate note release and has no envelope or antialiasing. + +Create a two-second WAV (one second of A4 followed by silence) through the actual +xtc synth -> effect host path: + +```sh +./build/steinberg-render \ + "$PWD/build/steinberg-sdk/VST3/Release/xtc-sdk-examples.vst3" \ + /absolute/path/to/new-sdk-demo.wav +``` + +The destination must not exist. This executable is built by `test-steinberg-sdk`. +To use a different xtc build directory, pass `BUILD_DIR=...`; the SDK build then +lives under that directory unless `STEINBERG_BUILD_DIR=...` is explicitly set. + +The main app's existing plugin loader can use the bundle path through its C API; +its file chooser may require selecting the inner `.so`. The effect loads as a +track insert; the synth is available to the instrument descriptor workflow. +See [the hosting contract](../../docs/STEINBERG.md) for remaining limitations. + +## ASIO driver diagnostics (Windows, pending qualification) + +Supply the official ASIO SDK separately. The source layout targets the inspected +ASIO 2.3.3 SDK (`common/asio.cpp`, `host/asiodrivers.cpp`, `host/pc/asiolist.cpp`). +Its Steinberg SDK license differs from the VST3 revision above; follow the +license accompanying your SDK before redistribution. SDK source or binaries are +not included in xtc packages. A current SDK may require adapting its source layout. + +On native Windows with a matching MSVC toolchain and an installed ASIO driver: + +```powershell +cmake -S examples/steinberg -B build/asio-sdk -DXTC_BUILD_ASIO_EXAMPLE=ON -DXTC_ASIO_SDK_ROOT=C:/SDKs/asiosdk +cmake --build build/asio-sdk --config Release --target xtc-asio-devices +./build/asio-sdk/Release/xtc-asio-devices.exe +./build/asio-sdk/Release/xtc-asio-devices.exe --driver "Exact installed driver name" +``` + +The first invocation lists drivers registered for the executable's architecture. +The second opens only the selected driver, queries its existing channel formats, +sample rate, buffer constraints and latencies, then exits. It never calls +ASIOStart, ASIOCreateBuffers, ASIOSetSampleRate or the driver's control panel. +Linux configuration with this option deliberately fails with a platform message. +Windows compilation and real driver execution have **not** been verified here. +This is a diagnostic example, not audio playback/capture or an app backend. diff --git a/examples/steinberg/asio_devices.cpp b/examples/steinberg/asio_devices.cpp new file mode 100644 index 0000000..3c5ff04 --- /dev/null +++ b/examples/steinberg/asio_devices.cpp @@ -0,0 +1,81 @@ +// Original MIT-licensed diagnostics; build against a separately supplied ASIO SDK. +// This example inspects an explicitly selected driver. It never starts audio, +// changes sample rate, opens a control panel, or installs/registers a driver. +#include "asiosys.h" +#include "asio.h" +#include "asiodrivers.h" +#include +#include +#include + +// The SDK dispatch layer (including ASIOExit) uses this SDK-owned pointer. +extern AsioDrivers* asioDrivers; + +namespace { +struct Driver { + AsioDrivers drivers; + bool initialized = false; + Driver() { asioDrivers = &drivers; } + ~Driver() { + if (initialized) ASIOExit(); + drivers.removeCurrentDriver(); + asioDrivers = nullptr; + } +}; +} +int main(int argc, char** argv) { + if (argc != 1 && (argc != 3 || std::strcmp(argv[1], "--driver"))) { + std::fprintf(stderr, "Usage: xtc-asio-devices [--driver \"installed driver name\"]\n"); + return 2; + } + Driver session; + if (argc == 1) { + const long count = session.drivers.asioGetNumDev(); + if (count < 0) return 1; + for (long i = 0; i < count; ++i) { + std::array name{}; + if (session.drivers.asioGetDriverName(static_cast(i), name.data(), + static_cast(name.size())) != 0) return 1; + std::puts(name.data()); + } + if (!count) std::puts("No ASIO drivers registered for this executable's architecture."); + return 0; + } + // ASIO SDK uses char*, but does not retain or modify this argument. + if (!session.drivers.loadDriver(argv[2])) { + std::fprintf(stderr, "Cannot load the named ASIO driver.\n"); return 1; + } + ASIODriverInfo info{}; + info.asioVersion = 2; + info.sysRef = GetDesktopWindow(); + if (ASIOInit(&info) != ASE_OK) { + std::fprintf(stderr, "ASIO initialization failed: %.124s\n", info.errorMessage); return 1; + } + session.initialized = true; + long inputs = 0, outputs = 0, minimum = 0, maximum = 0, preferred = 0, granularity = 0; + long inputLatency = 0, outputLatency = 0; + ASIOSampleRate rate = 0; + if (ASIOGetChannels(&inputs, &outputs) != ASE_OK || + ASIOGetSampleRate(&rate) != ASE_OK || + ASIOGetBufferSize(&minimum, &maximum, &preferred, &granularity) != ASE_OK || + ASIOGetLatencies(&inputLatency, &outputLatency) != ASE_OK) { + std::fprintf(stderr, "ASIO format/status query failed.\n"); return 1; + } + std::printf("%.32s | version %ld\nrate %.0f Hz | %ld inputs / %ld outputs\n" + "buffer %ld..%ld frames, preferred %ld, granularity %ld\n" + "latency input %ld / output %ld frames\n", + info.name, info.driverVersion, rate, inputs, outputs, + minimum, maximum, preferred, granularity, inputLatency, outputLatency); + if (inputs < 0 || outputs < 0 || inputs > 1024 || outputs > 1024) return 1; + for (long direction = 0; direction < 2; ++direction) { + for (long i = 0; i < (direction ? outputs : inputs); ++i) { + ASIOChannelInfo channel{}; + channel.channel = i; + channel.isInput = direction ? ASIOFalse : ASIOTrue; + if (ASIOGetChannelInfo(&channel) != ASE_OK) return 1; + std::printf("%s %ld: %.32s | ASIO sample type %ld\n", + direction ? "output" : "input", i, channel.name, channel.type); + } + } + return 0; +} diff --git a/examples/steinberg/render.c b/examples/steinberg/render.c new file mode 100644 index 0000000..41ad53f --- /dev/null +++ b/examples/steinberg/render.c @@ -0,0 +1,43 @@ +/* Render the SDK sine -> half-gain examples without an audio server. */ +#include "xtc/xtc_plugin.h" +#include "xtc/xtc_audio.h" +#include +int main(int argc, char **argv) +{ + xtc_plugin_spec spec; + xtc_plugin *synth = NULL, *gain = NULL; + xtc_recorder *writer = NULL; + xtc_result result = XTC_ERR_INVALID_ARGUMENT; + size_t position; + float dry[256], wet[256]; + if (argc != 3) { + fprintf(stderr,"Usage: steinberg-render /absolute/xtc-sdk-examples.vst3 /absolute/new.wav\n"); + return 2; + } + result = xtc_plugin_probe_format(XTC_PLUGIN_VST3,argv[1],1,48000,&spec); + if (result == XTC_OK) result = xtc_plugin_create(&spec,48000,256,&synth); + if (result == XTC_OK) result = xtc_plugin_probe_format(XTC_PLUGIN_VST3,argv[1],0,48000,&spec); + if (result == XTC_OK) result = xtc_plugin_create(&spec,48000,256,&gain); + if (result == XTC_OK) result = xtc_recorder_create(argv[2],48000,512,&writer); + for (position = 0; result == XTC_OK && position < 96000; position += 256) { + size_t frames = 96000-position < 256 ? 96000-position : 256; + xtc_midi_event event = {0}; + size_t count = 0; + event.channel = 0; event.pitch = 69; event.velocity = 100; + if (!position) { event.type = XTC_MIDI_NOTE_ON; count = 1; } + if (position <= 48000 && position+frames > 48000) { + event.type = XTC_MIDI_NOTE_OFF; event.offset = (unsigned int)(48000-position); count = 1; + } + result = xtc_plugin_process_midi_at(synth,&event,count,dry,frames,position); + if (result == XTC_OK) result = xtc_plugin_process_at(gain,dry,wet,frames,position); + if (result == XTC_OK) { + xtc_recorder_push(writer,wet,frames); + result = xtc_recorder_drain(writer); + } + } + if (result == XTC_OK) result = xtc_recorder_finish(writer); + xtc_recorder_destroy(writer); xtc_plugin_destroy(gain); xtc_plugin_destroy(synth); + if (result != XTC_OK) { fprintf(stderr,"SDK example render failed (%d).\n",(int)result); return 1; } + puts("Rendered 2 seconds at 48 kHz: A4 for 1 second, then silence."); + return 0; +} diff --git a/examples/steinberg/sdk_plugins.cpp b/examples/steinberg/sdk_plugins.cpp new file mode 100644 index 0000000..6024cdb --- /dev/null +++ b/examples/steinberg/sdk_plugins.cpp @@ -0,0 +1,132 @@ +// Original MIT-licensed xtc examples using an external official Steinberg SDK. +// C++ is confined to this optional SDK example, never linked into libxtc. +#include "public.sdk/source/vst/vstaudioeffect.h" +#include "public.sdk/source/vst/vsteditcontroller.h" +#include "public.sdk/source/main/pluginfactory.h" +#include "pluginterfaces/base/ibstream.h" +#include "pluginterfaces/vst/ivstevents.h" +#include +#include + +namespace { +using namespace Steinberg; +using namespace Steinberg::Vst; + +const FUID controllerID(0x58544343,0x4F4E5431,0x98561234,0xABCD0003); +class Controller final : public EditController { +public: + static FUnknown* create(void*) { return static_cast(new Controller); } + tresult PLUGIN_API setComponentState(IBStream* stream) override { return stream ? kResultOk : kInvalidArgument; } +}; + +class Gain final : public AudioEffect { +public: + Gain() { setControllerClass(controllerID); } + static FUnknown* create(void*) { return static_cast(new Gain); } + tresult PLUGIN_API initialize(FUnknown* host) override { + const auto result = AudioEffect::initialize(host); + if (result != kResultOk) return result; + addAudioInput(STR16("Input"), SpeakerArr::kMono); + addAudioOutput(STR16("Output"), SpeakerArr::kMono); + return kResultOk; + } + tresult PLUGIN_API setBusArrangements(SpeakerArrangement* in, int32 ni, + SpeakerArrangement* out, int32 no) override { + if (ni != 1 || no != 1 || !in || !out || in[0] != SpeakerArr::kMono || out[0] != SpeakerArr::kMono) + return kResultFalse; + return AudioEffect::setBusArrangements(in, ni, out, no); + } + tresult PLUGIN_API canProcessSampleSize(int32 size) override { + return size == kSample32 ? kResultOk : kResultFalse; + } + tresult PLUGIN_API setProcessing(TBool) override { return kResultOk; } + tresult PLUGIN_API process(ProcessData& data) override { + if (data.symbolicSampleSize != kSample32 || data.numSamples < 0) return kInvalidArgument; + if (!data.numSamples) return kResultOk; + if (data.numInputs != 1 || data.numOutputs != 1 || !data.inputs || !data.outputs || + data.inputs[0].numChannels != 1 || data.outputs[0].numChannels != 1 || + !data.inputs[0].channelBuffers32 || !data.outputs[0].channelBuffers32 || + !data.inputs[0].channelBuffers32[0] || !data.outputs[0].channelBuffers32[0]) return kInvalidArgument; + for (int32 i = 0; i < data.numSamples; ++i) + data.outputs[0].channelBuffers32[0][i] = data.inputs[0].channelBuffers32[0][i] * .5f; + data.outputs[0].silenceFlags = data.inputs[0].silenceFlags; + return kResultOk; + } + // Stateless examples still implement the SDK's stream contract. + tresult PLUGIN_API setState(IBStream* stream) override { return stream ? kResultOk : kInvalidArgument; } + tresult PLUGIN_API getState(IBStream* stream) override { return stream ? kResultOk : kInvalidArgument; } +}; + +class Sine final : public AudioEffect { + struct Voice { double phase = 0; float velocity = 0; }; + std::array, 16> voices{}; + void event(const Event& e) { + if (e.type == Event::kNoteOnEvent && e.noteOn.channel >= 0 && e.noteOn.channel < 16 && + e.noteOn.pitch >= 0 && e.noteOn.pitch < 128) { + auto& v = voices[e.noteOn.channel][e.noteOn.pitch]; + v.phase = 0; v.velocity = e.noteOn.velocity; + } else if (e.type == Event::kNoteOffEvent && e.noteOff.channel >= 0 && e.noteOff.channel < 16 && + e.noteOff.pitch >= 0 && e.noteOff.pitch < 128) + voices[e.noteOff.channel][e.noteOff.pitch].velocity = 0; + } +public: + Sine() { setControllerClass(controllerID); } + static FUnknown* create(void*) { return static_cast(new Sine); } + tresult PLUGIN_API initialize(FUnknown* host) override { + const auto result = AudioEffect::initialize(host); + if (result != kResultOk) return result; + addEventInput(STR16("Notes"), 16); + addAudioOutput(STR16("Output"), SpeakerArr::kMono); + return kResultOk; + } + tresult PLUGIN_API setBusArrangements(SpeakerArrangement*, int32 ni, + SpeakerArrangement* out, int32 no) override { + if (ni != 0 || no != 1 || !out || out[0] != SpeakerArr::kMono) return kResultFalse; + return kResultOk; + } + tresult PLUGIN_API canProcessSampleSize(int32 size) override { return size == kSample32 ? kResultOk : kResultFalse; } + tresult PLUGIN_API setProcessing(TBool) override { voices = {}; return kResultOk; } + tresult PLUGIN_API process(ProcessData& data) override { + if (data.symbolicSampleSize != kSample32 || data.numSamples < 0) return kInvalidArgument; + if (!data.numSamples) return kResultOk; + if (data.numOutputs != 1 || !data.outputs || data.outputs[0].numChannels != 1 || + !data.outputs[0].channelBuffers32 || !data.outputs[0].channelBuffers32[0]) return kInvalidArgument; + int32 index = 0; + const int32 count = data.inputEvents ? data.inputEvents->getEventCount() : 0; + Event next{}; + bool pending = index < count && data.inputEvents->getEvent(index, next) == kResultOk; + bool silent = true; + for (int32 i = 0; i < data.numSamples; ++i) { + while (pending && next.sampleOffset <= i) { + event(next); ++index; + pending = index < count && data.inputEvents->getEvent(index, next) == kResultOk; + } + double value = 0; + for (auto& channel : voices) for (size_t key = 0; key < channel.size(); ++key) { + auto& v = channel[key]; + if (v.velocity == 0) continue; + value += std::sin(v.phase) * v.velocity * .1; + v.phase += 6.283185307179586 * 440.0 * std::pow(2.0, (static_cast(key)-69.0)/12.0) / processSetup.sampleRate; + v.phase = std::fmod(v.phase, 6.283185307179586); + } + data.outputs[0].channelBuffers32[0][i] = static_cast(std::tanh(value)); + if (value != 0) silent = false; + } + data.outputs[0].silenceFlags = silent ? 1 : 0; + return kResultOk; + } + tresult PLUGIN_API setState(IBStream* stream) override { return stream ? kResultOk : kInvalidArgument; } + tresult PLUGIN_API getState(IBStream* stream) override { return stream ? kResultOk : kInvalidArgument; } +}; +} +BEGIN_FACTORY("xtc", "", "", 0) +DEF_CLASS2(INLINE_UID(0x58544347,0x41494E31,0x98561234,0xABCD0001), + PClassInfo::kManyInstances, kVstAudioEffectClass, "xtc SDK Half Gain", 0, + "Fx", "1.0.0", kVstVersionString, Gain::create) +DEF_CLASS2(INLINE_UID(0x58544353,0x494E4531,0x98561234,0xABCD0002), + PClassInfo::kManyInstances, kVstAudioEffectClass, "xtc SDK Sine", 0, + "Instrument|Synth", "1.0.0", kVstVersionString, Sine::create) +DEF_CLASS2(INLINE_UID(0x58544343,0x4F4E5431,0x98561234,0xABCD0003), + PClassInfo::kManyInstances, kVstComponentControllerClass, "xtc SDK Controller", 0, + "", "1.0.0", kVstVersionString, Controller::create) +END_FACTORY diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index 96616cb..206b109 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -84,7 +84,7 @@ typedef struct xtc_plugin xtc_plugin; /* Serialized control/main thread: service CLAP callback requests. */ void xtc_plugin_idle(void); /* Serialized main/control thread only (also create/destroy/idle). Probe explicitly - * enumerates a Linux binary file by descriptor index; + * enumerates a Linux binary or conventional .vst3 bundle by descriptor index; * NOT_FOUND means end, UNSUPPORTED means descriptor/layout unsupported. Loading * trusted libraries can execute arbitrary constructors. Never probe in audio. */ xtc_result xtc_plugin_probe_format(xtc_plugin_format format, const char *path, unsigned int index, double rate, xtc_plugin_spec *out); @@ -106,7 +106,8 @@ void xtc_plugin_destroy(xtc_plugin *plugin); /* control, after quiescence */ * 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. */ -/* Absolute sample position supports instrument parameter automation and seek. */ +/* Absolute sample position supports instrument parameter automation and seek. + * VST3 receives this clock plus the prepared spec tempo in its process context. */ xtc_result xtc_plugin_process_midi_at(xtc_plugin *plugin, const xtc_midi_event *events, size_t count, float *output, size_t frames, uint64_t position); /* Control-thread preset files use a versioned single-instrument project container. * Load is transactional, validates metadata and never loads executable modules. */ diff --git a/src/main.c b/src/main.c index d72683b..6eb955b 100644 --- a/src/main.c +++ b/src/main.c @@ -13,6 +13,7 @@ #include #include #include "xtc/xtc_plugin.h" +#include "plugin/module_path.h" /* Dependency inspection checks readable regular files, never plugin code or * audio contents. Escape untrusted path/label bytes in terminal output. */ @@ -32,6 +33,14 @@ static int readable_file(const char *path) struct stat info; return stat(path,&info) == 0 && S_ISREG(info.st_mode) && access(path,R_OK) == 0; } +static int readable_plugin(const xtc_plugin_spec *spec) +{ + char binary[512]; + if (spec->format == XTC_PLUGIN_BUILTIN) return 1; + if (spec->format == XTC_PLUGIN_VST3) + return xtc_plugin_module_path(spec->path,binary) == XTC_OK && readable_file(binary); + return readable_file(spec->path); +} static int inspect(const xtc_project *project, uint64_t version) { size_t tracks = 0, clips = 0, nodes = 0, edges = 0, i, missing = 0; @@ -54,7 +63,7 @@ static int inspect(const xtc_project *project, uint64_t version) xtc_node_info node; xtc_plugin_spec spec; xtc_graph_get_node(graph,i,&node); if (xtc_graph_get_plugin(graph,node.id,&spec) == XTC_OK) { - int available = spec.format == XTC_PLUGIN_BUILTIN || readable_file(spec.path); + int available = readable_plugin(&spec); printf("%s node %zu ID %u descriptor %u ",xtc_plugin_format_name(spec.format),node.id,spec.unique_id,spec.index); if (spec.identifier[0]) { quoted(spec.identifier); putchar(' '); } quoted(spec.label); printf(": %s ",available ? "available" : "missing/unreadable"); @@ -66,7 +75,7 @@ static int inspect(const xtc_project *project, uint64_t version) 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 = spec.format == XTC_PLUGIN_BUILTIN || readable_file(spec.path); + int available = readable_plugin(&spec); 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'); @@ -85,7 +94,7 @@ static int inspect(const xtc_project *project, uint64_t version) printf("Track %" PRIu64 " insert chain: %zu slots, output gain %.6g\n",track.id,chain.count,(double)chain.output_gain); for(slot=0;slotplugin; - int available=spec->format==XTC_PLUGIN_BUILTIN || readable_file(spec->path); + int available=readable_plugin(spec); printf("%s insert track %" PRIu64 " slot %zu %s wet %.6g ",xtc_plugin_format_name(spec->format),track.id,slot+1,insert->enabled ? "enabled" : "bypassed",(double)insert->wet); quoted(spec->label);printf(" %s: ",available ? "available" : "MISSING");quoted(spec->path);putchar('\n'); if(!available)++missing; diff --git a/src/plugin/host.c b/src/plugin/host.c index ab82239..7950e75 100644 --- a/src/plugin/host.c +++ b/src/plugin/host.c @@ -1,4 +1,5 @@ #include "modern.h" +#include "module_path.h" #include "builtin.h" #include #include @@ -11,7 +12,7 @@ xtc_result xtc_ladspa_describe(const xtc_plugin_spec *, double, xtc_plugin_detai xtc_result xtc_ladspa_create(const xtc_plugin_spec *, double, size_t, xtc_ladspa_plugin **); void xtc_ladspa_destroy(xtc_ladspa_plugin *); xtc_result xtc_ladspa_process_at(xtc_ladspa_plugin *, const float *, float *, size_t, uint64_t); -struct xtc_plugin { xtc_ladspa_plugin *ladspa; xtc_modern *modern; xtc_builtin *builtin; uint64_t next; }; +struct xtc_plugin { xtc_ladspa_plugin *ladspa; xtc_modern *modern; xtc_builtin *builtin; uint64_t next; unsigned int tempo; }; const char *xtc_plugin_format_name(xtc_plugin_format f) { static const char *names[] = {"LADSPA", "CLAP", "VST2", "VST3", "Internal"}; return f >= 0 && f <= XTC_PLUGIN_BUILTIN ? names[f] : "Unknown"; } xtc_result xtc_plugin_probe_format(xtc_plugin_format f, const char *path, unsigned int index, double rate, xtc_plugin_spec *out) @@ -28,9 +29,12 @@ xtc_result xtc_plugin_probe_format(xtc_plugin_format f, const char *path, unsign xtc_result xtc_plugin_probe(const char *path, unsigned int index, double rate, xtc_plugin_spec *out) { void *module; xtc_plugin_format f = XTC_PLUGIN_LADSPA; + char binary[512]; xtc_result r; if (!path || path[0] != '/' || strlen(path) >= 512) return XTC_ERR_INVALID_ARGUMENT; if (!strcmp(path,XTC_BUILTIN_PATH)) return xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,path,index,rate,out); - module = dlopen(path,RTLD_NOW | RTLD_LOCAL); + r = xtc_plugin_module_path(path,binary); + if (r != XTC_OK) return r; + module = dlopen(binary,RTLD_NOW | RTLD_LOCAL); if (!module) return XTC_ERR_NOT_FOUND; if (dlsym(module,"clap_entry")) f = XTC_PLUGIN_CLAP; else if (dlsym(module,"VSTPluginMain")) f = XTC_PLUGIN_VST2; @@ -74,7 +78,7 @@ xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frame strcmp(actual.identifier,s->identifier) || strcmp(actual.label,s->label))) r = XTC_ERR_UNSUPPORTED; } if (r != XTC_OK) { xtc_plugin_destroy(p); return r; } - *out = p; return XTC_OK; + p->tempo = s->tempo; *out = p; return XTC_OK; } xtc_result xtc_plugin_process_stereo_at(xtc_plugin *p,const float *left,const float *right,float *out,float *out_right,size_t frames,uint64_t position) { @@ -87,7 +91,7 @@ xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *in, float *out, siz { xtc_result r; if (!p) return XTC_ERR_INVALID_ARGUMENT; - r = p->builtin ? xtc_builtin_process(p->builtin,in,out,frames,position) : p->modern ? xtc_modern_process(p->modern,in,out,frames) : xtc_ladspa_process_at(p->ladspa,in,out,frames,position); + r = p->builtin ? xtc_builtin_process(p->builtin,in,out,frames,position) : p->modern ? xtc_modern_process(p->modern,in,out,frames,position,p->tempo) : xtc_ladspa_process_at(p->ladspa,in,out,frames,position); if (r == XTC_OK) p->next = position <= UINT64_MAX-frames ? position+frames : 0; return r; } @@ -95,7 +99,7 @@ xtc_result xtc_plugin_process_midi_at(xtc_plugin *p, const xtc_midi_event *event { xtc_result r; if (!p || position>UINT64_MAX-frames) return XTC_ERR_INVALID_ARGUMENT; - r=p->builtin ? xtc_builtin_midi(p->builtin,events,count,out,frames,position) : p->modern ? xtc_modern_process_midi(p->modern,events,count,out,frames) : XTC_ERR_UNSUPPORTED; + r=p->builtin ? xtc_builtin_midi(p->builtin,events,count,out,frames,position) : p->modern ? xtc_modern_process_midi(p->modern,events,count,out,frames,position,p->tempo) : XTC_ERR_UNSUPPORTED; if(r==XTC_OK)p->next=position+frames; return r; } diff --git a/src/plugin/modern.c b/src/plugin/modern.c index 013f606..3c2431a 100644 --- a/src/plugin/modern.c +++ b/src/plugin/modern.c @@ -1,5 +1,6 @@ #define _POSIX_C_SOURCE 200809L #include "modern.h" +#include "module_path.h" #include "vst2_abi.h" #include "xtc/xtc_dsp.h" #include "xtc/xtc_version.h" @@ -86,8 +87,11 @@ static void module_release(module *m) } static xtc_result module_open(xtc_plugin_format format, const char *path, module **out) { - void *handle = dlopen(path,RTLD_NOW | RTLD_LOCAL); - module *m; xtc_result r = XTC_ERR_UNSUPPORTED; + char binary[512]; void *handle; + module *m; xtc_result r = xtc_plugin_module_path(path,binary); + if (r != XTC_OK) return r; + handle = dlopen(binary,RTLD_NOW | RTLD_LOCAL); + r = XTC_ERR_UNSUPPORTED; if (!handle) return XTC_ERR_NOT_FOUND; for (m = modules; m; m = m->next) if (m->handle == handle && m->format == format) { ++m->references; dlclose(handle); *out = m; return XTC_OK; @@ -367,10 +371,11 @@ void xtc_modern_destroy(xtc_modern *p) if (p->factory && p->factory->lpVtbl && p->factory->lpVtbl->release) p->factory->lpVtbl->release(p->factory); module_release(p->module); free(p->input); free(p->output); free(p); } -xtc_result xtc_modern_process(xtc_modern *p, const float *input, float *output, size_t frames) +xtc_result xtc_modern_process(xtc_modern *p, const float *input, float *output, size_t frames, uint64_t position, unsigned int tempo) { size_t i; int ok = 1; - if (!p || !output || frames > p->frames) return XTC_ERR_INVALID_ARGUMENT; + if (!p || !output || frames > p->frames || position > INT64_MAX || + tempo < 20000 || tempo > 300000) return XTC_ERR_INVALID_ARGUMENT; if (!frames) return XTC_OK; if (__atomic_load_n(&p->restart,__ATOMIC_ACQUIRE)) { memset(output,0,frames*sizeof(float)); return XTC_ERR_INVALID_STATE; } for (i = 0; i < frames; ++i) p->input[i] = input ? xtc_dsp_finite(input[i]) : 0; @@ -396,6 +401,15 @@ xtc_result xtc_modern_process(xtc_modern *p, const float *input, float *output, else { struct Steinberg_Vst_AudioBusBuffers in = {0}, out = {0}; struct Steinberg_Vst_ProcessData data = {0}; + struct Steinberg_Vst_ProcessContext context = {0}; + context.state = Steinberg_Vst_ProcessContext_StatesAndFlags_kPlaying | + Steinberg_Vst_ProcessContext_StatesAndFlags_kTempoValid | + Steinberg_Vst_ProcessContext_StatesAndFlags_kProjectTimeMusicValid; + context.sampleRate = p->rate; + context.projectTimeSamples = (Steinberg_Vst_TSamples)position; + context.tempo = (double)tempo / 1000.0; + context.projectTimeMusic = (double)position / p->rate * context.tempo / 60.0; + data.processContext = &context; in.numChannels = out.numChannels = 1; in.Steinberg_Vst_AudioBusBuffers_channelBuffers32 = &p->input; out.Steinberg_Vst_AudioBusBuffers_channelBuffers32 = &p->output; @@ -443,10 +457,11 @@ static void vst_note(xtc_modern *p, unsigned int offset, unsigned int channel, u } 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) +xtc_result xtc_modern_process_midi(xtc_modern *p, const xtc_midi_event *events, size_t count, float *output, size_t frames, uint64_t position, unsigned int tempo) { 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; + if (!p || !p->instrument || !output || (count && !events) || count > XTC_MIDI_MAX_EVENTS || frames > p->frames || + position > INT64_MAX || tempo < 20000 || tempo > 300000) 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 || @@ -482,6 +497,6 @@ xtc_result xtc_modern_process_midi(xtc_modern *p, const xtc_midi_event *events, 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); + r = xtc_modern_process(p,NULL,output,frames,position,tempo); p->event_count = 0; p->clap_count = 0; p->vst_count = 0; return r; } diff --git a/src/plugin/modern.h b/src/plugin/modern.h index 9d017be..4ff9bf2 100644 --- a/src/plugin/modern.h +++ b/src/plugin/modern.h @@ -3,7 +3,7 @@ #include "xtc/xtc_plugin.h" 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); +xtc_result xtc_modern_process(xtc_modern *plugin, const float *input, float *output, size_t frames, uint64_t position, unsigned int tempo); +xtc_result xtc_modern_process_midi(xtc_modern *plugin, const xtc_midi_event *events, size_t count, float *output, size_t frames, uint64_t position, unsigned int tempo); void xtc_modern_destroy(xtc_modern *plugin); #endif diff --git a/src/plugin/module_path.c b/src/plugin/module_path.c new file mode 100644 index 0000000..0aaff4c --- /dev/null +++ b/src/plugin/module_path.c @@ -0,0 +1,40 @@ +#include "module_path.h" +#include +#include +#include + +xtc_result xtc_plugin_module_path(const char *path, char out[512]) +{ + struct stat status; + char resolved[512]; + const char *name; + size_t length, stem; + int count; + if (!path || !out || path[0] != '/' || (length = strlen(path)) >= sizeof(resolved)) + return XTC_ERR_INVALID_ARGUMENT; + if (stat(path, &status)) return XTC_ERR_NOT_FOUND; + if (S_ISREG(status.st_mode)) { memcpy(out, path, length + 1); return XTC_OK; } + if (!S_ISDIR(status.st_mode)) return XTC_ERR_UNSUPPORTED; + name = strrchr(path, '/') + 1; + stem = strlen(name); + if (stem <= 5 || strcmp(name + stem - 5, ".vst3")) return XTC_ERR_UNSUPPORTED; +#if defined(__x86_64__) +#define XTC_VST3_ARCH "x86_64-linux" +#elif defined(__aarch64__) +#define XTC_VST3_ARCH "aarch64-linux" +#elif defined(__i386__) +#define XTC_VST3_ARCH "i386-linux" +#elif defined(__arm__) +#define XTC_VST3_ARCH "arm-linux" +#else + return XTC_ERR_UNSUPPORTED; +#define XTC_VST3_ARCH "unsupported" +#endif + count = snprintf(resolved, sizeof(resolved), "%s/Contents/" XTC_VST3_ARCH "/%.*s.so", + path, (int)(stem - 5), name); + if (count < 0 || (size_t)count >= sizeof(resolved)) return XTC_ERR_INVALID_ARGUMENT; + if (stat(resolved, &status)) return XTC_ERR_NOT_FOUND; + if (!S_ISREG(status.st_mode)) return XTC_ERR_UNSUPPORTED; + memcpy(out, resolved, (size_t)count + 1); + return XTC_OK; +} diff --git a/src/plugin/module_path.h b/src/plugin/module_path.h new file mode 100644 index 0000000..3f4997b --- /dev/null +++ b/src/plugin/module_path.h @@ -0,0 +1,6 @@ +#ifndef XTC_PLUGIN_MODULE_PATH_H +#define XTC_PLUGIN_MODULE_PATH_H +#include "xtc/xtc.h" +/* Control-thread Linux binary/bundle resolution. Preserves out on error. */ +xtc_result xtc_plugin_module_path(const char *path, char out[512]); +#endif diff --git a/tests/integration/test_modern_plugins.c b/tests/integration/test_modern_plugins.c index 6b8fb45..6171dff 100644 --- a/tests/integration/test_modern_plugins.c +++ b/tests/integration/test_modern_plugins.c @@ -87,10 +87,36 @@ static void check(const char *path, xtc_plugin_format format) xtc_realtime_destroy(rt); xtc_graph_destroy(graph); xtc_project_destroy(p); xtc_project_destroy(loaded); unlink(document); printf("%s identity, buffers, stereo graph, retirement, reload and missing dependency passed.\n",xtc_plugin_format_name(format)); } +static void context_check(const char *path) +{ + xtc_plugin_spec spec; xtc_plugin *plugin = NULL; + void *handle, *symbol; + void (*read_context)(double *, int64_t *, double *, uint32_t *) = NULL; + float data[31] = {0}; double tempo, music; int64_t position; uint32_t flags; + assert(xtc_plugin_probe_format(XTC_PLUGIN_VST3,path,0,48000,&spec) == XTC_OK); + spec.tempo = 137000; + assert(xtc_plugin_create(&spec,48000,31,&plugin) == XTC_OK); + handle = dlopen(path,RTLD_NOW | RTLD_LOCAL); assert(handle); + symbol = dlsym(handle,"xtc_fixture_context"); assert(symbol && sizeof(symbol) == sizeof(read_context)); + memcpy(&read_context,&symbol,sizeof(symbol)); + audio = 1; assert(xtc_plugin_process_at(plugin,data,data,31,48000) == XTC_OK); audio = 0; + read_context(&tempo,&position,&music,&flags); + assert(tempo == 137 && position == 48000 && music == 137.0/60.0); + assert(flags == ((1U << 1) | (1U << 9) | (1U << 10))); + assert(xtc_plugin_process_at(plugin,data,data,31,0) == XTC_OK); + read_context(&tempo,&position,&music,&flags); assert(position == 0 && music == 0); + assert(xtc_plugin_process(plugin,data,data,31) == XTC_OK); + read_context(&tempo,&position,&music,&flags); assert(position == 31); + assert(xtc_plugin_process_at(plugin,data,data,31,UINT64_MAX) == XTC_ERR_INVALID_ARGUMENT); + read_context(&tempo,&position,&music,&flags); assert(position == 31); + xtc_plugin_destroy(plugin); assert(dlclose(handle) == 0); + puts("VST3 tempo, signed sample clock, seek and sequential context passed."); +} int main(int argc,char **argv) { unsigned int i; xtc_plugin_spec spec; xtc_plugin *p = NULL; float data[1] = {1}; assert(argc == 4); + context_check(argv[3]); check(argv[1],XTC_PLUGIN_CLAP); check(argv[2],XTC_PLUGIN_VST2); check(argv[3],XTC_PLUGIN_VST3); for (i = 1; i < 4; ++i) assert(xtc_plugin_probe_format(XTC_PLUGIN_CLAP,argv[1],i,48000,&spec) != XTC_OK); assert(xtc_plugin_probe_format(XTC_PLUGIN_CLAP,argv[1],4,48000,&spec) == XTC_OK); diff --git a/tests/integration/test_project_inspection.py b/tests/integration/test_project_inspection.py index 577ce51..c314269 100644 --- a/tests/integration/test_project_inspection.py +++ b/tests/integration/test_project_inspection.py @@ -4,6 +4,7 @@ from pathlib import Path import subprocess import sys import tempfile +import platform binary = str(Path(sys.argv[1]).resolve()) @@ -103,6 +104,18 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory: check("--inspect", project, 2, name + " insert track 0", "missing/unreadable: 1") project.write_text(document.replace("XTC_PROJECT 17", "XTC_PROJECT 16").replace(chain, "")) check("--validate", project, 0, "metadata valid") + # A conventional VST3 bundle is inspected without executing its fake binary. + architecture = {"x86_64": "x86_64", "aarch64": "aarch64", + "i386": "i386", "i686": "i386", "armv7l": "arm"}.get(platform.machine()) + if architecture: + bundle = root / "Example.vst3" + inner = bundle / "Contents" / (architecture + "-linux") / "Example.so" + inner.parent.mkdir(parents=True) + bundled = document.replace(str(module).encode().hex(), str(bundle).encode().hex()) + project.write_text(bundled) + check("--inspect", project, 2, "VST3 insert track 0", "missing/unreadable: 1") + inner.write_text("Not a module: metadata inspection must not execute it") + check("--inspect", project, 0, "VST3 insert track 0", "no plugin code executed", "missing/unreadable: 0") for text, messages in [ (base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..17")), ("XTC_PROJECT 10\n", ("line 2", "APP")), diff --git a/tests/integration/test_steinberg_sdk.c b/tests/integration/test_steinberg_sdk.c new file mode 100644 index 0000000..d4c71f7 --- /dev/null +++ b/tests/integration/test_steinberg_sdk.c @@ -0,0 +1,59 @@ +#define _POSIX_C_SOURCE 200809L +#include "xtc/xtc_plugin.h" +#include "xtc/xtc_project.h" +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + xtc_plugin_spec gain, synth, actual; + xtc_plugin *g = NULL, *s = NULL; + float in[256], out[256], wet[256]; + size_t i; + xtc_project *project = NULL, *loaded = NULL; + uint64_t track; + char document[] = "/tmp/xtc-sdk-project-XXXXXX"; + int fd; + xtc_midi_event events[] = { + {0}, {0} + }; + assert(argc == 2 && argv[1][0] == '/'); + assert(xtc_plugin_probe(argv[1],0,48000,&gain) == XTC_OK); + assert(gain.format == XTC_PLUGIN_VST3 && gain.inputs == 1 && !gain.instrument); + assert(!strcmp(gain.path,argv[1]) && !strcmp(gain.label,"xtc SDK Half Gain")); + assert(xtc_plugin_probe_format(XTC_PLUGIN_VST3,argv[1],1,48000,&synth) == XTC_OK); + assert(synth.instrument && !synth.inputs && strcmp(synth.identifier,gain.identifier)); + assert(xtc_plugin_probe(argv[1],2,48000,&actual) == XTC_ERR_UNSUPPORTED); /* Controller, not audio. */ + assert(xtc_plugin_probe(argv[1],3,48000,&actual) == XTC_ERR_NOT_FOUND); + assert(xtc_project_create(&project) == XTC_OK && xtc_project_create(&loaded) == XTC_OK); + assert(xtc_project_add_track(project,"SDK Sine",&track) == XTC_OK); + assert(xtc_project_set_instrument(project,track,&synth) == XTC_OK); + fd = mkstemp(document); assert(fd >= 0 && close(fd) == 0); + assert(xtc_project_save(project,document) == XTC_OK && xtc_project_load(loaded,document) == XTC_OK); + assert(xtc_project_get_instrument(loaded,track,&actual) == XTC_OK); + assert(!strcmp(actual.path,argv[1]) && !strcmp(actual.identifier,synth.identifier)); + assert(unlink(document) == 0); xtc_project_destroy(project); xtc_project_destroy(loaded); + assert(xtc_plugin_create(&gain,48000,256,&g) == XTC_OK); + assert(xtc_plugin_create(&synth,48000,256,&s) == XTC_OK); + for (i = 0; i < 256; ++i) in[i] = i % 2 ? -.75f : .25f; + assert(xtc_plugin_process_at(g,in,out,256,48000) == XTC_OK); + for (i = 0; i < 256; ++i) assert(out[i] == in[i]*.5f); + events[0].type = XTC_MIDI_NOTE_ON; events[0].offset = 13; + events[0].channel = 3; events[0].pitch = 69; events[0].velocity = 127; + events[1] = events[0]; events[1].type = XTC_MIDI_NOTE_OFF; events[1].offset = 200; + assert(xtc_plugin_process_midi_at(s,events,2,out,256,0) == XTC_OK); + for (i = 0; i <= 13; ++i) assert(out[i] == 0); + assert(fabsf(out[40]) > .05f); + for (i = 200; i < 256; ++i) assert(out[i] == 0); + assert(xtc_plugin_process(g,out,wet,256) == XTC_OK); + for (i = 0; i < 256; ++i) assert(wet[i] == out[i]*.5f && isfinite(wet[i])); + assert(xtc_plugin_process_midi(s,NULL,0,out,256) == XTC_OK); + for (i = 0; i < 256; ++i) assert(out[i] == 0); + xtc_plugin_destroy(g); xtc_plugin_destroy(s); + puts("Official SDK gain/synth: bundle discovery, identities, processing, note offsets and release passed."); + return 0; +} diff --git a/tests/plugins/vst3_fixture.c b/tests/plugins/vst3_fixture.c index 0db1675..459b908 100644 --- a/tests/plugins/vst3_fixture.c +++ b/tests/plugins/vst3_fixture.c @@ -5,6 +5,13 @@ #include #include static int loaded, instances; +static struct Steinberg_Vst_ProcessContext last_context; +void xtc_fixture_context(double *tempo, int64_t *position, double *music, uint32_t *flags); +void xtc_fixture_context(double *tempo, int64_t *position, double *music, uint32_t *flags) +{ + *tempo=last_context.tempo; *position=last_context.projectTimeSamples; + *music=last_context.projectTimeMusic; *flags=last_context.state; +} 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; } instance; static instance *from_processor(void *p) { return (instance *)((char *)p - offsetof(instance,processor)); } @@ -48,6 +55,8 @@ static Steinberg_tresult process(void *self, struct Steinberg_Vst_ProcessData *d { instance *p=from_processor(self);int32_t i;Steinberg_int32 index=-1,offset=-1;Steinberg_Vst_ParamValue value=-1; Steinberg_Vst_IParameterChanges *changes=data->outputParameterChanges;Steinberg_Vst_IParamValueQueue *queue;Steinberg_Vst_ParamID id=7; + assert(data->processContext && data->processContext->sampleRate > 0); + last_context = *data->processContext; assert(p->started && changes && changes->lpVtbl->getParameterCount(changes)==0); queue=changes->lpVtbl->addParameterData(changes,&id,&index);assert(queue && index==0); assert(queue->lpVtbl->getParameterId(queue)==7 && queue->lpVtbl->getPointCount(queue)==0); diff --git a/tests/plugins/vst3_synth_fixture.c b/tests/plugins/vst3_synth_fixture.c index d01b33e..4ea0156 100644 --- a/tests/plugins/vst3_synth_fixture.c +++ b/tests/plugins/vst3_synth_fixture.c @@ -49,6 +49,7 @@ static Steinberg_tresult process(void *self, struct Steinberg_Vst_ProcessData *d Steinberg_Vst_IEventList *events = data->inputEvents; int32_t count = events->lpVtbl->getEventCount(events); assert(p->started && data->numInputs == 0); + assert(data->processContext && data->processContext->sampleRate > 0 && data->processContext->tempo > 0); for (i = 0; i < data->numSamples; ++i) { while (j < count) { struct Steinberg_Vst_Event event; diff --git a/tests/unit/test_plugin_module_path.c b/tests/unit/test_plugin_module_path.c new file mode 100644 index 0000000..ea48679 --- /dev/null +++ b/tests/unit/test_plugin_module_path.c @@ -0,0 +1,57 @@ +#define _POSIX_C_SOURCE 200809L +#include "../../src/plugin/module_path.h" +#include +#include +#include +#include +#include +#include + +int main(void) +{ + char root[] = "/tmp/xtc-module-path-XXXXXX", out[512] = "preserved", path[512]; + char bundle[512], contents[512], binary[512]; + FILE *file; + assert(mkdtemp(root)); + assert(xtc_plugin_module_path(NULL,out) == XTC_ERR_INVALID_ARGUMENT); + assert(xtc_plugin_module_path("relative.vst3",out) == XTC_ERR_INVALID_ARGUMENT); + assert(xtc_plugin_module_path(root,out) == XTC_ERR_UNSUPPORTED); + assert(!strcmp(out,"preserved")); + snprintf(bundle,sizeof(bundle),"%s/Example.vst3",root); + assert(mkdir(bundle,0700) == 0); + #if defined(__x86_64__) || defined(__aarch64__) || defined(__i386__) || defined(__arm__) + assert(xtc_plugin_module_path(bundle,out) == XTC_ERR_NOT_FOUND); +#else + assert(xtc_plugin_module_path(bundle,out) == XTC_ERR_UNSUPPORTED); + assert(rmdir(bundle) == 0 && rmdir(root) == 0); + return 0; +#endif + assert(!strcmp(out,"preserved")); + snprintf(contents,sizeof(contents),"%s/Example.vst3/Contents",root); + assert(mkdir(contents,0700) == 0); +#if defined(__x86_64__) +#define ARCH "x86_64-linux" +#elif defined(__aarch64__) +#define ARCH "aarch64-linux" +#elif defined(__i386__) +#define ARCH "i386-linux" +#elif defined(__arm__) +#define ARCH "arm-linux" +#else +#define ARCH "unsupported" +#endif + snprintf(path,sizeof(path),"%s/Example.vst3/Contents/" ARCH,root); + assert(mkdir(path,0700) == 0); + snprintf(binary,sizeof(binary),"%s/Example.vst3/Contents/" ARCH "/Example.so",root); + file = fopen(binary,"wb"); assert(file); assert(fclose(file) == 0); + assert(xtc_plugin_module_path(bundle,out) == XTC_OK && !strcmp(out,binary)); + assert(xtc_plugin_module_path(binary,out) == XTC_OK && !strcmp(out,binary)); + assert(unlink(binary) == 0); + assert(mkfifo(binary,0600) == 0); + strcpy(out,"preserved"); + assert(xtc_plugin_module_path(bundle,out) == XTC_ERR_UNSUPPORTED && !strcmp(out,"preserved")); + assert(unlink(binary) == 0); + assert(rmdir(path) == 0 && rmdir(contents) == 0 && rmdir(bundle) == 0 && rmdir(root) == 0); + puts("Plugin binary/bundle resolution and transactional errors passed."); + return 0; +} diff --git a/third_party/README.md b/third_party/README.md index 1458663..cb9accc 100644 --- a/third_party/README.md +++ b/third_party/README.md @@ -18,3 +18,8 @@ Steinberg VST2 SDK, implementation or VST2-wrapping SDK is copied or distributed Legacy VST2 support does not grant SDK/trademark/plugin distribution rights. There is no VST2 plugin-building target for third-party SDKs. The original test fixture implements only the ABI subset needed to test this host. + +Optional full-SDK examples in `examples/steinberg` compile from explicitly +supplied external VST3/ASIO SDK roots. Normal builds do not fetch or link them; +no external SDK source or ASIO runtime is included in source/binary packages. +See [SDK integration and licenses](../examples/steinberg/README.md).