Added VST SDK support.

This commit is contained in:
Johannes Findeisen 2026-11-06 18:13:49 +01:00
commit 8e09f58944
25 changed files with 850 additions and 23 deletions

View file

@ -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

View file

@ -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.

View file

@ -233,9 +233,10 @@ parameters.
Choose the actual binary: `.clap` for CLAP, a VST2 `.so` exporting
`VSTPluginMain`, or a VST3 bundle's `Contents/<architecture>-linux/<module>.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.

View file

@ -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

57
docs/STEINBERG.md Normal file
View file

@ -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/<architecture>-linux/<bundle-stem>.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).

View file

@ -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.

View file

@ -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()

View file

@ -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.

View file

@ -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 <array>
#include <cstdio>
#include <cstring>
// 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<char, MAXDRVNAMELEN> name{};
if (session.drivers.asioGetDriverName(static_cast<int>(i), name.data(),
static_cast<int>(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;
}

View file

@ -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 <stdio.h>
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;
}

View file

@ -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 <array>
#include <cmath>
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<IEditController*>(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<IAudioProcessor*>(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<std::array<Voice, 128>, 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<IAudioProcessor*>(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<double>(key)-69.0)/12.0) / processSetup.sampleRate;
v.phase = std::fmod(v.phase, 6.283185307179586);
}
data.outputs[0].channelBuffers32[0][i] = static_cast<float>(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

View file

@ -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. */

View file

@ -13,6 +13,7 @@
#include <sys/stat.h>
#include <unistd.h>
#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;slot<chain.count;++slot) {
const xtc_track_insert *insert=chain.slots+slot;const xtc_plugin_spec *spec=&insert->plugin;
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;

View file

@ -1,4 +1,5 @@
#include "modern.h"
#include "module_path.h"
#include "builtin.h"
#include <dlfcn.h>
#include <math.h>
@ -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;
}

View file

@ -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;
}

View file

@ -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

40
src/plugin/module_path.c Normal file
View file

@ -0,0 +1,40 @@
#include "module_path.h"
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
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;
}

6
src/plugin/module_path.h Normal file
View file

@ -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

View file

@ -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);

View file

@ -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")),

View file

@ -0,0 +1,59 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_project.h"
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
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;
}

View file

@ -5,6 +5,13 @@
#include <stdlib.h>
#include <string.h>
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);

View file

@ -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;

View file

@ -0,0 +1,57 @@
#define _POSIX_C_SOURCE 200809L
#include "../../src/plugin/module_path.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
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;
}

View file

@ -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).