Complete Example Walkthrough

A complete example of integrating Rust DSP with a JUCE plugin.

Project Structure

my-utility-plugin/
├── CMakeLists.txt
├── dsp/
│   ├── Cargo.toml
│   ├── parameters.json
│   ├── include/
│   │   ├── bbx_ffi.h
│   │   └── bbx_graph.h
│   └── src/
│       └── lib.rs
├── src/
│   ├── PluginProcessor.cpp
│   ├── PluginProcessor.h
│   ├── PluginEditor.cpp
│   └── PluginEditor.h
└── vendor/
    └── corrosion/

Step 1: Define Parameters

dsp/parameters.json:

{
  "parameters": [
    {
      "id": "GAIN",
      "name": "Gain",
      "type": "float",
      "min": -60.0,
      "max": 30.0,
      "defaultValue": 0.0,
      "unit": "dB"
    },
    {
      "id": "PAN",
      "name": "Pan",
      "type": "float",
      "min": -100.0,
      "max": 100.0,
      "defaultValue": 0.0
    },
    {
      "id": "MONO",
      "name": "Mono",
      "type": "boolean",
      "defaultValue": false
    }
  ]
}

Step 2: Rust DSP Implementation

dsp/Cargo.toml:

[package]
name = "dsp"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["staticlib"]

[dependencies]
bbx_plugin = "0.1"

dsp/src/lib.rs:

#![allow(unused)]
fn main() {
use bbx_plugin::{
    PluginDsp, DspContext, bbx_plugin_ffi,
    blocks::{GainBlock, PannerBlock},
};

// Parameter indices
const PARAM_GAIN: usize = 0;
const PARAM_PAN: usize = 1;
const PARAM_MONO: usize = 2;
const PARAM_COUNT: usize = 3;

pub struct PluginGraph {
    gain: GainBlock<f32>,
    panner: PannerBlock<f32>,
    mono: bool,
}

impl Default for PluginGraph {
    fn default() -> Self {
        Self::new()
    }
}

impl PluginDsp for PluginGraph {
    fn new() -> Self {
        Self {
            gain: GainBlock::new(0.0, None),
            panner: PannerBlock::new(0.0),
            mono: false,
        }
    }

    fn prepare(&mut self, _context: &DspContext) {
        // Nothing to prepare for these simple blocks
    }

    fn reset(&mut self) {
        // Nothing to reset
    }

    fn apply_parameters(&mut self, params: &[f32]) {
        self.gain.level_db = params[PARAM_GAIN];
        self.panner.position = params[PARAM_PAN];
        self.mono = params[PARAM_MONO] > 0.5;
    }

    fn process(
        &mut self,
        inputs: &[&[f32]],
        outputs: &mut [&mut [f32]],
        context: &DspContext,
    ) {
        let num_samples = context.buffer_size;
        let multiplier = self.gain.multiplier();

        // Apply gain
        for ch in 0..inputs.len().min(outputs.len()) {
            for i in 0..num_samples {
                outputs[ch][i] = inputs[ch][i] * multiplier;
            }
        }

        // Apply mono summing if enabled
        if self.mono && outputs.len() >= 2 {
            for i in 0..num_samples {
                let sum = (outputs[0][i] + outputs[1][i]) * 0.5;
                outputs[0][i] = sum;
                outputs[1][i] = sum;
            }
        }

        // Apply panning
        if outputs.len() >= 2 {
            let (left_gain, right_gain) = self.panner.gains();
            for i in 0..num_samples {
                outputs[0][i] *= left_gain;
                outputs[1][i] *= right_gain;
            }
        }
    }
}

bbx_plugin_ffi!(PluginGraph);
}

Step 3: CMake Configuration

CMakeLists.txt:

cmake_minimum_required(VERSION 3.15)
project(MyUtilityPlugin VERSION 1.0.0)

add_subdirectory(JUCE)
add_subdirectory(vendor/corrosion)

corrosion_import_crate(MANIFEST_PATH dsp/Cargo.toml)

juce_add_plugin(MyUtilityPlugin
    PLUGIN_MANUFACTURER_CODE Bbxa
    PLUGIN_CODE Util
    FORMATS AU VST3 Standalone
    PRODUCT_NAME "My Utility Plugin")

target_sources(MyUtilityPlugin PRIVATE
    src/PluginProcessor.cpp
    src/PluginEditor.cpp)

target_link_libraries(MyUtilityPlugin PRIVATE
    juce::juce_audio_processors
    juce::juce_audio_utils
    dsp)

target_include_directories(MyUtilityPlugin PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/dsp/include)

Step 4: JUCE Processor

src/PluginProcessor.h:

#pragma once

#include <juce_audio_processors/juce_audio_processors.h>
#include <bbx_graph.h>
#include <bbx_ffi.h>
#include <array>
#include <vector>

class PluginAudioProcessor : public juce::AudioProcessor {
public:
    PluginAudioProcessor();
    ~PluginAudioProcessor() override = default;

