# JUCE quick start

> Add a sournd engine to a JUCE 8 plugin. Three CMake lines, three C++ objects, no Rust toolchain on your machine.

```flow
Download | the JUCE module and your engine library -> CMake | three lines -> Three C++ objects | engine, parameters, bridge -> Your plugin | built by JUCE as usual
```

## What you get

- `sournd_engine`, a standard JUCE module that depends only on `juce_audio_processors` and `juce_dsp` (Apache-2.0). Download it: [sournd-engine-juce-module.zip](https://sournd.com/downloads/sournd-engine-juce-module.zip).
- A static library for your platform (`libsournd_cabi.a`, or `sournd_cabi.lib` on Windows) and the header `sournd_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 implements `juce::dsp::ProcessorBase`, and `SourndEngineParameters`, 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.

```cmake
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:

```cmake
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:

```cpp
#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:

```flow
Every block :: Host | knobs and automation -> APVTS | your plugin's parameters -> `bridge.apply` | changed values, once a block -> `engine.process` | smooths sample by sample -> Audio out
Saving :: Host asks for state -> `getCallbackLock()` | keeps `processBlock` out -> `bridge.syncToEngine()` -> `engine.saveState()` | an opaque blob
Loading :: Host hands state back -> `getCallbackLock()` -> `engine.loadState()` | matched by parameter id -> `bridge.syncFromEngine()`
```

## 3. Choose an engine

Pick any id from the [catalogue](/engines/index.md) or [catalog.json](https://sournd.com/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, pass `createLayout (engine, false)` to keep them, because hosts store automation as 0 to 1 values.
- `bridge.apply` hands 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 `processBlock` that touches the engine (saving or loading state, `prepare`, `reset`) holds `getCallbackLock()`, 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 `float` buffers only. Leave `supportsDoublePrecisionProcessing()` 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 `isBusesLayoutSupported` rather than assuming (below). `supportsChannels` prepares 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.

```cpp
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](https://juce.com/legal/juce-8-licence/) and choose the [JUCE plan](https://juce.com/get-juce/) that fits what you ship. Paid engines ship as closed static libraries. See [licensing](/docs/licensing/index.md).
