Claude Code Plugins

Community-maintained marketplace

Feedback

juce-best-practices

@yebot/rad-cc-plugins
1
0

Professional JUCE development guide covering realtime safety, threading, memory management, modern C++, and audio plugin best practices. Use when writing JUCE code, reviewing for realtime safety, implementing audio threads, managing parameters, or learning JUCE patterns and idioms.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name juce-best-practices
description Professional JUCE development guide covering realtime safety, threading, memory management, modern C++, and audio plugin best practices. Use when writing JUCE code, reviewing for realtime safety, implementing audio threads, managing parameters, or learning JUCE patterns and idioms.
allowed-tools Read, Grep, Glob

JUCE Best Practices

Comprehensive guide to professional JUCE framework development with modern C++ patterns, realtime safety, thread management, and audio plugin best practices.

Table of Contents

  1. Realtime Safety
  2. Thread Management
  3. Memory Management
  4. Modern C++ in JUCE
  5. JUCE Idioms and Conventions
  6. Parameter Management
  7. State Management
  8. Performance Optimization
  9. Common Pitfalls

Realtime Safety

The Golden Rule

NEVER allocate, deallocate, lock, or block in the audio thread (processBlock).

What to Avoid in processBlock()

Memory Allocation

// BAD - allocates memory
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    std::vector<float> temp(buffer.getNumSamples()); // WRONG!
    auto dynamicArray = new float[buffer.getNumSamples()]; // WRONG!
}

Pre-allocate in prepare()

// GOOD - pre-allocate once
void prepareToPlay(double sampleRate, int maxBlockSize) {
    tempBuffer.setSize(2, maxBlockSize);
    workingMemory.resize(maxBlockSize);
}

void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    // Use pre-allocated buffers
    tempBuffer.makeCopyOf(buffer);
}

Mutex Locks

// BAD - blocks audio thread
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    const ScopedLock lock(parameterLock); // WRONG!
    auto value = sharedParameter;
}

Use Atomics or Lock-Free Structures

// GOOD - lock-free communication
std::atomic<float> cutoffFrequency{1000.0f};

void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    auto freq = cutoffFrequency.load(); // Lock-free!
    filter.setCutoff(freq);
}

System Calls and I/O

// BAD - system calls in audio thread
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    DBG("Processing " << buffer.getNumSamples()); // WRONG! (console I/O)
    saveAudioToFile(buffer); // WRONG! (file I/O)
}

Realtime Safety Checklist

  • No new or delete
  • No std::vector::push_back() (may allocate)
  • No mutex locks (ScopedLock, std::lock_guard)
  • No file I/O
  • No console output (std::cout, DBG())
  • No malloc or free
  • No unbounded loops (always have max iterations)
  • No exceptions (disable with -fno-exceptions)

Thread Management

The Two Worlds

JUCE audio plugins operate in two separate thread contexts:

  1. Message Thread - UI, user interactions, file I/O, networking
  2. Audio Thread - processBlock(), realtime audio processing

Thread Communication

Message Thread → Audio Thread

// Use atomics for simple values
std::atomic<float> gain{1.0f};

// In UI (message thread)
void sliderValueChanged(Slider* slider) {
    gain.store(slider->getValue()); // Safe!
}

// In audio thread
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    auto currentGain = gain.load(); // Safe!
    buffer.applyGain(currentGain);
}

Audio Thread → Message Thread

// Use AsyncUpdater for async callbacks
class MyProcessor : public AudioProcessor,
                    private AsyncUpdater {
private:
    void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
        // Process audio...
        if (needsUIUpdate) {
            triggerAsyncUpdate(); // Safe!
        }
    }

    void handleAsyncUpdate() override {
        // This runs on message thread - safe to update UI
        editor->updateDisplay();
    }
};

Complex Data with Lock-Free Queue

// For passing complex data (MIDI, analysis, etc.)
juce::AbstractFifo fifo;
std::vector<float> ringBuffer;

// Audio thread writes
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    int start1, size1, start2, size2;
    fifo.prepareToWrite(buffer.getNumSamples(), start1, size1, start2, size2);

    // Write to ring buffer...

    fifo.finishedWrite(size1 + size2);
}

// Message thread reads
void timerCallback() {
    int start1, size1, start2, size2;
    fifo.prepareToRead(fifo.getNumReady(), start1, size1, start2, size2);

    // Read from ring buffer...

    fifo.finishedRead(size1 + size2);
}

Thread Safety Rules

Action Message Thread Audio Thread
Allocate memory ✅ OK ❌ Never
File I/O ✅ OK ❌ Never
Lock mutex ✅ OK ❌ Never
Update UI ✅ OK ❌ Never
Process audio ❌ Never ✅ OK
Use atomics ✅ OK ✅ OK

