Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@ jobs:
deps-${{ runner.os }}-ninja-

- name: Configure
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DSINGULARITY_BUILD_EXAMPLES=ON
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DSINGULARITY_BUILD_EXAMPLES=ON -DSINGULARITY_BUILD_TESTS=ON

- name: Build
run: |
cmake --build build --config Release

- name: Test
run: ctest --test-dir build -C Release --output-on-failure

- name: Upload VST3 artifacts
uses: actions/upload-artifact@v7
with:
Expand Down Expand Up @@ -89,4 +92,3 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: artifacts/*.zip

50 changes: 50 additions & 0 deletions BuiltInProgram.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#pragma once

#include "IParameterProvider.h"
#include <cstddef>
#include <span>
#include <string>
#include <string_view>
#include <vector>

struct BuiltInProgram
{
// IDs and ordering are persistent. Adapters may expose programs to hosts by
// index, so existing programs must not be reordered between releases.
std::string id;
std::string name;
std::string category;
std::vector<ParameterChange> parameters;

// Optional format-neutral plug-in data. Singularity never interprets these
// bytes: they may contain any serialization or resource reference chosen
// by the plug-in.
std::vector<std::byte> data;
};

struct ProgramCollection
{
std::string id;
std::string name;

// Parameters owned by this collection. Every program begins with the
// declared parameter defaults and may override a subset of these IDs.
std::vector<unsigned int> parameterIds;
std::vector<BuiltInProgram> programs;
};

template<typename P>
concept HasProgramCollections = requires
{
{ P::getProgramCollections() };
};

template<typename P>
concept HandlesProgramData = requires(
P& plugin,
std::string_view collectionId,
std::string_view programId,
std::span<const std::byte> data)
{
{ plugin.loadProgramData(collectionId, programId, data) };
};
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
include(SingularityPlugin)

option(SINGULARITY_BUILD_EXAMPLES "Build the bundled example projects" OFF)
option(SINGULARITY_BUILD_TESTS "Build Singularity adapter tests" OFF)
if(SINGULARITY_BUILD_TESTS)
include(CTest)
enable_testing()
endif()

if(SINGULARITY_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
Expand Down
93 changes: 93 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,99 @@ public:
static_assert(SingularityPlugin<MyEffect>);
```

### VST3 Preset Files

Singularity exposes component state so a VST3 host can save and restore normal
`.vstpreset` files. Factory preset files should be authored in a host by the
plug-in developer and copied to the standard VST3 factory-preset location by
the product installer. The framework does not generate preset files.

### Built-in Programs and VST3 Program Lists

Use a program collection when the plug-in has persistent, numbered built-in
programs—for example, a multitimbral instrument with a collection per MIDI
part. Ordinary named presets should remain host-managed preset files.

```cpp
static auto getProgramCollections()
{
return std::to_array<ProgramCollection>({
{
.id = "performance-bank",
.name = "Performance Bank",
.parameterIds = {13},
.programs = {
{
.id = "soft",
.name = "Soft",
.category = "Synth",
.parameters = {{13, 0.2}},
.data = {std::byte {1}},
},
},
},
});
}

// VST3-only host layout. This is separate from the reusable program data.
static auto getVst3ProgramUnits()
{
return std::to_array<Vst3ProgramUnit>({
{
.id = 1,
.parentId = 0,
.name = "Instrument",
.eventBusIndex = 0,
.midiChannel = 0,
},
});
}

static auto getVst3ProgramListBindings()
{
return std::to_array<Vst3ProgramListBinding>({
{.collectionId = "performance-bank", .unitId = 1},
});
}

void loadProgramData(
std::string_view collectionId,
std::string_view programId,
std::span<const std::byte> data)
{
// Apply optional non-parameter program data.
}
```

`ProgramCollection` and `BuiltInProgram` are adapter-neutral. The VST3-only
`Vst3ProgramUnit` and `Vst3ProgramListBinding` types map collections to
`IUnitInfo`, program-selector parameters, and `IProgramListData`. A unit can
own one collection. Collection and program IDs must be non-empty and unique;
collection IDs and program ordering must remain stable because hosts persist
the resulting list ID and program index. Parameters listed in
`ProgramCollection::parameterIds` are assigned to the collection's VST3 unit
without requiring framework parameter groups to mirror VST3 unit IDs.
For instrument plug-ins, `BuiltInProgram::category` is exposed as the VST3
musical-instrument attribute. It is left unset for effect plug-ins.

Program payloads are optional. If any program supplies `data`, the plug-in must
implement `loadProgramData()`. The callback runs when the processor applies a
program, including at a process-block boundary for host program changes, so it
must be safe for the audio-processing thread. Plug-ins without
`getVst3ProgramListBindings()` do not expose `IProgramListData`.

The payload bytes are opaque to Singularity. `getProgramCollections()` can
populate them from compiled resources, generated headers, JSON or another
serialization, compressed data, or references to separately installed
content. Singularity wraps the payload with the program's parameter snapshot
for VST3 transport and returns the payload unchanged to `loadProgramData()`.
Modified program slots are included in VST3 component state so project restore
does not fall back to the original built-in payload. Program lists are fixed
after initialization, and the current VST3 transport limits one program
payload to 64 MiB and all modified payloads in component state to 256 MiB;
large sample libraries should therefore store lightweight identifiers or paths
rather than sample data in each slot.

The UI exports a component from `App.js`:

```js
Expand Down
1 change: 1 addition & 0 deletions SingularityPlugin.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include <span>
#include "IParameterProvider.h"
#include "BuiltInProgram.h"
#include "AudioDataExchange.h"

using Singularity::AudioDataExchange::sendAudioDataToUI;
Expand Down
113 changes: 112 additions & 1 deletion examples/ExampleInstrument/ExampleInstrument.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "SingularityPlugin.h"
#include "vst3/Vst3ProgramLayout.h"
#include <algorithm>

class ExampleInstrument {
Expand All @@ -11,10 +12,117 @@ class ExampleInstrument {
static auto getParameters()
{
return std::to_array<Parameter>({
{ .id = 13, .name = "Volume", .type = ParamType::Float, .minValue = 0.0, .maxValue = 1.0, .defaultValue = 0.5 }
{
.id = 13,
.name = "Volume",
.type = ParamType::Float,
.minValue = 0.0,
.maxValue = 1.0,
.defaultValue = 0.5,
},
{
.id = 14,
.name = "Brightness",
.type = ParamType::Float,
.minValue = 0.0,
.maxValue = 1.0,
.defaultValue = 0.5,
},
});
}

static auto getVst3ProgramUnits()
{
return std::to_array<Vst3ProgramUnit>({
{
.id = 1,
.parentId = 0,
.name = "Instrument",
.eventBusIndex = 0,
.midiChannel = 0,
},
{
.id = 2,
.parentId = 1,
.name = "Tone",
.eventBusIndex = 0,
.midiChannel = 1,
},
});
}

static auto getVst3ProgramListBindings()
{
return std::to_array<Vst3ProgramListBinding>({
{.collectionId = "performance-bank", .unitId = 1},
{.collectionId = "tone-bank", .unitId = 2},
});
}

static auto getProgramCollections()
{
return std::to_array<ProgramCollection>({
{
.id = "performance-bank",
.name = "Performance Bank",
.parameterIds = {13},
.programs = {
{
.id = "default",
.name = "Default",
.category = "Synth",
.parameters = {{13, 0.5}},
.data = {std::byte {0}},
},
{
.id = "soft",
.name = "Soft",
.category = "Synth",
.parameters = {{13, 0.2}},
.data = {std::byte {1}},
},
{
.id = "full",
.name = "Full",
.category = "Synth",
.parameters = {{13, 0.9}},
.data = {std::byte {2}},
},
},
},
{
.id = "tone-bank",
.name = "Tone Bank",
.parameterIds = {14},
.programs = {
{
.id = "dark",
.name = "Dark",
.category = "Synth",
.parameters = {{14, 0.2}},
.data = {std::byte {3}},
},
{
.id = "bright",
.name = "Bright",
.category = "Synth",
.parameters = {{14, 0.8}},
.data = {std::byte {4}},
},
},
},
});
}

void loadProgramData(
std::string_view,
std::string_view,
std::span<const std::byte> data)
{
programVariant_ =
data.empty() ? 0 : std::to_integer<int>(data.front());
}

void prepare(double sampleRate, int maxBlockSize) {}

template<typename SampleType>
Expand All @@ -26,6 +134,9 @@ class ExampleInstrument {
for (auto* output : outputs)
std::fill_n(output, numSamples, SampleType{});
}

private:
int programVariant_ = 0;
};

static_assert(SingularityPlugin<ExampleInstrument>);
29 changes: 29 additions & 0 deletions vst3/SingularityVst3.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ function(singularity_create_vst3_plugin target)
smtg_add_vst3plugin(${target}_VST3
PACKAGE_NAME "${VST3_PLUGIN_TITLE}"
${SINGULARITY_ROOT_DIR}/vst3/vst3version.h
${SINGULARITY_ROOT_DIR}/vst3/Vst3ComponentState.h
${SINGULARITY_ROOT_DIR}/vst3/Vst3ParameterSupport.h
${SINGULARITY_ROOT_DIR}/vst3/Vst3ProgramData.h
${SINGULARITY_ROOT_DIR}/vst3/Vst3ProgramLayout.h
${SINGULARITY_ROOT_DIR}/vst3/Vst3ProgramModel.h
${SINGULARITY_ROOT_DIR}/vst3/vst3processor.h
${SINGULARITY_ROOT_DIR}/vst3/vst3controller.h
${SINGULARITY_ROOT_DIR}/vst3/vst3controller.cpp
Expand Down Expand Up @@ -161,6 +166,30 @@ function(singularity_create_vst3_plugin target)
${VST3_SOURCE_DIR}
)

if(SINGULARITY_BUILD_TESTS)
add_executable(${target}_VST3_ProgramDataTests
${SINGULARITY_ROOT_DIR}/vst3/tests/Vst3ProgramDataTests.cpp
${SINGULARITY_ROOT_DIR}/vst3/vst3controller.cpp
${SINGULARITY_ROOT_DIR}/vst3/SingularityView.cpp
${SINGULARITY_VST3_PUBLIC_SDK_DIR}/source/common/memorystream.cpp)
target_compile_features(
${target}_VST3_ProgramDataTests PRIVATE cxx_std_23)
target_compile_definitions(${target}_VST3_ProgramDataTests PRIVATE
PLUGIN_CLASS=${VST3_PLUGIN_CLASS}
PLUGIN_CLASS_HEADER="${VST3_PLUGIN_CLASS_HEADER}")
target_include_directories(${target}_VST3_ProgramDataTests PRIVATE
${SINGULARITY_ROOT_DIR}/platform
${SINGULARITY_ROOT_DIR}
${SINGULARITY_ROOT_DIR}/vst3
${VST3_BINARY_DIR}
${VST3_SOURCE_DIR})
target_link_libraries(${target}_VST3_ProgramDataTests
PRIVATE sdk ${VST3_BASE_TARGET})
add_test(
NAME ${target}_VST3_ProgramDataTests
COMMAND ${target}_VST3_ProgramDataTests)
endif()

smtg_target_configure_version_file(${target}_VST3)

if(SMTG_MAC)
Expand Down
Loading
Loading