Search guides and the APICtrl K
  1. Docs
  2. Guides

Hosting plugins

The other direction: an application that loads other people’s plugins. sonore/host.h scans folders, describes what it finds, loads a plugin and drives it with audio, MIDI, parameters and state.

On this page
Format
CLAP always compiled in
VST3, LV2 compiled in where their C headers are on the include path: sonore_enable_hosting(<target>) in CMake arranges it
LADSPA always compiled in: audio and control ports, which is all the format has
Audio Units (v2 and v3) macOS, through host_au.h; compiled for macOS and not yet run
VST2 never

Without sonore_enable_hosting a host still builds, hosts CLAP, and finds no VST3 or LV2 plugin at all, so call it for every program that hosts.

Scan, load, run

Scanning means running each file’s own code, which is slow and can crash the host. sonore::host::scanDirectoryCached keeps a sonore::host::PluginCache: unchanged files are not opened again, and the path being scanned is written to the cache file and flushed to disk before the file is opened, so a plugin that kills the process is blacklisted on the next run instead of killing it again.

C++Compiled in CI
#include <sonore/plugin_cache.h>

#include <cstdio>
#include <memory>
#include <vector>

int main(int argc, char** argv) {
  if (argc < 2) {
    std::printf("usage: scan <plugin folder>\n");
    return 0;
  }
  using namespace sonore::host;

  // [main thread] Scan, remembering what was found and what crashed.
  PluginCache cache;
  cache.load("plugins.cache"); // false the first time: there is nothing yet
  const std::vector<PluginDescription> found = scanDirectoryCached(argv[1], cache, "plugins.cache");
  for (const PluginDescription& d : found)
    std::printf("%-5s %s by %s%s\n", d.format.c_str(), d.name.c_str(), d.vendor.c_str(),
                d.isInstrument ? " (instrument)" : "");
  if (found.empty()) return 0;

  // Load the first, and give it its buffers. Null when the file moved, the id
  // is gone or the plugin refused to start.
  std::unique_ptr<HostedPlugin> plugin = loadPlugin(found[0]);
  if (!plugin || !plugin->prepare(48000.0, 512, 2)) return 1;
  for (int i = 0; i < plugin->numParameters(); ++i)
    std::printf("  %s = %s\n", plugin->parameter(i).name.c_str(),
                plugin->parameterText(i, plugin->parameterValue(i)).c_str());

  // [audio thread, in a real host] One block, in place. process() allocates
  // nothing: prepare() sized every buffer.
  std::vector<float> left(512, 0.0f), right(512, 0.0f);
  float* channels[2] = {left.data(), right.data()};
  sonore::AudioBlock<float> block(channels, 2, 512);
  plugin->process(block);

  // A session: the plugin's own state, to hand back to loadState() later.
  std::vector<uint8_t> state;
  plugin->saveState(state);
  std::printf("latency %u samples, state %zu bytes\n", (unsigned) plugin->latencySamples(), state.size());
  return 0;
}

sonore::host::HostedPlugin is the same interface whatever the format: parameters (numParameters, parameterValue, setParameterValue, parameterText), state (saveState, loadState), presets, latency and tail, MIDI in and out, bypass, and the plugin’s editor (hasEditor, openEditor into a native parent window). The threading follows the plugin side’s rules: scanning, loading, prepare(), state and parameter reads on the main thread; process() on the audio thread.

More than one plugin

sonore::host::PluginGraph (graph.h) runs hosted plugins as a graph: addNode() takes a loaded plugin, connect() routes audio and connectMidi() routes the notes one plugin emits into another, and process() runs the graph in dependency order. gfx/plugin_list.h is the table a host shows of the plugins it knows about.

Building a host

CMake
add_executable(MyHost main.cpp)
target_link_libraries(MyHost PRIVATE sonore_sdk)   # SonoreSDK::sonore_sdk from find_package
sonore_enable_hosting(MyHost)

The SDK’s own host tests are built on this header (sdk/tests/host_test.cpp, sdk/tests/render_tool.cpp): they load the plugins the SDK builds, as a DAW would, on every run.

↑↓ to move↵ to openEvery public name in the headers, and every guide