SKILL: DSP IMPLEMENTATION
SkillMediaGoal: Implement audio processing where parameters control DSP Focus: PluginProcessor.h, PluginProcessor.cpp Output Location: $PluginPath/Source/
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the SKILL: DSP IMPLEMENTATION skill
About this capability
Audio Plugin Coder (APC) is a groundbreaking, open-source framework that enables musicians, producers, sound designers, and developers to create professional VST3/AU audio plugins using natural language and AI assistance. Built on JUCE 9 and powered by a revolutionary 5-phase workflow system.
What this skill tells your AI
The instructions your AI receives, as published by noizefield/audio-plugin-coder in .agent/skills/skill_implementation/SKILL.md and read by ahel’s review.
Goal: Implement audio processing where parameters control DSP
Focus: PluginProcessor.h, PluginProcessor.cpp
Output Location: $PluginPath/Source/
📊 PHASE 4: CODE (DSP Implementation)
Trigger: /impl [Name] (after DESIGN phase complete)
Input: Reads $PluginPath/status.json and .ideas/parameter-spec.md
Prerequisites: Architecture plan complete, UI framework selected
State Validation:
# Import state management module
. "$PSScriptRoot\..\scripts\state-management.ps1"
$PluginPath = Get-ApcPluginPath -PluginName $PluginName
# Validate prerequisites
if (-not (Test-PluginState -PluginPath $PluginPath -RequiredPhase "design_complete" -RequiredFiles @(".ideas/architecture.md", ".ideas/plan.md"))) {
Write-Error "Prerequisites not met. Complete design phase first."
exit 1
}
# Check framework selection
$state = Get-PluginState -PluginPath $PluginPath
if ($state.ui_framework -eq "pending") {
Write-Error "UI framework not selected. Cannot proceed with implementation."
exit 1
}
Write-Host "Framework: $($state.ui_framework)" -ForegroundColor Cyan
🎨 PHASE 4.0: DESIGN-TO-FRAMEWORK CONVERSION (CRITICAL - BEFORE DSP CODE)
IMPORTANT: This phase converts the approved design specifications into framework-specific code. User must approve the conversion before DSP implementation begins.
Framework Routing:
- If
ui_framework == webview: use templates fromtemplates/webview/ - If
ui_framework == visage: use templates fromtemplates/visage/and do not generate HTML
4.0.1 Read Approved Design
- Read
Design/v[N]-ui-spec.md(latest approved version) - Read
Design/v[N]-style-guide.md(latest approved version) - Read
.ideas/parameter-spec.mdfor parameter definitions
4.0.2 Framework-Specific Conversion
For WebView Framework: Convert the approved design specs into production JUCE WebView code.
Create the required directory structure:
$PluginPath/Source/ui/
└───public/
│ index.html # Production UI based on approved design - ALL CSS/JS inline (webview-008)
│ test-local.html # Browser-test copy of index.html
Implementation Steps:
- Create directory:
$PluginPath/Source/ui/public/ - Inline the JUCE frontend library: Take
_tools/JUCE/modules/juce_gui_extra/native/typescript/webview-interop/dist/index.js, remove itsimport/exportstatements, expose it aswindow.Juce, and paste it into a single<script>block inindex.html(ES6 modules fail silently in WebView - webview-008) - Convert design to HTML: Transform approved design specs into
index.htmlwith embedded CSS - Inline UI JavaScript: Append parameter state setup and UI controls to the SAME
<script>block - NO external.jsfiles - Create browser-test copy: Duplicate
index.htmlastest-local.html
Conversion Process:
- Extract layout, colors, and styling from
v[N]-style-guide.md - Extract control specifications from
v[N]-ui-spec.md - Generate HTML structure matching the approved design
- Implement interactive controls using JUCE parameter states
- Apply approved color palette and visual style
Example Output (based on approved design):
ui/public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[Name] Plugin</title>
<!-- webview-008: ALL JavaScript must be INLINE - external scripts fail silently -->
<style>
/* Styles based on approved v[N]-style-guide.md */
body {
background: [approved-background-color];
color: [approved-text-color];
font-family: [approved-font-family];
margin: 0;
padding: 20px;
}
/* Additional styles from approved design */
</style>
</head>
<body>
<!-- Layout based on approved v[N]-ui-spec.md -->
<div id="plugin-ui">
<!-- Controls generated from parameter-spec.md -->
</div>
</body>
</html>
Inline JavaScript (inside ui/public/index.html):
// NO imports allowed (webview-008):
const Juce = window.Juce;
// Initialize parameter states from parameter-spec.md
const parameterStates = {};
document.addEventListener("DOMContentLoaded", () => {
// Create UI controls based on approved design
initializeUI();
console.log("WebView UI initialized from approved design");
});
function initializeUI() {
// Generate controls based on v[N]-ui-spec.md specifications
// Bind to JUCE parameter states
}
USER APPROVAL REQUIRED - CRITICAL STOP POINT:
✅ Design converted to WebView code
Files created:
- $PluginPath/Source/ui/public/index.html (ALL JavaScript inline)
⚠️ **MANDATORY STOP** - You MUST test the WebView setup before proceeding to DSP implementation!
What would you like to do?
1. Test WebView - Open $PluginPath/Source/ui/public/index.html in browser and verify appearance
2. Approve - Proceed with DSP implementation (confirms WebView GUI is acceptable)
3. Revise - Make changes to the conversion
**YOU MUST CHOOSE OPTION 1 OR 2 BEFORE CONTINUING**
**DO NOT PROCEED TO PHASE 4.1 WITHOUT USER APPROVAL**
Choose (1-3): _
Approval Validation:
- If user chooses 1: Wait for user to test and return to this menu
- If user chooses 2: Mark WebView GUI as approved and proceed to Phase 4.1
- If user chooses 3: Allow revisions to the design conversion
For Visage Framework:
Convert approved design to Visage C++ code (Source/VisageControls.h).
Use templates from templates/visage/ and the shared host in common/VisageJuceHost.h.
? VISAGE IMPLEMENTATION CHECKLIST (MANDATORY)
Before proceeding to DSP implementation, validate Visage setup:
.\scripts\validate-visage-setup.ps1 -PluginName [Name]
Mandatory Checklist (All Must Pass):
- ? CMakeLists.txt links Visage
- Contains
visage::visageintarget_link_libraries
- Contains
- ? Visage controls exist
Source/VisageControls.hpresent
- ? Editor uses Visage host
PluginEditor.hincludesVisageJuceHost.h- Editor inherits
VisagePluginEditor
- ? No WebView-only flags
NEEDS_WEBVIEW2 TRUEandJUCE_WEB_BROWSER=1not present
✅ WEBVIEW IMPLEMENTATION CHECKLIST (MANDATORY)
CRITICAL: When implementing WebView plugins, you MUST verify ALL 8 points below. Use templates from templates/webview/ and run validation script.
WebView Setup Validation (Run Before DSP Implementation)
Before proceeding to Phase 4.1, validate WebView setup:
# Run validation script
.\scripts\validate-webview-setup.ps1 -PluginName [Name]
Mandatory Checklist (All Must Pass):
-
✅ CMakeLists.txt embeds web files
- Contains
juce_add_binary_data([Name]_WebUI ...) - Links binary data target:
target_link_libraries([Name] PRIVATE [Name]_WebUI ...) - Has
NEEDS_WEBVIEW2 TRUEinjuce_add_plugin() - Has compile definitions:
JUCE_WEB_BROWSER=1andJUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1
- Contains
-
✅ WebBrowserComponent uses WebView2 backend
.withBackend(WebBrowserComponent::Options::Backend::webview2)is present- NOT using default backend (must be explicit)
-
✅ WebBrowserComponent has user data folder
.withUserDataFolder(File::getSpecialLocation(File::SpecialLocationType::tempDirectory))is present- Required for Windows plugins to work
-
✅ Native integration enabled
.withNativeIntegrationEnabled()is present- Enables JavaScript ↔ C++ communication
-
✅ Resource provider implemented
.withResourceProvider([this](const auto& url) { return getResource(url); })is presentgetResource()function exists and loads from embedded zipgetZipFile()helper function exists
-
✅ Parameter relays created BEFORE WebBrowserComponent
- Relays created before
std::make_unique<WebBrowserComponent>() - Relays passed via
.withOptionsFrom(*relay)for each parameter
- Relays created before
-
✅ Parameter attachments created AFTER WebBrowserComponent
- Attachments created after
addAndMakeVisible(*webView) - Attachments connect parameters to relays
- Attachments created after
-
✅ Web content loaded via resource provider
- Uses
webView->goToURL(WebBrowserComponent::getResourceProviderRoot()) - NOT using
data:text/html;base64,...orloadHTML() - NOT using hardcoded HTML strings
- Uses
🔴 CRITICAL: Member Declaration Order (PluginEditor.h)
⚠️ #1 CAUSE OF DAW CRASHES - VERIFY THIS FIRST
C++ destroys members in REVERSE order of declaration. If WebView is declared before relays, it will be destroyed AFTER relays, causing a crash when it tries to access freed relay memory.
✅ CORRECT ORDER (in PluginEditor.h):
private:
// 1. RELAYS FIRST (destroyed last)
juce::WebSliderRelay gainRelay { "GAIN" };
// 2. WEBVIEW SECOND (destroyed middle)
std::unique_ptr<juce::WebBrowserComponent> webView;
// 3. ATTACHMENTS LAST (destroyed first)
std::unique_ptr<juce::WebSliderParameterAttachment> gainAttachment;
❌ WRONG ORDER (causes DAW crash on unload):
private:
std::unique_ptr<juce::WebBrowserComponent> webView; // ❌ Too early!
juce::WebSliderRelay gainRelay { "GAIN" }; // ❌ Too late!
Verification: Run validation script before building:
.\scripts\validate-webview-member-order.ps1 -PluginName [Name]
See: ..agent/troubleshooting/resolutions/webview-member-order-crash.md
Common Mistakes to Avoid:
❌ DON'T: Declare webView before relays in header file ✅ DO: Always use order: Relays → WebView → Attachments
❌ DON'T: Use data URIs (data:text/html;base64,...)
✅ DO: Use getResourceProviderRoot() with embedded files
❌ DON'T: Create WebBrowserComponent without WebView2 backend
✅ DO: Explicitly specify .withBackend(webview2)
❌ DON'T: Create parameter attachments before WebBrowserComponent ✅ DO: Create relays → WebBrowserComponent → attachments (in that order)
❌ DON'T: Skip resource provider
✅ DO: Implement getResource() function to serve embedded files
❌ DON'T: Forget to embed web files in CMakeLists.txt
✅ DO: Use juce_add_binary_data() to embed all web UI files
Template Usage:
Copy templates from: templates/webview/
PluginEditor.h.template→Source/PluginEditor.hPluginEditor.cpp.template→Source/PluginEditor.cppCMakeLists.txt.template→CMakeLists.txt
Replace placeholders:
{{PLUGIN_NAME}}→ Your plugin class name{{PLUGIN_NAME_LOWER}}→ Lowercase plugin name{{PARAMETER_RELAYS}}→ Your parameter relay declarations{{CREATE_PARAMETER_RELAYS}}→ Code to create relays{{WITH_OPTIONS_FROM_RELAYS}}→.withOptionsFrom()calls{{CREATE_PARAMETER_ATTACHMENTS}}→ Code to create attachments
If Validation Fails:
- Review error messages from validation script
- Check templates in
templates/webview/ - Compare your code with JUCE example:
_tools/JUCE/examples/Plugins/WebViewPluginDemo.h - Ensure
Source/ui/public/index.htmlexists with ALL JavaScript inline (no separate .js files) - Verify CMakeLists.txt embeds files correctly
DO NOT PROCEED TO DSP IMPLEMENTATION UNTIL ALL 8 CHECKS PASS
🔧 PHASE 4.1: DSP IMPLEMENTATION
Read $PluginPath/.ideas/plan.md to determine implementation approach:
Complexity Score: [N]
If score ≤2: Single-pass implementation (all at once)
If score ≥3: Phased implementation (multiple passes)
Single-pass (Simple plugins):
- One implementation session
- All DSP components at once
- Example: Simple gain, filter, compressor
Phased (Complex plugins):
- Multiple implementation phases
- Break into logical chunks
- Example: Multi-band processing, synthesis engines
🔧 PHASE 4.1: DSP IMPLEMENTATION
Prerequisites: UI structure must be created (Phase 4.0) before DSP implementation begins.
FOR SINGLE-PASS (Complexity ≤2):
Step 1: Read contracts
.ideas/creative-brief.md- Plugin purpose and behavior.ideas/architecture.md- DSP components and math.ideas/parameter-spec.md- Parameter bindings
Step 2: Update PluginProcessor.h
Add DSP member variables:
private:
// DSP Components from .ideas/architecture.md
juce::dsp::Gain inputGain;
juce::dsp::IIR::Filter filter;
juce::dsp::Compressor compressor;
// State
double currentSampleRate = 44100.0;
Step 3: Implement prepareToPlay()
Initialize DSP at sample rate:
void prepareToPlay(double sampleRate, int samplesPerBlock) override
{
currentSampleRate = sampleRate;
juce::dsp::ProcessSpec spec;
spec.sampleRate = sampleRate;
spec.maximumBlockSize = samplesPerBlock;
spec.numChannels = getTotalNumOutputChannels();
inputGain.prepare(spec);
filter.prepare(spec);
compressor.prepare(spec);
}
Step 4: Implement processBlock()
Add DSP processing:
void processBlock(juce::AudioBuffer& buffer,
juce::MidiBuffer& midiMessages) override
{
juce::ScopedNoDenormals noDenormals;
// Get parameter values from APVTS
auto gainValue = apvts.getRawParameterValue("gain")->load();
auto thresholdValue = apvts.getRawParameterValue("threshold")->load();
// Update DSP components
inputGain.setGainDecibels(gainValue);
compressor.setThreshold(thresholdValue);
// Process audio
juce::dsp::AudioBlock block(buffer);
juce::dsp::ProcessContextReplacing context(block);
inputGain.process(context);
filter.process(context);
compressor.process(context);
}
Step 5: Connect parameters to DSP
Ensure all parameters from .ideas/parameter-spec.md are:
- Read from APVTS in processBlock using the unified parameter handling system
- Applied to DSP components with proper validation and smoothing
- Mapped correctly (linear, logarithmic, exponential as specified)
- Consistent with parameter ranges defined in the specification
Consistency Verification:
// Verify parameter consistency during development
#ifdef DEBUG
void verifyParameterConsistency()
{
// Check that parameter IDs match between spec and implementation
// This helps catch typos and ensures all specified parameters are implemented
const auto& parameters = apvts.processor.getParameters();
for (int i = 0; i < parameters.size(); ++i)
{
auto* param = dynamic_cast<juce::AudioProcessorParameterWithID*>(parameters[i]);
if (param != nullptr)
{
// Verify this parameter exists in parameter-spec.md
// (Implementation would read and parse the spec file)
}
}
}
#endif
FOR PHASED (Complexity ≥3):
plan.md will define phases like:
### Phase 4.1.1: Core Processing
- Input/output gain
- Basic filtering
### Phase 4.1.2: Dynamics
- Compressor
- Limiter
### Phase 4.1.3: Modulation
- LFO
- Envelope follower
Execute each phase sequentially:
-
Phase 4.1.1 - Implement core components
- Add member variables
- Update prepareToPlay()
- Add to processBlock()
- Build and test
- Git commit
-
Phase 4.1.2 - Add dynamics components
- Build on Phase 4.1.1 code (preserve everything)
- Add new components
- Build and test
- Git commit
-
Phase 4.1.3 - Add modulation
- Build on Phase 4.1.2 code
- Complete implementation
- Build and test
- Git commit
Decision menu after each phase:
✓ Phase 4.1.1 complete
Progress: 1 of 3 phases
What's next?
1. Continue to Phase 4.1.2 (recommended)
2. Test current state in DAW
3. Review Phase 4.1.1 code
4. Pause here
Choose (1-4): _
🎯 CRITICAL IMPLEMENTATION RULES
Real-Time Safety:
- NO heap allocations in processBlock() - Pre-allocate in prepareToPlay()
- NO locks in audio thread - Use atomic values for parameter access
- NO file I/O in processBlock() - All resources loaded beforehand
- Use
juce::ScopedNoDenormalsat start of processBlock()
🔄 UNIFIED PARAMETER HANDLING SYSTEM
Parameter Validation & Range Checking
// Helper function for parameter validation
float validateParameter(float value, float minVal, float maxVal, const char* paramName)
{
if (value < minVal || value > maxVal)
{
// Log warning but clamp to valid range
jassertfalse; // Debug warning
return juce::jlimit(minVal, maxVal, value);
}
return value;
}
// In processBlock() - with validation
auto* gainParam = apvts.getRawParameterValue("gain");
float rawGainValue = gainParam->load();
float validatedGain = validateParameter(rawGainValue, -60.0f, 24.0f, "gain");
inputGain.setGainDecibels(validatedGain);
Parameter Smoothing (Anti-Zipper Noise)
// Member variables for smoothed parameters
juce::SmoothedValue<float> smoothedGain;
juce::SmoothedValue<float> smoothedCutoff;
juce::SmoothedValue<float> smoothedResonance;
// In prepareToPlay()
void prepareToPlay(double sampleRate, int samplesPerBlock) override
{
// Initialize smoothing with appropriate time constants
smoothedGain.reset(sampleRate, 0.020); // 20ms for gain
smoothedCutoff.reset(sampleRate, 0.050); // 50ms for filter
smoothedResonance.reset(sampleRate, 0.010); // 10ms for resonance
}
// In processBlock() - with smoothing
auto* gainParam = apvts.getRawParameterValue("gain");
smoothedGain.setTargetValue(gainParam->load());
for (int sample = 0; sample < numSamples; ++sample)
{
float currentGain = smoothedGain.getNextValue();
// Apply to DSP with per-sample smoothing
inputGain.setGainDecibels(currentGain);
}
Parameter Mapping Templates
// Template for different parameter types
enum class ParameterType
{
Linear,
Logarithmic,
Exponential,
Boolean
};
// Parameter mapping helper
float mapParameter(float normalizedValue, ParameterType type, float minVal, float maxVal)
{
switch (type)
{
case ParameterType::Linear:
return juce::jmap(normalizedValue, 0.0f, 1.0f, minVal, maxVal);
case ParameterType::Logarithmic:
return juce::jmap(juce::jlimit(0.0f, 1.0f, normalizedValue),
0.0f, 1.0f, minVal, maxVal, true);
case ParameterType::Exponential:
return minVal * std::pow(maxVal / minVal, normalizedValue);
case ParameterType::Boolean:
return normalizedValue > 0.5f ? maxVal : minVal;
default:
return normalizedValue;
}
}
// Usage example
auto* cutoffParam = apvts.getRawParameterValue("cutoff");
float normalizedCutoff = cutoffParam->load();
float mappedCutoff = mapParameter(normalizedCutoff,
ParameterType::Logarithmic,
20.0f, 20000.0f);
*filter.coefficients = juce::dsp::IIR::Coefficients::makeLowPass(
sampleRate, mappedCutoff);
Consistency Verification
// Helper to verify parameter consistency between spec and implementation
void verifyParameterConsistency()
{
// This should be called during development/debug builds
#ifdef DEBUG
// Check that all parameters in parameter-spec.md are implemented
// This is a development-time check, not runtime
#endif
}
Parameter Binding:
// Standardized parameter binding pattern
void processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) override
{
juce::ScopedNoDenormals noDenormals;
// 1. Read all parameters with validation
auto* gainParam = apvts.getRawParameterValue("gain");
auto* cutoffParam = apvts.getRawParameterValue("cutoff");
auto* resonanceParam = apvts.getRawParameterValue("resonance");
// 2. Update smoothed values
smoothedGain.setTargetValue(gainParam->load());
smoothedCutoff.setTargetValue(cutoffParam->load());
smoothedResonance.setTargetValue(resonanceParam->load());
// 3. Process audio with per-sample parameter updates
const int numSamples = buffer.getNumSamples();
for (int sample = 0; sample < numSamples; ++sample)
{
// Get current smoothed values
float currentGain = smoothedGain.getNextValue();
float currentCutoff = smoothedCutoff.getNextValue();
float currentResonance = smoothedResonance.getNextValue();
// Apply to DSP components
inputGain.setGainDecibels(currentGain);
// Update filter with mapped values
float mappedCutoff = mapParameter(currentCutoff,
ParameterType::Logarithmic,
20.0f, 20000.0f);
float mappedResonance = mapParameter(currentResonance,
ParameterType::Linear,
0.1f, 10.0f);
*filter.coefficients = juce::dsp::IIR::Coefficients::makeLowPass(
currentSampleRate, mappedCutoff, mappedResonance);
// Process this sample
juce::dsp::AudioBlock block(buffer.getArrayOfWritePointers(),
buffer.getNumChannels(), 1);
juce::dsp::ProcessContextReplacing context(block);
inputGain.process(context);
filter.process(context);
}
}
Edge Cases:
// Handle zero-length buffers
if (buffer.getNumSamples() == 0)
return;
// Handle silent input
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
Buffer Management:
// Pre-allocate buffers in prepareToPlay()
void prepareToPlay(double sampleRate, int samplesPerBlock) override
{
tempBuffer.setSize(2, samplesPerBlock);
// Use tempBuffer in processBlock() - no allocation
}
✅ PHASE 4.2: BUILD & VERIFY
After implementation complete:
Step 1: Validate JUCE/CMake setup
# Import state management module
. "$PSScriptRoot\..\scripts\state-management.ps1"
$PluginPath = Get-ApcPluginPath -PluginName $PluginName
# Validate prerequisites using standardized function
if (-not (Validate-PhasePrerequisites -PluginPath $PluginPath -CurrentPhase "code" -RequiredPhase "design_complete" -RequiredFiles @(".ideas/architecture.md", ".ideas/plan.md"))) {
Write-Host "ERROR: Prerequisites not met. Complete design phase first." -ForegroundColor Red
exit 1
}
# Check JUCE installation
if (-not (Test-Path "C:\JUCE")) {
Write-Host "ERROR: JUCE not found at C:\JUCE" -ForegroundColor Red
Write-Host "Please install JUCE 8 and set up the project correctly" -ForegroundColor Yellow
exit 1
}
# Check CMake availability
if (-not (Get-Command cmake -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: CMake not found" -ForegroundColor Red
Write-Host "Please install CMake and ensure it's in your PATH" -ForegroundColor Yellow
exit 1
}
# Validate project structure
if (-not (Test-Path "CMakeLists.txt")) {
Write-Host "ERROR: CMakeLists.txt not found in project root" -ForegroundColor Red
exit 1
}
# Validate canvas implementation for WebView framework
if ($state.ui_framework -eq "webview") {
if (-not (Test-CanvasImplementation -PluginPath $PluginPath)) {
Write-Host "ERROR: Canvas implementation required for WebView framework" -ForegroundColor Red
Write-Host "WebView plugins must use HTML5 Canvas API with JUCE frontend library" -ForegroundColor Yellow
Write-Host "Please ensure Design/index.html uses canvas-based rendering" -ForegroundColor Yellow
exit 1
}
}
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 317
- Forks
- 51
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
skill-implementation- Source
- github.com/noizefield/audio-plugin-coder