Claude
Skills
Sign in
Back

juce-best-practices

Included with Lifetime
$97 forever

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.

Image & Video

What this skill does


# 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](#realtime-safety)
2. [Thread Management](#thread-management)
3. [Memory Management](#memory-management)
4. [Modern C++ in JUCE](#modern-cpp-in-juce)
5. [JUCE Idioms and Conventions](#juce-idioms-and-conventions)
6. [Parameter Management](#parameter-management)
7. [State Management](#state-management)
8. [Performance Optimization](#performance-optimization)
9. [Common Pitfalls](#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**
```cpp
// 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()**
```cpp
// 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**
```cpp
// BAD - blocks audio thread
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
    const ScopedLock lock(parameterLock); // WRONG!
    auto value = sharedParameter;
}
```

✅ **Use Atomics or Lock-Free Structures**
```cpp
// 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**
```cpp
// 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**
```cpp
// 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**
```cpp
// 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**
```cpp
// 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**
```cpp
// 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**
```cpp
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**
```cpp
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)**
```cpp
auto [min, max] = buffer.findMinMax(0, buffer.getNumSamples());
```

✅ **if constexpr (C++17)**
```cpp
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)**
```cpp
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**
```cpp
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**
```cpp
// 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**
```cpp
// 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**
```cpp
ValueTree state("PluginState");
state.setProperty("v

Related in Image & Video