Search guides and the APICtrl K
  1. API reference
  2. Hosting plugins

host.h

Hosting OTHER people's plugins.

#include <sonore/host.h>
On this page

Design notes

Everything else in this SDK is about being a plugin. This is the other direction: scanning a folder, finding what is in it, loading one, driving it with audio and MIDI, reading and writing its state. It is what an application needs to put third-party processors in its own signal path.

The machinery is not new. The host tests in sdk/tests have been loading built artifacts and driving them as a host does for a long time, and this is that code made reusable and given a contract. What it adds is the part a test never needed: scanning, enumeration, and a lifetime that survives being handed around.

WHAT THIS SUPPORTS, and what it does not:

CLAP
complete, and verified against every plugin this SDK builds.
VST3
complete, same. Compiled in only where Steinberg's C API has been vendored, because a user who wants CLAP hosting should not have to fetch a second SDK to make a header compile.
AU
/ AUv3: macOS only, in host_au.h, which this file includes at its end: the system registry scanned by type, AUv2 and AUv3 loaded, rendered, automated, saved and edited. COMPILED for macOS and never run -- there is no Mac in this loop -- and every Apple call reports its failure by name; tests/mac_host_selftest.cpp is the run a Mac owes it.
LV2
complete, and the reason it took longest: an LV2 host cannot learn a plugin's NAME without parsing RDF, so this needed turtle.h first. Compiled in where the LV2 headers have been vendored.
LADSPA
complete for what the format has: audio and control ports, and nothing else -- no MIDI, no editor, no presets. Always compiled in, because ladspa_abi.h declares its ABI and there is no SDK to vendor.
VST2
never. Steinberg no longer licenses its SDK, and a host built on a reconstruction of it carries the same risk a plugin would.

THREADING follows the same rules the wrappers obey. scan(), load(), prepare(), state and parameter reads are main-thread. process() is the audio thread and allocates nothing: every buffer it needs is sized in prepare().

Types 30

template <typename T, size_t Capacity> class MpscQueue

A bounded multi-producer, single-consumer queue: Vyukov's ring, each slot carrying a sequence number that says whose turn it is. A producer never waits -- a full queue refuses -- and nothing allocates after construction, so a UI thread, an automation thread and the audio thread itself can all push while whoever is running the plugin pops.

One plugin, described without loading it.

