dot_claude/skills/juce-expert/SKILL.md
Use when building JUCE 8 audio plugins or applications. Covers AudioProcessor lifecycle, APVTS, DSP module, CMake setup, real-time safety rules, and plugin format targets.
npx skillsauth add nijaru/dotfiles juce-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
processBlock must never allocate, deallocate, lock a mutex, call new/delete, or block. These are hard invariants.AudioProcessorValueTreeState. Never manage parameters manually.RangedAudioParameter* once at construction — never look up by string ID in processBlock.isBusesLayoutSupported for channel configuration. Never use the deprecated JucePlugin_PreferredChannelConfigurations.cmake_minimum_required(VERSION 3.22)
project(MyPlugin VERSION 0.1.0)
include(FetchContent)
FetchContent_Declare(juce
GIT_REPOSITORY https://github.com/juce-framework/JUCE.git
GIT_TAG 8.0.12)
FetchContent_MakeAvailable(juce)
juce_add_plugin(MyPlugin
PLUGIN_MANUFACTURER_CODE Mfr1
PLUGIN_CODE Mp01
FORMATS VST3 AU Standalone
PRODUCT_NAME "My Plugin")
target_sources(MyPlugin PRIVATE src/PluginProcessor.cpp src/PluginEditor.cpp)
target_compile_features(MyPlugin PRIVATE cxx_std_23)
target_link_libraries(MyPlugin
PRIVATE
juce::juce_audio_processors
juce::juce_audio_utils
juce::juce_dsp
PUBLIC
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags)
Community starter: Pamplejuce — CMake + Catch2 + CI, the de facto template.
Constructor → prepareToPlay → [processBlock]* → releaseResources → Destructor
| Method | Thread | Purpose |
| :------------------------------------- | :----- | :------------------------------------------------------------------- |
| prepareToPlay(sampleRate, blockSize) | Main | Allocate buffers, reset DSP state, prepare everything for processing |
| processBlock(buffer, midiMessages) | Audio | Real-time processing — NO allocation, NO locking, NO blocking |
| releaseResources() | Main | Free audio-thread resources |
| getStateInformation(MemoryBlock&) | Main | Serialize state to DAW |
| setStateInformation(data, size) | Main | Restore state from DAW |
new, delete, malloc, freestd::vector::push_back (may reallocate)std::mutex::lock (may block)ValueTree or APVTS method calls that touch the message threadjuce::AbstractFifo or lock-free queues (e.g., moodycamel::ReaderWriterQueue) for audio↔UI communicationAlways use the ParameterLayout constructor. Build layout in a static factory.
// PluginProcessor.h
class MyProcessor : public juce::AudioProcessor {
public:
using APVTS = juce::AudioProcessorValueTreeState;
static APVTS::ParameterLayout createParameterLayout();
APVTS apvts { *this, nullptr, "Parameters", createParameterLayout() };
private:
// Cached raw pointers — safe to read on audio thread
std::atomic<float>* gainParam = nullptr;
std::atomic<float>* cutoffParam = nullptr;
};
// PluginProcessor.cpp
APVTS::ParameterLayout MyProcessor::createParameterLayout() {
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
params.push_back(std::make_unique<juce::AudioParameterFloat>(
juce::ParameterID{"gain", 1}, "Gain",
juce::NormalisableRange<float>(0.0f, 1.0f), 0.5f));
return { std::move(params) };
}
MyProcessor::MyProcessor() {
gainParam = apvts.getRawParameterValue("gain");
cutoffParam = apvts.getRawParameterValue("cutoff");
}
In processBlock: read via gainParam->load() — atomic, real-time safe.
In editor: use attachment classes — never read APVTS on the audio thread from the UI.
// PluginEditor.h
juce::AudioProcessorValueTreeState::SliderAttachment gainAttachment;
// PluginEditor.cpp — constructor
gainAttachment { audioProcessor.apvts, "gain", gainSlider }
Configure via ProcessSpec in prepareToPlay, then call process in processBlock.
void MyProcessor::prepareToPlay(double sampleRate, int blockSize) {
juce::dsp::ProcessSpec spec;
spec.sampleRate = sampleRate;
spec.maximumBlockSize = static_cast<uint32>(blockSize);
spec.numChannels = static_cast<uint32>(getTotalNumOutputChannels());
processorChain.prepare(spec);
}
void MyProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) {
juce::dsp::AudioBlock<float> block(buffer);
juce::dsp::ProcessContextReplacing<float> ctx(block);
processorChain.process(ctx);
}
// Chain declaration (order = signal flow)
juce::dsp::ProcessorChain<
juce::dsp::IIR::Filter<float>,
juce::dsp::Gain<float>,
juce::dsp::Reverb
> processorChain;
Key DSP types: IIR::Filter, FIR::Filter, Gain, Oscillator, Convolution, Oversampling, Reverb, Chorus, Phaser, LadderFilter, WaveShaper, Compressor.
juce::universal_midi_packets namespace. MidiMessage2 for UMP handling.juce::WebBrowserComponent with JavaScript bridge for web-based UIs.juce::AnimatedPosition, VBlank-synced via juce::VBlankAttachment.juce::ShapedText for correct multilingual text layout.Declared in juce_add_plugin via FORMATS. Build outputs appear in <build>/MyPlugin_artefacts/.
| Format | Target Use |
| :----------- | :-------------------------------- |
| VST3 | DAWs on Windows/Linux/macOS |
| AU | Logic, GarageBand (macOS only) |
| AUv3 | iOS/macOS App Store |
| AAX | Pro Tools |
| LV2 | Linux DAWs (Ardour, Reaper/Linux) |
| Standalone | Standalone app for testing |
Always develop against Standalone first — fastest iteration loop.
Use for plugin state that isn't automatable (presets, UI state, non-parameter data).
juce::ValueTree state { "MyPluginState" };
// Persist alongside APVTS:
void getStateInformation(juce::MemoryBlock& dest) {
auto combined = juce::ValueTree("Root");
combined.appendChild(apvts.copyState(), nullptr);
combined.appendChild(state.createCopy(), nullptr);
juce::MemoryOutputStream stream(dest, false);
combined.writeToStream(stream);
}
Thread safety: ValueTree listeners fire on the message thread. Never read/write ValueTree from the audio thread.
Both interop through the C ABI — JUCE itself must remain in C++.
Zig: Write DSP algorithms in Zig, expose via extern "C" from a compiled static lib.
export fn process_filter(buf: [*]f32, len: usize, cutoff: f32) void { ... }
Link in CMake: target_link_libraries(MyPlugin PRIVATE zig_filter_lib). Include the C header in the JUCE processor.
Mojo: Compile Mojo to a shared library exposing C functions via @export. Same pattern — link via CMake, call from C++ shim, never cross the boundary with C++ types.
Invariant: No C++ exceptions, STL types, or virtual dispatch may cross the C boundary into Zig or Mojo.
| Mistake | Fix |
| :------------------------------------------------------ | :--------------------------------------------- |
| Looking up parameter by ID in processBlock | Cache getRawParameterValue() at construction |
| Calling apvts.copyState() on audio thread | Only call on message thread |
| Using JucePlugin_PreferredChannelConfigurations | Use isBusesLayoutSupported |
| new/delete in processBlock | Pre-allocate in prepareToPlay |
| Inheriting from Slider::Listener | Use SliderAttachment |
| String ID lookups in hot path | Cache typed pointers at startup |
| Not calling processorChain.reset() in prepareToPlay | Always reset DSP state before reuse |
development
Use after completing a bug fix, feature, refactor, or tk task when the first implementation taught enough context to replace it with a simpler, cleaner, or more coherent version before finalizing.
development
Use when writing, migrating, or reviewing Zig code across recent stable versions (0.14-0.16), especially to correct stale syntax or stdlib, build.zig, allocator, formatting, or runtime API knowledge.
documentation
Use when reviewing or revising text (prose, docs, commits) to remove AI patterns and improve voice/clarity.
content-media
Use when fetching X/Twitter post content by URL, or searching for recent X posts.