Memory Management

RAII and Smart Pointers

Use RAII for Resource Management

// GOOD - automatic cleanup
class MyProcessor : public AudioProcessor {
private:
    std::unique_ptr<Reverb> reverb;
    std::vector<float> delayBuffer;

    void prepareToPlay(double sr, int maxBlockSize) override {
        reverb = std::make_unique<Reverb>(); // Auto-managed
        delayBuffer.resize(sr * 2.0); // Auto-managed
    }
    // No manual cleanup needed - automatic destruction
};

Prefer Stack Allocation in processBlock()

Stack Allocation is Realtime-Safe

void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    // OK - stack allocation
    float tempGain = 0.5f;
    int sampleCount = buffer.getNumSamples();

    // Process...
}

Pre-allocate Buffers

Allocate Once, Reuse Many Times

class MyProcessor : public AudioProcessor {
private:
    AudioBuffer<float> tempBuffer;
    std::vector<float> fftData;

    void prepareToPlay(double sr, int maxBlockSize) override {
        // Allocate once
        tempBuffer.setSize(2, maxBlockSize);
        fftData.resize(2048);
    }

    void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
        // Reuse pre-allocated buffers
        tempBuffer.makeCopyOf(buffer);
        // Process using tempBuffer...
    }
};

Modern C++ in JUCE

Use C++17/20 Features Appropriately

Structured Bindings (C++17)

auto [min, max] = buffer.findMinMax(0, buffer.getNumSamples());

if constexpr (C++17)

template<typename SampleType>
void process(AudioBuffer<SampleType>& buffer) {
    if constexpr (std::is_same_v<SampleType, float>) {
        // Float-specific optimizations
    } else {
        // Double-specific code
    }
}

std::optional (C++17)

std::optional<float> tryGetParameter(const String& id) {
    if (auto* param = parameters.getParameter(id))
        return param->getValue();
    return std::nullopt;
}

Const Correctness

Mark Non-Mutating Methods const

class Filter {
public:
    float getCutoff() const { return cutoff; } // const!
    float getResonance() const { return resonance; }

    void setCutoff(float f) { cutoff = f; } // not const - mutates state

private:
    float cutoff = 1000.0f;
    float resonance = 0.707f;
};

Range-Based For Loops

Cleaner Iteration

// OLD WAY
for (int ch = 0; ch < buffer.getNumChannels(); ++ch) {
    auto* channelData = buffer.getWritePointer(ch);
    for (int i = 0; i < buffer.getNumSamples(); ++i) {
        channelData[i] *= gain;
    }
}

// MODERN WAY
for (int ch = 0; ch < buffer.getNumChannels(); ++ch) {
    auto* data = buffer.getWritePointer(ch);
    for (int i = 0; i < buffer.getNumSamples(); ++i) {
        data[i] *= gain;
    }
}

// Or use JUCE's helpers
buffer.applyGain(gain);

JUCE Idioms and Conventions

Audio Buffer Operations

Use JUCE's Buffer Methods

// Apply gain
buffer.applyGain(0.5f);

// Clear buffer
buffer.clear();

// Copy buffer
AudioBuffer<float> copy;
copy.makeCopyOf(buffer);

// Add buffers
outputBuffer.addFrom(0, 0, inputBuffer, 0, 0, numSamples);

Value Tree for State

Use ValueTree for Hierarchical State

ValueTree state("PluginState");
state.setProperty("version", "1.0.0", nullptr);

ValueTree parameters("Parameters");
parameters.setProperty("gain", 0.5f, nullptr);
parameters.setProperty("frequency", 1000.0f, nullptr);

state.appendChild(parameters, nullptr);

// Serialize
auto xml = state.toXmlString();

// Deserialize
auto loadedState = ValueTree::fromXml(xml);

AudioProcessorValueTreeState for Parameters

Standard Parameter Management

class MyProcessor : public AudioProcessor {
public:
    MyProcessor()
        : parameters(*this, nullptr, "Parameters", createParameterLayout())
    {
    }

private:
    AudioProcessorValueTreeState parameters;

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

        params.push_back(std::make_unique<AudioParameterFloat>(
            "gain",
            "Gain",
            NormalisableRange<float>(0.0f, 1.0f),
            0.5f
        ));

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

Parameter Management

Parameter Smoothing

Smooth Parameter Changes to Avoid Zipper Noise

class MyProcessor : public AudioProcessor {
private:
    SmoothedValue<float> gainSmooth;

    void prepareToPlay(double sr, int maxBlockSize) override {
        gainSmooth.reset(sr, 0.05); // 50ms ramp time
    }

    void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
        // Update target from parameter
        auto* gainParam = parameters.getRawParameterValue("gain");
        gainSmooth.setTargetValue(*gainParam);