A scan may find several plugins inside one file (CLAP's factory returns a list), so path alone does not identify anything. The pair (path, id) does, and that pair is what a host should store in its own session.

One parameter of a loaded plugin.

A parameter's position, given the plugin's id for it.

Ids need not be 0..n-1, nor in declaration order, and the lookup runs once per event a plugin reports -- on the audio thread, for CLAP. Scanning the list made one event cost the parameter count, so a preset load reporting every one of 4000 parameters was millions of comparisons in one block. Hashed once when the list is read, at most half full so every probe run is short and ends; a lookup after that costs the same at any size.

Something the plugin did to one of its own parameters, on its way to the host.

Automation is not a one-way street. A host that only ever WRITES parameters records nothing when the user turns a knob on the plugin's own face, and shows stale values in its generic editor after a preset loads. The gesture marks matter as much as the values: they are what lets a host fold a whole mouse drag into one undo step, and what tells it to stop playing automation back over a knob the user is currently holding.

One instantiated plugin, driven the way a host drives one.

The object owns the module it came from, so it stays valid for as long as it is held and unloads cleanly when it is not. Copying is disabled for the same reason: two owners of one plugin instance is a double destroy.

The interface is what the two formats have in COMMON, which is nearly everything a host actually does: prepare, process, read and write parameters, save and restore state, ask how late the output is. Where they genuinely differ -- CLAP delivers MIDI as events, VST3 splits it between typed events and hidden parameters -- the difference is absorbed by the implementation rather than pushed onto the caller.

class ClapPlugin final : public HostedPlugin

The CLAP side of it.

IParamValueQueue: one parameter's points for this block, at sample offsets inside it. Kept in a vector reserved before processing, so a block's automation allocates nothing.

IParameterChanges, the host's side: every parameter queue this block carries into process() as ProcessData::inputParameterChanges.

The other direction: what the PROCESSOR changed during a block -- a meter it publishes as a read-only parameter, a value its DSP moved. Only the last point of each is kept, because this host is block-granular and what the controller and the caller need is where the value ended.

IParameterChanges, the plugin's side: ProcessData::outputParameterChanges, where the DSP reports the parameters it moved itself. Read after the block and sent on to the main thread, where the controller and drainParameterEdits() hear about them.

IComponentHandler2, the same object's second face: setDirty is how a VST3 plugin says its state changed where no parameter shows it.

The object a VST3 plugin tells when its own editor moves something.

This is the direction the format makes a host implement rather than call. Without it a plugin has nowhere to report an edit, which is why some refuse to initialise their controller at all -- and why, for the ones that do not, every knob turned on the plugin's face is invisible to automation.

restartComponent is the other half and the less obvious one: it is how a plugin says "I changed values you did not write", which is exactly what happens when a program is selected. A host that ignores it keeps showing the numbers from before the preset.

The other object VST3 makes a host implement: what a view talks to.

A plugin is entitled to ask for a different size, and the way it asks is through the frame. Attaching a view WITHOUT one is legal and common, and it is also how a plugin ends up permanently stuck at its initial size -- including plugins whose editor computes its own size on the first frame.

This one declined every request, "for the same reason the CLAP side does: the window belongs to the caller", and recorded it where no caller could read it. A view that waits for onSize() -- Steinberg's own do -- kept its size, editorSize() never moved, and a caller following it had nothing to follow. The request is granted now as Steinberg's host grants it: the view is given the size through onSize() before resizeView returns, and the caller's window follows through editorSize().

An IBStream over a std::vector.

VST3 reads and writes state through a stream object rather than a buffer, so a host that wants the bytes has to provide one. Seek is implemented because setComponentState needs the stream rewound between the component and the controller -- both read it from the start.

IEventList: a block's notes and MIDI, in either direction -- the events handed to process(), and the ones the plugin sends back out. The vector behind it is reserved before the block.

class Vst3Plugin final : public HostedPlugin

The VST3 side of it: a module's component and its controller, connected to each other and driven as HostedPlugin describes. Made by loadVst3(); the COM objects above are what it hands the plugin.

LV2_Feature: a URI and whatever the extension it names hands over.

LV2_Options_Option: one value the host states, keyed and typed by URID. A list ends with an entry whose key and value are both zero.

LV2_Worker_Schedule: the host's half, given to the plugin.

LV2_Worker_Interface: the plugin's half, from extension_data().

One direction of the worker's traffic: whole messages, each a 32-bit length and its bytes, in a ring one thread writes and one reads. Lock-free and allocation-free once made, because one end is the audio thread. A message that does not fit is refused: LV2 has "no space" for exactly that.

LV2UI_Descriptor: what a UI binary exports, one per UI it contains.

LV2UI_Idle_Interface: called on the host's UI tick. A non-zero answer means the UI has closed itself.

LV2UI_Resize, both ways round: as a feature the host offers, an interface asks for a size through it; as an interface's extension data, the host tells the interface its new size (the handle field then unused, the interface's own handle passed instead). Zero is success.

One port of an LV2 plugin, as the bundle's Turtle describes it: kind and direction, range and default, and what its values are called.

One pset:Preset, reduced to what applying it costs: port indices and the values they take.

A preset that says nothing about a port leaves it alone -- that is LV2's rule, not a shortcut here, and it is why this is a sparse list rather than a full control vector. State properties (state:state) are not read: our own plugins put everything in ports, and silently applying half a preset would be worse than declining a kind we cannot honour.

class Lv2Plugin final : public HostedPlugin

The LV2 side of it: a plugin loaded from its bundle -- the Turtle read for its ports and presets, the binary for its code -- and driven as HostedPlugin describes. Made by loadLv2(). Control ports are floats the host owns; a value's text comes from the bundle, since LV2 has no call that asks the plugin.

class LadspaPlugin final : public HostedPlugin

One LADSPA plugin, driven through the same interface as the other three.

The format, as the reference arranges it: where its plugins live, which files might be one, what a file holds, and loading one.

Static throughout, because there is nothing per-object to keep -- the list of what was found belongs in a PluginCache, and a loaded plugin owns its library.

Functions 66

Aliases 3

Constants 1

  • constconstexpr uint8_t kVst3StateFrame[8] = { … }
    A VST3 session blob that carries the controller's own state as well as the component's: this, then the component's length and bytes, then the controller's, lengths as 64-bit little-endian. A blob without it is the component's state alone -- what a plugin with no controller state saves, and every session saved before the controller's was kept.
    host::kVst3StateFrame · host.h:4194
↑↓ to move↵ to openEvery public name in the headers, and every guide