    void prepareToPlay(double sampleRate, int samplesPerBlock) override;
    void releaseResources() override;
    void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;

    juce::AudioProcessorEditor* createEditor() override;
    bool hasEditor() const override { return true; }

    const juce::String getName() const override { return "My Utility Plugin"; }
    bool acceptsMidi() const override { return false; }
    bool producesMidi() const override { return false; }
    double getTailLengthSeconds() const override { return 0.0; }

    int getNumPrograms() override { return 1; }
    int getCurrentProgram() override { return 0; }
    void setCurrentProgram(int) override {}
    const juce::String getProgramName(int) override { return {}; }
    void changeProgramName(int, const juce::String&) override {}

    void getStateInformation(juce::MemoryBlock& destData) override;
    void setStateInformation(const void* data, int sizeInBytes) override;

    juce::AudioProcessorValueTreeState parameters;

private:
    juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();

    bbx::Graph m_rustDsp;
    std::vector<float> m_paramBuffer;

    std::atomic<float>* gainParam = nullptr;
    std::atomic<float>* panParam = nullptr;
    std::atomic<float>* monoParam = nullptr;

    static constexpr size_t MAX_CHANNELS = 8;
    std::array<const float*, MAX_CHANNELS> m_inputPtrs {};
    std::array<float*, MAX_CHANNELS> m_outputPtrs {};
};

src/PluginProcessor.cpp:

#include "PluginProcessor.h"
#include "PluginEditor.h"

PluginAudioProcessor::PluginAudioProcessor()
    : AudioProcessor(BusesProperties()
        .withInput("Input", juce::AudioChannelSet::stereo(), true)
        .withOutput("Output", juce::AudioChannelSet::stereo(), true))
    , parameters(*this, nullptr, "Parameters", createParameterLayout())
{
    gainParam = parameters.getRawParameterValue("gain");
    panParam = parameters.getRawParameterValue("pan");
    monoParam = parameters.getRawParameterValue("mono");

    m_paramBuffer.resize(PARAM_COUNT);
}

juce::AudioProcessorValueTreeState::ParameterLayout
PluginAudioProcessor::createParameterLayout()
{
    std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;

    params.push_back(std::make_unique<juce::AudioParameterFloat>(
        "gain", "Gain",
        juce::NormalisableRange<float>(-60.0f, 30.0f, 0.1f),
        0.0f));

    params.push_back(std::make_unique<juce::AudioParameterFloat>(
        "pan", "Pan",
        juce::NormalisableRange<float>(-100.0f, 100.0f, 1.0f),
        0.0f));

    params.push_back(std::make_unique<juce::AudioParameterBool>(
        "mono", "Mono", false));

    return { params.begin(), params.end() };
}

void PluginAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
    m_rustDsp.Prepare(
        sampleRate,
        static_cast<uint32_t>(samplesPerBlock),
        static_cast<uint32_t>(getTotalNumOutputChannels()));
}

void PluginAudioProcessor::releaseResources()
{
    m_rustDsp.Reset();
}

void PluginAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer,
                                         juce::MidiBuffer&)
{
    juce::ScopedNoDenormals noDenormals;

    auto numChannels = static_cast<uint32_t>(buffer.getNumChannels());
    auto numSamples = static_cast<uint32_t>(buffer.getNumSamples());
    numChannels = std::min(numChannels, static_cast<uint32_t>(MAX_CHANNELS));

    m_paramBuffer[PARAM_GAIN] = gainParam->load();
    m_paramBuffer[PARAM_PAN] = panParam->load();
    m_paramBuffer[PARAM_MONO] = monoParam->load();

    for (uint32_t ch = 0; ch < numChannels; ++ch) {
        m_inputPtrs[ch] = buffer.getReadPointer(static_cast<int>(ch));
        m_outputPtrs[ch] = buffer.getWritePointer(static_cast<int>(ch));
    }

    m_rustDsp.Process(
        m_inputPtrs.data(),
        m_outputPtrs.data(),
        numChannels,
        numSamples,
        m_paramBuffer.data(),
        static_cast<uint32_t>(m_paramBuffer.size()));
}

juce::AudioProcessorEditor* PluginAudioProcessor::createEditor()
{
    return new PluginEditor(*this);
}

void PluginAudioProcessor::getStateInformation(juce::MemoryBlock& destData)
{
    auto state = parameters.copyState();
    std::unique_ptr<juce::XmlElement> xml(state.createXml());
    copyXmlToBinary(*xml, destData);
}

void PluginAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
{
    std::unique_ptr<juce::XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));
    if (xmlState && xmlState->hasTagName(parameters.state.getType()))
        parameters.replaceState(juce::ValueTree::fromXml(*xmlState));
}

juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
    return new PluginAudioProcessor();
}

Step 5: Build

# Configure
cmake -B build -DCMAKE_BUILD_TYPE=Release

# Build
cmake --build build --config Release

The plugin will be in build/MyUtilityPlugin_artefacts/.