JUCE quick start
Add a sournd engine to a JUCE 8 plugin. Three CMake lines, three C++ objects, no Rust toolchain on your machine.
- Downloadthe JUCE module and your engine library
- CMakethree lines
- Three C++ objectsengine, parameters, bridge
- Your pluginbuilt by JUCE as usual
What you get
sournd_engine, a standard JUCE module that depends only onjuce_audio_processorsandjuce_dsp(Apache-2.0). Download it: sournd-engine-juce-module.zip.- A static library for your platform (
libsournd_cabi.a, orsournd_cabi.libon Windows) and the headersournd_engine.h: the free engines from any free engine's page (https://sournd.com/download/free/macos, https://sournd.com/download/free/linux, https://sournd.com/download/free/windows), paid ones from Your engines. SourndEngine, which implementsjuce::dsp::ProcessorBase, andSourndEngineParameters, which builds an APVTS layout from the engine and keeps them in step.
1. Add the module and the library
Unzip the module so you have a sournd_engine folder, and unzip your engine download next to it.
juce_add_module(path/to/sournd_engine)
target_include_directories(MyPlugin PRIVATE path/to/include) # sournd_engine.h
target_link_libraries(MyPlugin PRIVATE sournd_engine path/to/libsournd_cabi.a)For a universal macOS plugin, build with -DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' and link the universal archive.
On Linux the download has one archive per architecture. Pick the one for the machine you build for and add the system libraries it needs:
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
set(SOURND_ARCH linux-aarch64)
else()
set(SOURND_ARCH linux-x86_64)
endif()
target_link_libraries(MyPlugin PRIVATE sournd_engine
path/to/lib/${SOURND_ARCH}/libsournd_cabi.a pthread dl m)2. Create the engine, its parameters and the bridge
Construct them in this order, on the message thread:
#include <sournd_engine/sournd_engine.h>
class MyPlugin final : public juce::AudioProcessor
{
public:
MyPlugin()
: AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo())
.withOutput ("Output", juce::AudioChannelSet::stereo())),
engine ("org.sournd.reverb.roomette"),
parameters (*this, nullptr, "Parameters", sournd::SourndEngineParameters::createLayout (engine)),
bridge (engine, parameters)
{ jassert (engine.isValid()); }
void prepareToPlay (double rate, int maximumBlock) override
{
engine.prepare ({ rate, (juce::uint32) maximumBlock, (juce::uint32) getTotalNumOutputChannels() });
bridge.prepare (rate);
setLatencySamples ((int) engine.latency());
}
void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override
{
bridge.apply ((uint32_t) buffer.getNumSamples());
juce::dsp::AudioBlock<float> block { buffer };
engine.process (juce::dsp::ProcessContextReplacing<float> { block });
}
void getStateInformation (juce::MemoryBlock& destination) override
{
// Hosts can ask for state during playback: JUCE's callback lock keeps processBlock out
// while the engine is read (the C ABI allows one call at a time per engine).
const juce::ScopedLock lock (getCallbackLock());
bridge.syncToEngine();
destination = engine.saveState();
}
void setStateInformation (const void* data, int bytes) override
{
if (bytes < 0) return;
const juce::ScopedLock lock (getCallbackLock());
if (engine.loadState (data, (size_t) bytes)) bridge.syncFromEngine();
}
double getTailLengthSeconds() const override { return engine.tailSeconds(); }
private:
sournd::SourndEngine engine;
juce::AudioProcessorValueTreeState parameters;
sournd::SourndEngineParameters bridge;
};What happens at run time:
Every block
- Hostknobs and automation
- APVTSyour plugin's parameters
bridge.applychanged values, once a blockengine.processsmooths sample by sample- Audio out
Saving
- Host asks for state
getCallbackLock()keepsprocessBlockoutbridge.syncToEngine()engine.saveState()an opaque blob
Loading
- Host hands state back
getCallbackLock()engine.loadState()matched by parameter idbridge.syncFromEngine()
3. Choose an engine
Pick any id from the catalogue or catalog.json. An unknown id produces an invalid engine that outputs silence, so check isValid() and lastError().
Rules worth knowing
- Parameter ids are stable decimal strings: the position of the parameter in the engine's manifest, shown on every engine page. Parameters are only ever added, never renumbered.
- Host ranges follow each parameter's taper. A log parameter (cutoff, time, ratio) maps the host's 0 to 1 as
min * (max / min)^p, so automation lanes and generic editors move in equal ratios, as the engine's own knobs do. If you already shipped a plugin with linear ranges, passcreateLayout (engine, false)to keep them, because hosts store automation as 0 to 1 values. bridge.applyhands each changed value to the engine at the start of the block. The engine smooths continuous controls itself, sample by sample, so the sound does not depend on the host's block size (tested bit for bit at 1, 32, 64 and 512 frames). Stepped and boolean controls snap.- Frames per call must fit the maximum passed to
prepare. The wrapper does not split host blocks. - State is opaque and tied to the engine id. It stores settings, not delay or filter history. It migrates across engine versions: values are matched by parameter id, parameters added since the session was saved take their defaults, and values from a newer engine that this one lacks are ignored (
lastStateReport()counts both). A state is refused, with nothing changed, only when it belongs to another engine or is damaged. - The audio thread never allocates, locks or calls the message manager. Use one engine instance per processing thread.
- Calls into one engine must not overlap. Anything outside
processBlockthat touches the engine (saving or loading state,prepare,reset) holdsgetCallbackLock(), as above, so it never runs while audio is processed.
Limits
These are the edges of the bridge today.
- Audio is 32-bit float. The bridge processes
floatbuffers only. LeavesupportsDoublePrecisionProcessing()at its default (false), so hosts hand your plugin float audio. - Mono or stereo, the same on input and output. One or two main channels; mono to stereo, stereo to mono and surround layouts are not supported. Some engines run in stereo only, so ask the engine in
isBusesLayoutSupportedrather than assuming (below).supportsChannelsprepares a scratch instance, so cache the answer rather than calling it on the audio thread. - Sidechain engines accept a disconnected, mono or stereo auxiliary input.
- MIDI. Effects ignore the MIDI buffer, and the bridge never produces MIDI. Instrument engines read note on, note off and all notes off (CC 120 and 123) at their sample positions, on every channel; pitch bend, aftertouch, other controllers and MPE are not passed on.
- Sample rates from 8 kHz to 768 kHz, and up to 1,048,576 frames per block.
- Hidden engine parameters stay inside the engine's state and are not host controls.
bool isBusesLayoutSupported (const BusesLayout& layout) const override
{
const auto& in = layout.getMainInputChannelSet();
return in == layout.getMainOutputChannelSet()
&& ((in == juce::AudioChannelSet::mono() && mono) || (in == juce::AudioChannelSet::stereo() && stereo));
}
// members, after engine:
const bool mono = engine.supportsChannels (1), stereo = engine.supportsChannels (2);Platforms
Downloads are built for macOS universal (arm64 and x86_64), Linux (x86_64 and aarch64, glibc 2.36 or newer) and Windows x64 (MSVC). Pick the platform when you download from your engines page; an agent asks for it with platform=linux or platform=windows-x64.
On Windows the zip holds two copies of sournd_cabi.lib: lib/windows-x64/ for projects on the dynamic C runtime (/MD, the MSVC default) and lib/windows-x64-static-crt/ for the static one (/MT). Link the one that matches your plugin, followed by kernel32 ntdll userenv ws2_32 dbghelp. The README in the zip has the CMake lines.
Licensing
The JUCE module and C header are Apache-2.0, so they link into GPL or commercial JUCE projects. JUCE itself has its own licence and plans, which a sournd licence does not cover: follow the JUCE 8 licence and choose the JUCE plan that fits what you ship. Paid engines ship as closed static libraries. See licensing.