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

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