        // Apply smoothed value
        for (int i = 0; i < buffer.getNumSamples(); ++i) {
            auto gain = gainSmooth.getNextValue();
            for (int ch = 0; ch < buffer.getNumChannels(); ++ch) {
                buffer.setSample(ch, i, buffer.getSample(ch, i) * gain);
            }
        }
    }
};

Parameter Change Notifications

Efficient Parameter Updates

void parameterChanged(const String& parameterID, float newValue) override {
    if (parameterID == "cutoff") {
        cutoffFrequency.store(newValue);
    }
    // Don't do heavy processing here - mark for update instead
}

State Management

Save and Restore State

Implement getStateInformation/setStateInformation

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

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

Version Your State

Handle Backward Compatibility

void setStateInformation(const void* data, int sizeInBytes) override {
    auto xml = getXmlFromBinary(data, sizeInBytes);

    int version = xml->getIntAttribute("version", 1);

    if (version == 1) {
        // Load v1 format and migrate
        migrateFromV1(xml);
    } else if (version == 2) {
        // Load v2 format
        parameters.replaceState(ValueTree::fromXml(*xml));
    }
}

Performance Optimization

Avoid Unnecessary Calculations

Calculate Once, Use Many Times

// BAD
for (int i = 0; i < buffer.getNumSamples(); ++i) {
    auto coeff = std::exp(-1.0f / (sampleRate * timeConstant)); // Recalculated every sample!
}

// GOOD
auto coeff = std::exp(-1.0f / (sampleRate * timeConstant)); // Calculate once
for (int i = 0; i < buffer.getNumSamples(); ++i) {
    // Use coeff
}

Use SIMD When Appropriate

JUCE's dsp::SIMDRegister

void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    auto* data = buffer.getWritePointer(0);
    auto gain = dsp::SIMDRegister<float>(0.5f);

    for (int i = 0; i < buffer.getNumSamples(); i += gain.size()) {
        auto samples = dsp::SIMDRegister<float>::fromRawArray(data + i);
        samples *= gain;
        samples.copyToRawArray(data + i);
    }
}

Denormal Prevention

Prevent Denormals for CPU Performance

void prepareToPlay(double sr, int maxBlockSize) override {
    // Enable flush-to-zero
    juce::FloatVectorOperations::disableDenormalisedNumberSupport();
}

// Or add DC offset in feedback loops
float processSample(float input) {
    static constexpr float denormalPrevention = 1.0e-20f;
    feedbackState = input + feedbackState * 0.99f + denormalPrevention;
    return feedbackState;
}

Common Pitfalls

❌ Pitfall 1: Calling repaint() from Audio Thread

// WRONG
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    // Process...
    if (editor)
        editor->repaint(); // BAD! UI call from audio thread
}

Solution: Use AsyncUpdater

void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    // Process...
    triggerAsyncUpdate(); // Schedules UI update for message thread
}

void handleAsyncUpdate() override {
    if (editor)
        editor->repaint(); // GOOD! On message thread
}

❌ Pitfall 2: Not Handling Sample Rate Changes

// WRONG - assumes 44.1kHz
float delayTimeInSamples = 0.5f * 44100.0f;

Solution: Update in prepareToPlay

void prepareToPlay(double sampleRate, int maxBlockSize) override {
    delayTimeInSamples = 0.5f * sampleRate; // Correct for any sample rate
}

❌ Pitfall 3: Forgetting to Call Base Class Methods

// WRONG
void prepareToPlay(double sr, int maxBlockSize) override {
    // Forgot to call base class!
    mySetup(sr, maxBlockSize);
}

Solution: Always Call Base

void prepareToPlay(double sr, int maxBlockSize) override {
    AudioProcessor::prepareToPlay(sr, maxBlockSize);
    mySetup(sr, maxBlockSize);
}

Quick Reference

Do's ✅

  • Use AudioProcessorValueTreeState for parameters
  • Pre-allocate buffers in prepareToPlay()
  • Use atomics for simple thread communication
  • Smooth parameter changes to avoid zipper noise
  • Version your plugin state
  • Handle all sample rates correctly
  • Use RAII and smart pointers
  • Mark const methods const
  • Use JUCE's helper functions

Don'ts ❌

  • Allocate/deallocate in processBlock()
  • Lock mutexes in audio thread
  • Call UI methods from audio thread
  • Use DBG() or logging in processBlock()
  • Assume fixed sample rate or buffer size
  • Forget to handle state save/load
  • Use raw pointers for ownership
  • Ignore const correctness
  • Reinvent JUCE functionality

Further Reading


Remember: Audio plugins must be realtime-safe, thread-aware, and robust. Follow these best practices to create professional, stable plugins that work reliably across all DAWs and platforms.