Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/juce-best-practices" ~/.claude/skills/majiayu000-claude-skill-registry-juce-best-practices && rm -rf "$T"
skills/data/juce-best-practices/SKILL.mdJUCE 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
ornewdelete - No
(may allocate)std::vector::push_back() - No mutex locks (
,ScopedLock
)std::lock_guard - No file I/O
- No console output (
,std::cout
)DBG() - No
ormallocfree - 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
repaint()// 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
for parametersAudioProcessorValueTreeState - 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
or logging in processBlock()DBG() - 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.