Agent skill
juce-best-practices
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 this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/juce-best-practices
SKILL.md
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
- Realtime Safety
- Thread Management
- Memory Management
- Modern C++ in JUCE
- JUCE Idioms and Conventions
- Parameter Management
- State Management
- Performance Optimization
- 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
newordelete - 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
mallocorfree - 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:
- Message Thread - UI, user interactions, file I/O, networking
- 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
AudioProcessorValueTreeStatefor 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
- JUCE Documentation: https://docs.juce.com/
- JUCE Forum: https://forum.juce.com/
- JUCE Tutorials: https://juce.com/learn/tutorials
- Audio EQ Cookbook: /docs/dsp-resources/audio-eq-cookbook.html
- C++ Core Guidelines: https://isocpp.github.io/CppCoreGuidelines/
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.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?