Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:08:03 +08:00
commit a886924d29
29 changed files with 11395 additions and 0 deletions

View File

@@ -0,0 +1,924 @@
---
name: cross-platform-builds
description: Comprehensive guide to building JUCE plugins for macOS, Windows, and Linux with CMake, code signing, notarization, and CI/CD. Use when configuring builds, setting up CI/CD pipelines, troubleshooting cross-platform compilation, implementing code signing, or creating installers for multiple platforms.
allowed-tools: Read, Grep, Glob
---
# Cross-Platform Builds for JUCE Plugins
Comprehensive guide to building JUCE audio plugins across macOS, Windows, and Linux with proper configuration, code signing, and continuous integration.
## Overview
JUCE audio plugins must be built for multiple platforms and plugin formats:
- **macOS**: VST3, AU (Audio Unit), AAX
- **Windows**: VST3, AAX
- **Linux**: VST3
Each platform has specific requirements for build tools, code signing, and packaging. This skill covers:
1. CMake configuration for all platforms and formats
2. Platform-specific build instructions
3. Code signing and notarization
4. Continuous integration setup
5. Reproducible builds
---
## 1. CMake Configuration
### Root CMakeLists.txt Structure
```cmake
cmake_minimum_required(VERSION 3.22)
project(MyPlugin VERSION 1.0.0)
# C++17 minimum for JUCE 7+
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Export compile_commands.json for IDEs
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Add JUCE
add_subdirectory(JUCE)
# Plugin formats to build
set(PLUGIN_FORMATS VST3 AU Standalone)
# Add AAX if PACE SDK is available
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/SDKs/AAX")
list(APPEND PLUGIN_FORMATS AAX)
juce_set_aax_sdk_path("${CMAKE_CURRENT_SOURCE_DIR}/SDKs/AAX")
endif()
# Define the plugin
juce_add_plugin(MyPlugin
COMPANY_NAME "YourCompany"
PLUGIN_MANUFACTURER_CODE Manu # 4-character code
PLUGIN_CODE Plug # 4-character code (unique!)
FORMATS ${PLUGIN_FORMATS}
PRODUCT_NAME "MyPlugin"
# Bundle IDs
BUNDLE_ID com.yourcompany.myplugin
# Plugin characteristics
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
# Copy plugin to system folder after build
COPY_PLUGIN_AFTER_BUILD TRUE
# VST3 category
VST3_CATEGORIES Fx
# AU type (aufx = effect, aumu = instrument)
AU_MAIN_TYPE kAudioUnitType_Effect
)
# Source files
target_sources(MyPlugin PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/DSP/Filter.cpp
Source/DSP/Modulation.cpp
)
# Public compile definitions
target_compile_definitions(MyPlugin PUBLIC
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_VST3_CAN_REPLACE_VST2=0
JUCE_DISPLAY_SPLASH_SCREEN=0 # Commercial license only!
)
# Link JUCE modules
target_link_libraries(MyPlugin PRIVATE
juce::juce_audio_utils
juce::juce_dsp
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags
)
# Platform-specific settings
if(APPLE)
# macOS deployment target
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS version")
# Universal binary (Apple Silicon + Intel)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures")
# Hardened runtime for notarization
target_compile_options(MyPlugin PUBLIC
-Wall -Wextra -Wpedantic
)
elseif(WIN32)
# Static runtime for standalone distribution
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
# Windows-specific definitions
target_compile_definitions(MyPlugin PRIVATE
_CRT_SECURE_NO_WARNINGS
)
elseif(UNIX)
# Linux-specific flags
target_compile_options(MyPlugin PRIVATE
-Wall -Wextra
)
# Link against ALSA, JACK, etc.
find_package(PkgConfig REQUIRED)
pkg_check_modules(ALSA REQUIRED alsa)
target_link_libraries(MyPlugin PRIVATE ${ALSA_LIBRARIES})
endif()
# Tests (optional)
option(BUILD_TESTS "Build unit tests" ON)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(Tests)
endif()
```
### Key Configuration Options
#### Plugin Codes
```cmake
PLUGIN_MANUFACTURER_CODE Manu # Your unique 4-character manufacturer ID
PLUGIN_CODE Plug # Unique 4-character plugin ID
```
**Important**: Register manufacturer code at [Steinberg](https://www.steinberg.net/en/company/developers.html) to avoid conflicts.
#### Bundle Identifiers
```cmake
BUNDLE_ID com.yourcompany.myplugin # Reverse domain notation
```
Must be unique and consistent across versions for AU validation.
#### Plugin Characteristics
```cmake
IS_SYNTH TRUE # Instrument vs effect
NEEDS_MIDI_INPUT TRUE # Accept MIDI input
NEEDS_MIDI_OUTPUT FALSE # Send MIDI output
IS_MIDI_EFFECT FALSE # MIDI-only processing (no audio)
```
#### VST3 Categories
```cmake
VST3_CATEGORIES Fx # Effect
VST3_CATEGORIES Instrument # Instrument
VST3_CATEGORIES Fx Dynamics # Multiple categories
```
Available categories: `Fx`, `Instrument`, `Analyzer`, `Delay`, `Distortion`, `Dynamics`, `EQ`, `Filter`, `Mastering`, `Modulation`, `Restoration`, `Reverb`, `Spatial`, `Synth`, `Tools`
#### AU Types
```cmake
AU_MAIN_TYPE kAudioUnitType_Effect # Effect
AU_MAIN_TYPE kAudioUnitType_MusicDevice # Instrument
AU_MAIN_TYPE kAudioUnitType_MIDIProcessor # MIDI effect
```
---
## 2. macOS Builds
### Prerequisites
1. **Xcode** (latest version recommended)
```bash
xcode-select --install
```
2. **CMake** (3.22+)
```bash
brew install cmake
```
3. **Developer ID Certificate** (for distribution)
- Enroll in Apple Developer Program ($99/year)
- Create "Developer ID Application" certificate in Xcode
### Building
```bash
# Configure
cmake -B build-mac -G Xcode \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
# Build all formats
cmake --build build-mac --config Release --parallel
# Or build with Xcode
open build-mac/MyPlugin.xcodeproj
```
### Universal Binaries (Apple Silicon + Intel)
```cmake
# In CMakeLists.txt
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
```
Or at build time:
```bash
cmake -B build-mac -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
```
Verify architectures:
```bash
lipo -info build-mac/MyPlugin_artefacts/Release/VST3/MyPlugin.vst3/Contents/MacOS/MyPlugin
# Output: Architectures in the fat file: MyPlugin are: x86_64 arm64
```
### Code Signing
#### Manual Signing
```bash
# Sign VST3
codesign --force \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
--options runtime \
--entitlements Resources/Entitlements.plist \
--timestamp \
--deep \
MyPlugin.vst3
# Sign AU
codesign --force \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
--options runtime \
--timestamp \
--deep \
MyPlugin.component
# Verify signature
codesign --verify --deep --strict --verbose=2 MyPlugin.vst3
```
#### Entitlements File (Resources/Entitlements.plist)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Allow JIT for DSP optimization -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Allow loading unsigned plugins (for VST3 presets, etc.) -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- For networked plugins (optional) -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
```
#### Automated Signing in CMake
```cmake
# Add to CMakeLists.txt
if(APPLE AND CMAKE_BUILD_TYPE STREQUAL "Release")
set(CODESIGN_IDENTITY "Developer ID Application: Your Name")
add_custom_command(TARGET MyPlugin POST_BUILD
COMMAND codesign --force
--sign "${CODESIGN_IDENTITY}"
--options runtime
--entitlements "${CMAKE_SOURCE_DIR}/Resources/Entitlements.plist"
--timestamp
$<TARGET_BUNDLE_DIR:MyPlugin>
COMMENT "Code signing ${TARGET}"
)
endif()
```
### Notarization
Required for macOS 10.15+ (Catalina and later).
#### Setup
1. Create app-specific password at [appleid.apple.com](https://appleid.apple.com)
2. Store credentials in keychain:
```bash
xcrun notarytool store-credentials "notary-profile" \
--apple-id "developer@example.com" \
--team-id "TEAM_ID" \
--password "xxxx-xxxx-xxxx-xxxx"
```
#### Notarize Plugin
```bash
# 1. Create ZIP for notarization
ditto -c -k --keepParent MyPlugin.vst3 MyPlugin-vst3.zip
# 2. Submit to notary service
xcrun notarytool submit MyPlugin-vst3.zip \
--keychain-profile "notary-profile" \
--wait
# 3. If successful, staple the ticket
xcrun stapler staple MyPlugin.vst3
# 4. Verify
spctl -a -vvv -t install MyPlugin.vst3
xcrun stapler validate MyPlugin.vst3
```
#### Troubleshooting Notarization
Check submission status:
```bash
xcrun notarytool info <submission-id> --keychain-profile "notary-profile"
```
View detailed log:
```bash
xcrun notarytool log <submission-id> --keychain-profile "notary-profile"
```
Common issues:
- **Missing entitlements**: Add to Entitlements.plist
- **Unsigned nested binaries**: Sign all frameworks before parent bundle
- **Invalid bundle structure**: Verify with `pkgutil --check-signature`
### AU Validation
```bash
# Validate AU (required for App Store distribution)
auval -v aufx Plug Manu
# Output should end with "PASSED"
```
Fix common AU validation errors:
- **"Could not open component"**: Check bundle ID and AU type
- **"Plugin crash"**: Debug in Xcode, check for exceptions in initialization
- **"Latency reporting"**: Implement `getTailLengthSeconds()` correctly
---
## 3. Windows Builds
### Prerequisites
1. **Visual Studio 2022** (Community, Professional, or Enterprise)
- Install "Desktop development with C++" workload
- Includes Windows 10 SDK
2. **CMake** (3.22+)
```powershell
# Via Chocolatey
choco install cmake
# Or download from cmake.org
```
3. **Code Signing Certificate** (optional, for distribution)
- EV or standard code signing certificate
- From vendors: DigiCert, Sectigo, GlobalSign
### Building
```powershell
# Configure for Visual Studio 2022
cmake -B build-win -G "Visual Studio 17 2022" -A x64
# Build Release
cmake --build build-win --config Release --parallel
# Or open in Visual Studio
start build-win/MyPlugin.sln
```
### MSVC Runtime Linking
**Static Runtime** (recommended for plugins):
```cmake
# Statically link MSVC runtime (no DLL dependencies)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
```
**Dynamic Runtime** (smaller binary, requires MSVC redistributable):
```cmake
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
```
### Code Signing
#### Manual Signing with signtool
```powershell
# Sign with PFX file
signtool sign /f certificate.pfx /p <password> `
/tr http://timestamp.digicert.com `
/td sha256 /fd sha256 `
MyPlugin.vst3
# Sign with certificate store
signtool sign /n "Your Company Name" `
/tr http://timestamp.digicert.com `
/td sha256 /fd sha256 `
MyPlugin.vst3
# Verify signature
signtool verify /pa /v MyPlugin.vst3
```
#### Automated Signing in CMake
```cmake
if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Release")
find_program(SIGNTOOL_EXECUTABLE signtool
PATHS "C:/Program Files (x86)/Windows Kits/10/bin/*/x64"
)
if(SIGNTOOL_EXECUTABLE)
add_custom_command(TARGET MyPlugin POST_BUILD
COMMAND ${SIGNTOOL_EXECUTABLE} sign
/f "${CMAKE_SOURCE_DIR}/certificate.pfx"
/p "$ENV{CERT_PASSWORD}"
/tr http://timestamp.digicert.com
/td sha256 /fd sha256
$<TARGET_FILE:MyPlugin>
COMMENT "Code signing ${TARGET}"
)
endif()
endif()
```
### Visual Studio Configuration
#### Optimization Settings
```cmake
if(MSVC)
# Enable whole program optimization (Release)
target_compile_options(MyPlugin PRIVATE
$<$<CONFIG:Release>:/GL> # Whole program optimization
/MP # Multi-processor compilation
)
target_link_options(MyPlugin PRIVATE
$<$<CONFIG:Release>:/LTCG> # Link-time code generation
)
endif()
```
#### Suppress Warnings
```cmake
target_compile_definitions(MyPlugin PRIVATE
_CRT_SECURE_NO_WARNINGS # Disable CRT security warnings
NOMINMAX # Prevent min/max macros
)
```
---
## 4. Linux Builds
### Prerequisites
**Ubuntu/Debian:**
```bash
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
libasound2-dev \
libjack-jackd2-dev \
libfreetype6-dev \
libx11-dev \
libxrandr-dev \
libxinerama-dev \
libxcursor-dev \
libgl1-mesa-dev \
libcurl4-openssl-dev
```
**Fedora/RHEL:**
```bash
sudo dnf install -y \
gcc-c++ \
cmake \
alsa-lib-devel \
jack-audio-connection-kit-devel \
freetype-devel \
libX11-devel \
libXrandr-devel \
libXinerama-devel \
libXcursor-devel \
mesa-libGL-devel \
libcurl-devel
```
### Building
```bash
# Configure
cmake -B build-linux -DCMAKE_BUILD_TYPE=Release
# Build
cmake --build build-linux --config Release --parallel
# Install to system (optional)
sudo cmake --install build-linux
```
### Packaging
#### Create .tar.gz
```bash
tar -czf MyPlugin-1.0.0-Linux-x86_64.tar.gz \
-C build-linux/MyPlugin_artefacts/Release/VST3 \
MyPlugin.vst3
```
#### Create .deb Package
```bash
# Install packaging tools
sudo apt-get install checkinstall
# Create .deb
sudo checkinstall \
--pkgname=myplugin \
--pkgversion=1.0.0 \
--pkgrelease=1 \
--pkggroup=sound \
--maintainer="you@example.com" \
cmake --install build-linux
```
---
## 5. AAX Format (Pro Tools)
### Prerequisites
1. **AAX SDK** (requires iLok account)
- Sign up at [developer.avid.com](https://developer.avid.com)
- Download AAX SDK
- Extract to `SDKs/AAX/`
2. **PACE Licensing** (for distribution)
- Create account at [paceap.com](https://www.paceap.com)
- Use PACE Eden for signing (replaces codesign for AAX)
### CMake Configuration
```cmake
# Set AAX SDK path
juce_set_aax_sdk_path("${CMAKE_CURRENT_SOURCE_DIR}/SDKs/AAX")
# Add AAX to plugin formats
set(PLUGIN_FORMATS VST3 AU AAX Standalone)
```
### Building AAX
```bash
# macOS
cmake -B build-mac -DAAX_SDK_PATH=SDKs/AAX
cmake --build build-mac --config Release
# Windows
cmake -B build-win -DAAX_SDK_PATH=SDKs/AAX
cmake --build build-win --config Release
```
### AAX Signing with PACE Eden
AAX plugins **must** be signed with PACE Eden (not regular codesign).
```bash
# Sign AAX (macOS/Windows)
wraptool sign \
--account <your-pace-account> \
--password <password> \
--signid <signid> \
--in MyPlugin.aaxplugin \
--out MyPlugin-signed.aaxplugin
# Verify
wraptool verify --verbose MyPlugin-signed.aaxplugin
```
**Note**: Keep AAX signing credentials secure. Never commit to version control.
---
## 6. Continuous Integration
### GitHub Actions Workflow
**.github/workflows/build.yml:**
```yaml
name: Build Plugin
on: [push, pull_request]
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS
os: macos-latest
cmake_args: -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
- name: Windows
os: windows-latest
cmake_args: -G "Visual Studio 17 2022" -A x64
- name: Linux
os: ubuntu-latest
cmake_args: ""
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libasound2-dev libjack-jackd2-dev \
libfreetype6-dev libx11-dev libxrandr-dev libxinerama-dev \
libxcursor-dev libgl1-mesa-dev
- name: Configure
run: cmake -B build ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --config Release --parallel
- name: Test
run: ctest --test-dir build -C Release --output-on-failure
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.name }}
path: |
build/*_artefacts/Release/VST3/*.vst3
build/*_artefacts/Release/AU/*.component
```
### Secrets for Code Signing
Store signing credentials in GitHub Secrets:
1. Go to repository **Settings → Secrets → Actions**
2. Add secrets:
- `MACOS_CERTIFICATE_BASE64`: Base64-encoded .p12 file
- `MACOS_CERTIFICATE_PASSWORD`: Certificate password
- `APPLE_ID`: Apple ID for notarization
- `APPLE_TEAM_ID`: Developer team ID
- `APPLE_APP_PASSWORD`: App-specific password
- `WINDOWS_CERTIFICATE_BASE64`: Base64-encoded .pfx file
- `WINDOWS_CERTIFICATE_PASSWORD`: Certificate password
### Automated Code Signing in CI
**macOS:**
```yaml
- name: Import Certificate
env:
CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
echo $CERTIFICATE_BASE64 | base64 --decode > certificate.p12
security create-keychain -p temp build.keychain
security import certificate.p12 -k build.keychain -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-keychain-settings -lut 21600 build.keychain
security unlock-keychain -p temp build.keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k temp build.keychain
- name: Sign and Notarize
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
codesign --force --sign "Developer ID Application" --options runtime MyPlugin.vst3
xcrun notarytool submit MyPlugin.vst3.zip --apple-id $APPLE_ID --team-id $APPLE_TEAM_ID --password $APPLE_APP_PASSWORD --wait
xcrun stapler staple MyPlugin.vst3
```
**Windows:**
```yaml
- name: Import Certificate
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
[System.Convert]::FromBase64String($env:CERTIFICATE_BASE64) | Set-Content -Path certificate.pfx -Encoding Byte
certutil -importpfx -p $env:CERTIFICATE_PASSWORD certificate.pfx
- name: Sign Binary
run: |
signtool sign /f certificate.pfx /p $env:CERTIFICATE_PASSWORD /tr http://timestamp.digicert.com /td sha256 /fd sha256 MyPlugin.vst3
```
---
## 7. Reproducible Builds
### Deterministic Builds
Ensure builds are reproducible across machines:
1. **Pin JUCE version** (use git submodule or specific release)
```bash
git submodule add https://github.com/juce-framework/JUCE.git
cd JUCE && git checkout 7.0.9
```
2. **Lock dependency versions** (CMake FetchContent)
```cmake
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0 # Specific version
)
```
3. **Document toolchain versions** (README.md)
```markdown
Build Requirements:
- CMake 3.22+
- JUCE 7.0.9
- macOS: Xcode 14.3+
- Windows: Visual Studio 2022
- Linux: GCC 11+ or Clang 14+
```
4. **Disable timestamp embedding**
```cmake
# Remove __DATE__ and __TIME__ macros
target_compile_definitions(MyPlugin PRIVATE
NO_BUILD_TIMESTAMP=1
)
```
### Build Verification
Generate checksums for reproducibility:
```bash
# macOS/Linux
shasum -a 256 MyPlugin.vst3 > checksums.txt
# Windows
certutil -hashfile MyPlugin.vst3 SHA256 >> checksums.txt
```
---
## 8. Troubleshooting
### Common Build Errors
#### "JUCE modules not found"
```
Solution:
git submodule update --init --recursive
```
#### "Symbol not found" (macOS)
```
Solution:
- Check deployment target matches minimum system requirement
- Verify all symbols are available in target SDK
- Use `nm` to inspect missing symbols:
nm -gU MyPlugin.vst3/Contents/MacOS/MyPlugin | grep <symbol>
```
#### "Unresolved external symbol" (Windows)
```
Solution:
- Ensure all .cpp files are in CMakeLists.txt
- Check library linking order
- Verify static/dynamic runtime consistency (/MT vs /MD)
```
#### "Undefined reference" (Linux)
```
Solution:
- Install missing libraries (libasound2-dev, etc.)
- Add libraries to target_link_libraries()
- Check pkg-config: pkg-config --libs alsa
```
### Plugin Doesn't Load in DAW
**macOS:**
1. Check signing: `codesign --verify --deep --strict MyPlugin.vst3`
2. Verify notarization: `spctl -a -vvv -t install MyPlugin.vst3`
3. Check Gatekeeper: `xattr -l MyPlugin.vst3` (remove quarantine if needed)
4. AU validation: `auval -v aufx Plug Manu`
**Windows:**
1. Check dependencies: Use [Dependency Walker](http://www.dependencywalker.com/)
2. Verify signature: `signtool verify /pa MyPlugin.vst3`
3. Check registry (for VST3): `Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Classes\VST3`
**Linux:**
1. Check shared library dependencies: `ldd MyPlugin.vst3`
2. Verify VST3 path: `~/.vst3/` or `/usr/lib/vst3/`
3. Check permissions: `chmod 755 MyPlugin.vst3`
---
## 9. Best Practices
### Version Management
```cmake
project(MyPlugin VERSION 1.2.3)
# Access in code
target_compile_definitions(MyPlugin PRIVATE
PLUGIN_VERSION="${CMAKE_PROJECT_VERSION}"
)
```
### Conditional Compilation
```cpp
#if JUCE_MAC
// macOS-specific code
#elif JUCE_WINDOWS
// Windows-specific code
#elif JUCE_LINUX
// Linux-specific code
#endif
#if JUCE_DEBUG
// Debug-only code
#endif
```
### Minimize Plugin Size
- **Strip symbols** in Release builds
- **Enable LTO** (link-time optimization)
- **Remove unused JUCE modules**
- **Compress resources** (images, fonts)
### Cross-Platform File Paths
```cpp
// Use JUCE File class for portability
juce::File presetFolder = juce::File::getSpecialLocation(
juce::File::userApplicationDataDirectory
).getChildFile("MyPlugin").getChildFile("Presets");
// Not hardcoded paths like:
// "C:\\Users\\...\\Presets" ❌
```
---
## Summary
**Key Takeaways:**
1. **Use CMake** for cross-platform builds - single configuration for all platforms
2. **Code signing is essential** for distribution (macOS requires notarization)
3. **Test on all platforms** - behavior can differ (especially AU vs VST3)
4. **Automate in CI/CD** - GitHub Actions, GitLab CI, or Jenkins
5. **Reproducible builds** - pin dependency versions, document toolchain
**Platform Checklist:**
- [ ] macOS: Universal binary (arm64 + x86_64)
- [ ] macOS: Code signed with Developer ID
- [ ] macOS: Notarized (10.15+ requirement)
- [ ] macOS: AU validation passes (`auval`)
- [ ] Windows: Code signed (recommended)
- [ ] Windows: Static runtime linked (/MT)
- [ ] Linux: Dependencies documented
- [ ] All: Tested in major DAWs on each platform
---
**Related Resources:**
- `/release-build` command - Automated release workflow
- BUILD_GUIDE.md - Detailed build procedures
- RELEASE_CHECKLIST.md - Pre-release validation steps
- @build-engineer - CI/CD and build automation expert

View File

@@ -0,0 +1,992 @@
---
name: daw-compatibility-guide
description: DAW-specific quirks, known issues, and workarounds for Logic Pro, Ableton Live, Pro Tools, Cubase, Reaper, FL Studio, Bitwig with format-specific requirements (AU/VST3/AAX). Use when troubleshooting DAW compatibility, fixing host-specific bugs, implementing DAW workarounds, passing auval validation, or debugging automation issues.
allowed-tools: Read, Grep, Glob
---
# DAW Compatibility Guide
Comprehensive guide to DAW-specific quirks, known issues, and workarounds for ensuring your JUCE plugin works correctly across all major digital audio workstations.
## Overview
Each DAW has unique behaviors, assumptions, and quirks that can affect plugin operation. This guide documents common issues and proven solutions for Logic Pro, Ableton Live, Pro Tools, Cubase, Reaper, FL Studio, Bitwig, and others.
## When to Use This Guide
- Debugging DAW-specific issues reported by users
- Testing plugin compatibility across multiple DAWs
- Implementing DAW-specific workarounds
- Understanding format-specific requirements (AU, VST3, AAX)
- Planning cross-DAW automation and state compatibility
---
## Logic Pro (macOS - AU/VST3)
### Overview
- **Formats:** Audio Unit (preferred), VST3
- **Strictness:** Very strict AU validation (`auval`)
- **Automation:** Sample-accurate, works well
- **Unique Features:** AU-specific validation, side-chain routing
### AU Validation Requirements
**Issue:** Logic requires plugins to pass `auval` validation.
**Requirements:**
```cpp
// PluginProcessor.h - Ensure these are set correctly
#define JucePlugin_Name "MyPlugin"
#define JucePlugin_Desc "Description"
#define JucePlugin_Manufacturer "YourCompany"
#define JucePlugin_ManufacturerCode 'Manu' // 4 characters, unique!
#define JucePlugin_PluginCode 'Plug' // 4 characters, unique!
#define JucePlugin_AUMainType 'aufx' // Effect
// OR 'aumu' for MIDI effect
// OR 'aumf' for instrument
#define JucePlugin_AUSubType JucePlugin_PluginCode
#define JucePlugin_AUExportPrefix MyPluginAU
#define JucePlugin_AUExportPrefixQuoted "MyPluginAU"
```
**Validation Command:**
```bash
auval -v aufx Plug Manu
```
**Common auval Failures:**
1. **Failure: "FATAL ERROR: AudioUnitInitialize failed"**
```cpp
// Cause: prepareToPlay() throws exception or asserts
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
// ❌ Don't do this:
jassert(sampleRate == 44100.0); // Fails in auval!
// ✅ Do this:
if (sampleRate < 8000.0 || sampleRate > 192000.0)
return; // Handle gracefully
}
```
2. **Failure: "FATAL ERROR: Property size is incorrect"**
```cpp
// Cause: Incorrect I/O configuration
bool isBusesLayoutSupported(const BusesLayout& layouts) const override {
// Be permissive for auval
if (layouts.getMainOutputChannelSet().isDisabled())
return false; // Must have output
return true; // Allow any channel config for auval
}
```
3. **Failure: "FATAL ERROR: Render called with null buffer"**
```cpp
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
// auval sometimes passes zero-size buffers
if (buffer.getNumSamples() == 0)
return; // Early exit for empty blocks
}
```
**Run auval successfully:**
```bash
# Full validation (takes ~5 minutes)
auval -v aufx Plug Manu
# Strict validation (recommended)
auval -strict -v aufx Plug Manu
# If it passes, you'll see:
# * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# AU VALIDATION SUCCEEDED
# * * * * * * * * * * * * * * * * * * * * * * * * * * * *
```
### Logic-Specific Issues
**Issue: Automation writes incorrectly or doesn't play back**
**Cause:** Parameter smoothing interfering with Logic's automation.
**Solution:**
```cpp
// Don't smooth parameters that Logic is automating
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
// ❌ Always smoothing
float cutoff = smoother.getNextValue(*cutoffParam);
// ✅ Only smooth if parameter changed recently
if (cutoffParam->load() != lastCutoffValue) {
smoother.setTargetValue(*cutoffParam);
lastCutoffValue = *cutoffParam;
}
float cutoff = smoother.getNextValue();
}
```
**Issue: Offline bounce doesn't match realtime**
**Cause:** Tempo/time info assumptions.
**Solution:**
```cpp
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
// ✅ Always query tempo from host
auto playHead = getPlayHead();
if (playHead != nullptr) {
juce::AudioPlayHead::CurrentPositionInfo posInfo;
playHead->getCurrentPosition(posInfo);
float bpm = posInfo.bpm;
double ppqPosition = posInfo.ppqPosition;
bool isPlaying = posInfo.isPlaying;
// Use this info, don't cache tempo!
}
}
```
**Issue: Side-chain input doesn't work (AU)**
**Cause:** AU side-chain requires special bus configuration.
**Solution:**
```cpp
MyPluginProcessor::MyPluginProcessor()
: AudioProcessor(BusesProperties()
.withInput ("Input", AudioChannelSet::stereo(), true)
.withOutput("Output", AudioChannelSet::stereo(), true)
.withInput ("Sidechain", AudioChannelSet::stereo(), false)) // Optional
{
}
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
// Get sidechain bus
auto mainInputOutput = getBusBuffer(buffer, true, 0);
auto sidechain = getBusBuffer(buffer, true, 1); // Second input bus
if (!sidechain.getNumChannels())
return; // No sidechain connected
// Process with sidechain...
}
```
---
## Ableton Live (VST3/AU)
### Overview
- **Formats:** VST3 (Windows/macOS), AU (macOS)
- **Automation:** Works well, supports breakpoint automation
- **Unique Features:** Max for Live integration, Device Racks, Macro mapping
### Live-Specific Issues
**Issue: Plugin doesn't appear in Live's browser**
**Cause:** VST3 not in correct folder or not scanned.
**Solution:**
```bash
# macOS VST3 path
~/Library/Audio/Plug-Ins/VST3/MyPlugin.vst3
# Windows VST3 path
C:\Program Files\Common Files\VST3\MyPlugin.vst3
# Rescan in Live:
# Preferences → Plug-Ins → "Rescan" button
```
**Issue: Undo/Redo causes parameter jumps**
**Cause:** Live's undo system conflicts with plugin parameter changes.
**Solution:**
```cpp
// Mark parameter changes as gestures to prevent undo conflicts
void parameterValueChanged(int parameterIndex, float newValue) override {
// Notify host of parameter change
auto* param = getParameters()[parameterIndex];
param->beginChangeGesture();
param->setValueNotifyingHost(newValue);
param->endChangeGesture();
}
```
**Issue: Plugin latency not compensated correctly**
**Cause:** Plugin doesn't report latency.
**Solution:**
```cpp
int getLatencySamples() const override {
return latencyInSamples; // Report actual latency
}
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
// Update latency if it changes
latencyInSamples = calculateLatency();
setLatencySamples(latencyInSamples);
}
```
**Issue: Freeze/Flatten produces incorrect audio**
**Cause:** Non-deterministic behavior or offline/realtime mismatch.
**Solution:**
- Ensure processBlock is deterministic
- Reset all state in prepareToPlay()
- Don't use system time or random numbers without seeding
```cpp
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
// ✅ Reset all state for deterministic processing
filter.reset();
envelope.reset();
rng.setSeed(12345); // Seeded random for determinism
}
```
**Issue: Macro mapping doesn't work**
**Cause:** Parameter range or normalization issues.
**Solution:**
```cpp
// Ensure parameters use normalized 0-1 range internally
auto param = std::make_unique<AudioParameterFloat>(
"cutoff",
"Cutoff",
NormalisableRange<float>(20.0f, 20000.0f, 0.01f, 0.3f), // Skew
1000.0f
);
// Live's macros expect normalized parameter access to work
```
---
## Pro Tools (AAX)
### Overview
- **Format:** AAX (PACE/iLok signed)
- **Strictness:** Very strict, requires code signing
- **Automation:** Sample-accurate, very robust
- **Unique Features:** AudioSuite (offline processing), HDX DSP
### AAX Requirements
**Issue: Plugin doesn't load - "damaged" or "unsigned" error**
**Cause:** AAX requires PACE signing with iLok account.
**Solution:**
1. Sign up for PACE iLok developer account
2. Get Developer ID certificate from PACE
3. Sign AAX bundle:
```bash
wraptool sign --verbose \
--account <your-ilok-account> \
--password <password> \
--wcguid <your-wcguid> \
--dsig1-compat \
--in MyPlugin.aaxplugin \
--out MyPlugin.aaxplugin
```
**AAX Manifest:**
```cpp
// Required in CMakeLists.txt
juce_add_plugin(MyPlugin
COMPANY_NAME "YourCompany"
PLUGIN_MANUFACTURER_CODE Manu
PLUGIN_CODE Plug
FORMATS AAX
AAX_IDENTIFIER com.yourcompany.myplugin
)
```
### Pro Tools-Specific Issues
**Issue: Plugin doesn't appear in correct category**
**Cause:** AAX category not set.
**Solution:**
```cpp
#define JucePlugin_AAXCategory AAX_ePlugInCategory_EQ // For EQ
// Other categories:
// AAX_ePlugInCategory_Dynamics
// AAX_ePlugInCategory_PitchShift
// AAX_ePlugInCategory_Reverb
// AAX_ePlugInCategory_Delay
// AAX_ePlugInCategory_Modulation
// AAX_ePlugInCategory_Harmonic
// AAX_ePlugInCategory_SWGenerators
```
**Issue: AudioSuite (offline processing) produces different results**
**Cause:** AudioSuite processes entire selection at once.
**Solution:**
```cpp
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
// AudioSuite may pass very large buffers (entire selection)
// Must handle variable buffer sizes gracefully
int numSamples = buffer.getNumSamples();
// Process in chunks if needed for stability
const int chunkSize = 512;
for (int start = 0; start < numSamples; start += chunkSize) {
int count = std::min(chunkSize, numSamples - start);
AudioBuffer<float> chunk(buffer.getArrayOfWritePointers(),
buffer.getNumChannels(),
start, count);
processChunk(chunk);
}
}
```
**Issue: Session doesn't restore plugin state correctly**
**Cause:** State serialization issue specific to AAX.
**Solution:**
```cpp
void getStateInformation(MemoryBlock& destData) override {
// AAX is very strict about state format
// Use XML or ValueTree (both work reliably)
auto state = apvts.copyState();
std::unique_ptr<XmlElement> xml(state.createXml());
// Ensure proper encoding
copyXmlToBinary(*xml, destData);
}
```
**Issue: Delay compensation not working**
**Cause:** Pro Tools requires explicit latency reporting.
**Solution:**
```cpp
// Report latency during initialization
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
int latency = calculatePluginLatency();
setLatencySamples(latency);
}
// Update if latency changes (rare)
void parameterChanged(const String& paramID, float newValue) override {
if (paramID == "quality" && qualityAffectsLatency) {
int newLatency = calculatePluginLatency();
setLatencySamples(newLatency);
}
}
```
---
## FL Studio (Windows - VST3)
### Overview
- **Format:** VST3 (VST2 deprecated)
- **Automation:** Works but has quirks
- **Unique Features:** Piano Roll, native wrapper
### FL Studio-Specific Issues
**Issue: Plugin state lost on crash**
**Cause:** FL Studio caches state, but doesn't always flush on crash.
**Solution:**
```cpp
// Save state more frequently
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
static int counter = 0;
if (++counter % 44100 == 0) { // Every ~1 second @ 44.1kHz
updateHostDisplay(); // Hint to FL to save state
}
}
```
**Issue: Multiple instances interfere with each other**
**Cause:** Shared static data or singletons.
**Solution:**
```cpp
// ❌ Don't use static/global state
static float globalGain = 1.0f; // BAD! Shared across instances
// ✅ Instance variables only
class MyPluginProcessor : public AudioProcessor {
float instanceGain = 1.0f; // Each instance has its own
};
```
**Issue: Wrapper shows generic UI instead of custom UI**
**Cause:** FL's wrapper can create generic UI if custom editor fails.
**Solution:**
```cpp
AudioProcessorEditor* createEditor() override {
// Ensure editor constructor doesn't throw
try {
return new MyPluginEditor(*this);
} catch (...) {
jassertfalse; // Debug: why did it fail?
return nullptr; // FL will show generic UI
}
}
```
**Issue: Preset browser doesn't show presets**
**Cause:** FL looks for presets in specific format/location.
**Solution:**
```bash
# FL Studio preset path (Windows)
Documents\Image-Line\FL Studio\Presets\Plugin database\Generators\[Your Plugin]
# Save presets as .fst (FL's format)
# Or implement VST3 preset format (.vstpreset)
```
---
## Cubase/Nuendo (VST3)
### Overview
- **Format:** VST3 (Steinberg's own format)
- **Automation:** Excellent, sample-accurate
- **Unique Features:** Expression maps, VST3 spec reference implementation
### Cubase-Specific Issues
**Issue: Plugin doesn't load or shows "failed to load" error**
**Cause:** VST3 bundle structure incorrect.
**Solution:**
```bash
# Correct VST3 bundle structure:
MyPlugin.vst3/
Contents/
Resources/ # Optional: icons, documentation
x86_64-win/
MyPlugin.vst3 # Windows 64-bit
x86-win/
MyPlugin.vst3 # Windows 32-bit (optional)
MacOS/
MyPlugin # macOS universal binary (arm64 + x86_64)
# Verify bundle with:
pluginval --strictness-level 10 MyPlugin.vst3
```
**Issue: Automation doesn't write or playback correctly**
**Cause:** Parameter flags not set correctly.
**Solution:**
```cpp
auto param = std::make_unique<AudioParameterFloat>(
"cutoff",
"Cutoff",
NormalisableRange<float>(20.0f, 20000.0f),
1000.0f
);
// Ensure automation is enabled
// (JUCE does this by default, but verify)
```
**Issue: Side-chain routing doesn't work**
**Cause:** VST3 side-chain requires specific bus configuration.
**Solution:**
```cpp
MyPluginProcessor()
: AudioProcessor(BusesProperties()
.withInput("Input", AudioChannelSet::stereo(), true)
.withOutput("Output", AudioChannelSet::stereo(), true)
.withInput("Sidechain", AudioChannelSet::stereo(), false)) // Aux input
{
}
bool isBusesLayoutSupported(const BusesLayout& layouts) const override {
// Main input/output must match
if (layouts.getMainInputChannelSet() != layouts.getMainOutputChannelSet())
return false;
// Sidechain is optional
return true;
}
```
**Issue: Expression maps don't trigger MIDI correctly**
**Cause:** Plugin doesn't handle MIDI correctly.
**Solution:**
```cpp
void processBlock(AudioBuffer<float>& buffer, MidiBuffer& midi) override {
// Ensure MIDI messages are processed at correct sample positions
for (const auto metadata : midi) {
auto message = metadata.getMessage();
int samplePosition = metadata.samplePosition;
// Process MIDI at exact sample position
handleMidiMessage(message, samplePosition);
}
}
```
---
## Reaper (VST3/AU)
### Overview
- **Formats:** VST3 (all platforms), AU (macOS)
- **Automation:** Highly flexible, supports both VST3 and AU
- **Unique Features:** Extremely permissive, good for testing
### Reaper-Specific Issues
**Issue: Plugin loads but doesn't process audio**
**Cause:** Reaper allows unusual configurations that other DAWs don't.
**Solution:**
```cpp
bool isBusesLayoutSupported(const BusesLayout& layouts) const override {
// Reaper may try unusual layouts - be permissive
auto mainIn = layouts.getMainInputChannelSet();
auto mainOut = layouts.getMainOutputChannelSet();
// Require at least mono or stereo
if (mainIn == AudioChannelSet::disabled() ||
mainOut == AudioChannelSet::disabled())
return false;
// Allow flexible channel counts
return true;
}
```
**Issue: Offline render (Export) doesn't match realtime**
**Cause:** Reaper's offline render can use different buffer sizes.
**Solution:**
```cpp
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
// Don't assume fixed buffer size - handle variable sizes
maxBufferSize = samplesPerBlock;
// Allocate for worst case
workBuffer.setSize(2, samplesPerBlock);
}
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
// Handle any buffer size up to max
jassert(buffer.getNumSamples() <= maxBufferSize);
}
```
**Issue: Plugin delay compensation incorrect**
**Cause:** Reaper is very strict about latency reporting.
**Solution:**
```cpp
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
// Calculate and report latency accurately
int latency = fftSize / 2; // Example: FFT-based effect
setLatencySamples(latency);
}
// If latency changes dynamically
void setFFTSize(int newSize) {
fftSize = newSize;
setLatencySamples(fftSize / 2);
// Reaper will adjust compensation
}
```
---
## Bitwig Studio (VST3)
### Overview
- **Format:** VST3
- **Automation:** Excellent, supports modulation
- **Unique Features:** Modulation system, Grid, Operator devices
### Bitwig-Specific Issues
**Issue: Bitwig's modulators don't affect plugin parameters**
**Cause:** Parameter update rate too slow.
**Solution:**
```cpp
// Bitwig can modulate at audio rate - ensure parameters respond
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
auto cutoffParam = apvts.getRawParameterValue("cutoff");
// Read parameter every sample if Bitwig is modulating
for (int i = 0; i < buffer.getNumSamples(); ++i) {
float cutoff = cutoffParam->load(); // Audio-rate read
// Process sample with current value
}
}
// Or use parameter smoothing:
smoother.reset(sampleRate, 0.005); // 5ms smoothing
for (int i = 0; i < buffer.getNumSamples(); ++i) {
float cutoff = smoother.getNextValue(cutoffParam->load());
}
```
**Issue: Plugin conflicts with Bitwig's Grid devices**
**Cause:** Unusual buffer configurations.
**Solution:**
```cpp
// Be very permissive with channel layouts for Bitwig
bool isBusesLayoutSupported(const BusesLayout& layouts) const override {
// Bitwig may use unusual channel counts for CV/modulation
return !layouts.getMainOutputChannelSet().isDisabled();
}
```
---
## Studio One (VST3)
### Overview
- **Format:** VST3
- **Automation:** Works well
- **Unique Features:** Scratch pads, arranger track
### Studio One-Specific Issues
**Issue: Plugin doesn't save/recall with song**
**Cause:** State information issue.
**Solution:**
```cpp
void getStateInformation(MemoryBlock& destData) override {
// Studio One is strict about state consistency
auto state = apvts.copyState();
std::unique_ptr<XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, destData);
}
void setStateInformation(const void* data, int sizeInBytes) override {
// Validate before loading
std::unique_ptr<XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
if (!xml || !xml->hasTagName(apvts.state.getType()))
return; // Invalid state, don't crash
apvts.replaceState(ValueTree::fromXml(*xml));
}
```
---
## Common Cross-DAW Issues
### Issue: Plugin crashes on load in specific DAW
**Debugging Steps:**
1. Check Console.app (macOS) or Event Viewer (Windows) for crash logs
2. Run plugin in debugger attached to DAW
3. Use Address Sanitizer to detect memory errors:
```bash
cmake -B build -DCMAKE_CXX_FLAGS="-fsanitize=address"
```
4. Verify thread safety - most crashes are threading issues
**Common Causes:**
- Accessing UI from audio thread (or vice versa)
- Static initialization order issues
- Missing null checks
- Buffer overruns
**Solution Template:**
```cpp
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
// ✅ Validate inputs
if (buffer.getNumSamples() == 0)
return;
if (buffer.getNumChannels() == 0)
return;
// ✅ Null checks for all pointers
if (auto* param = cutoffParam.load())
float cutoff = param->load();
// ✅ Bounds checking
jassert(buffer.getNumSamples() <= maxBufferSize);
}
```
### Issue: Automation sounds different in different DAWs
**Cause:** Different automation smoothing or timing.
**Solution:**
```cpp
// Implement your own parameter smoothing
class ParameterSmoother {
public:
void reset(double sampleRate, double timeSeconds = 0.05) {
rampLength = static_cast<int>(sampleRate * timeSeconds);
currentValue = targetValue = 0.0f;
counter = 0;
}
void setTarget(float target) {
targetValue = target;
counter = rampLength;
}
float getNext() {
if (counter > 0) {
currentValue += (targetValue - currentValue) / counter;
--counter;
} else {
currentValue = targetValue;
}
return currentValue;
}
private:
float currentValue = 0.0f, targetValue = 0.0f;
int rampLength = 0, counter = 0;
};
// Use consistently across all DAWs
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) override {
smoother.setTarget(cutoffParam->load());
for (int i = 0; i < buffer.getNumSamples(); ++i) {
float smoothedCutoff = smoother.getNext();
// Use smoothedCutoff for consistent automation across DAWs
}
}
```
### Issue: State doesn't transfer between DAW sessions
**Cause:** Incompatible serialization formats.
**Solution:**
```cpp
// Use JUCE's ValueTree for reliable cross-DAW state
void getStateInformation(MemoryBlock& destData) override {
ValueTree state("PluginState");
state.setProperty("version", 1, nullptr);
// Add parameter state
state.appendChild(apvts.copyState(), nullptr);
// Add custom state
ValueTree customState("CustomState");
customState.setProperty("uiWidth", uiWidth, nullptr);
state.appendChild(customState, nullptr);
// Serialize to XML (most compatible)
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("PluginState"))
return;
ValueTree state = ValueTree::fromXml(*xml);
// Check version for compatibility
int version = state.getProperty("version", 0);
if (version > 1)
return; // Future version, don't load
// Restore state...
}
```
---
## Format-Specific Considerations
### AU (Audio Unit) - macOS Only
**Advantages:**
- Native to macOS
- Best integration with Logic Pro, GarageBand
- Sample-accurate automation
**Disadvantages:**
- Strict validation (`auval`)
- macOS-only
- Limited to Apple ecosystem
**Best Practices:**
```cpp
// Always pass auval before shipping
// Test on multiple macOS versions (10.13+)
// Verify on both Intel and Apple Silicon
```
### VST3 - Cross-Platform
**Advantages:**
- Cross-platform (macOS, Windows, Linux)
- Open specification
- Supported by most DAWs
**Disadvantages:**
- Some DAWs still prefer AU (macOS)
- Complex specification
- Side-chain setup can be tricky
**Best Practices:**
```cpp
// Validate with pluginval at strictness level 10
// Test side-chain routing thoroughly
// Ensure bundle structure is correct
```
### AAX - Pro Tools Only
**Advantages:**
- Pro Tools integration
- Professional studios standard
**Disadvantages:**
- Requires PACE/iLok signing ($$)
- Pro Tools-only
- Strict requirements
**Best Practices:**
```bash
# Always code-sign AAX bundles
# Test AudioSuite mode separately
# Verify delay compensation
```
---
## Testing Strategy for DAW Compatibility
### Minimum Test Matrix
| DAW | Format | Platform | Priority |
|-----|--------|----------|----------|
| Logic Pro | AU, VST3 | macOS | High |
| Ableton Live | VST3 | macOS, Windows | High |
| Pro Tools | AAX | macOS, Windows | High |
| Reaper | VST3 | macOS, Windows, Linux | Medium |
| FL Studio | VST3 | Windows | Medium |
| Cubase | VST3 | macOS, Windows | Medium |
| Bitwig | VST3 | macOS, Windows, Linux | Low |
| Studio One | VST3 | macOS, Windows | Low |
### Quick Compatibility Checklist
For each DAW:
- ✅ Plugin loads without errors
- ✅ Audio processes correctly
- ✅ Automation writes and plays back
- ✅ State saves and restores
- ✅ Offline render matches realtime
- ✅ No crashes after 5 minutes of use
---
## Emergency Fixes for Specific DAWs
### If plugin works everywhere except Logic:
```bash
# Run auval and fix reported issues
auval -strict -v aufx Plug Manu
# Common fix: handle zero-size buffers
if (buffer.getNumSamples() == 0) return;
```
### If plugin works everywhere except FL Studio:
```cpp
// Check for shared static state
// Ensure each instance is independent
```
### If plugin works everywhere except Pro Tools:
```cpp
// Verify AAX signing
# Check certificate:
codesign --display --verbose=4 MyPlugin.aaxplugin
// Ensure AudioSuite handles large buffers
```
---
## Summary
**Key Takeaways:**
- Test on at least 3 major DAWs before release
- Use automated validation tools (auval, pluginval)
- Implement consistent parameter smoothing
- Handle edge cases gracefully (zero-size buffers, unusual layouts)
- Use standard state serialization (ValueTree → XML)
- Report latency accurately for delay compensation
- Be permissive with bus layouts for compatibility
**DAW-Specific Priorities:**
1. **Logic Pro (macOS):** Pass `auval` validation
2. **Ableton Live:** Test Freeze/Flatten and undo/redo
3. **Pro Tools:** Sign with PACE, test AudioSuite
4. **Reaper:** Handle flexible configurations
5. **FL Studio:** Avoid shared state between instances
---
## Related Resources
- **/run-daw-tests** command - Automated DAW compatibility testing
- **TESTING_STRATEGY.md** - Comprehensive testing approach
- **RELEASE_CHECKLIST.md** - Pre-release DAW validation
- JUCE Forum - Search for DAW-specific issues
---
**Remember:** Every DAW is different, but following best practices (realtime safety, robust state management, proper latency reporting) will prevent 90% of compatibility issues. The remaining 10% require DAW-specific testing and workarounds documented here.

View File

@@ -0,0 +1,704 @@
---
name: dsp-cookbook
description: Production-ready DSP algorithms including filters, compressors, delays, modulation effects, saturation, and distortion with JUCE integration and optimization techniques. Use when implementing audio processing, DSP algorithms, audio effects, dynamics processors, or need code examples for common audio operations.
---
# DSP Cookbook
Practical DSP algorithm implementations for audio plugins. Production-ready code examples with JUCE framework integration, covering filters, dynamics, modulation, delays, and common audio effects.
## Table of Contents
1. [Filters](#filters)
2. [Dynamics Processors](#dynamics-processors)
3. [Modulation Effects](#modulation-effects)
4. [Delay-Based Effects](#delay-based-effects)
5. [Saturation & Distortion](#saturation--distortion)
6. [Parameter Smoothing](#parameter-smoothing)
7. [Utility Functions](#utility-functions)
---
## Filters
### Biquad Filter (2nd Order IIR)
**Use for**: EQ, lowpass, highpass, bandpass, notch filters
```cpp
class BiquadFilter {
public:
enum class Type {
Lowpass,
Highpass,
Bandpass,
Notch,
Allpass,
PeakingEQ,
LowShelf,
HighShelf
};
void setCoefficients(Type type, float frequency, float sampleRate,
float Q = 0.707f, float gainDB = 0.0f) {
const float w0 = juce::MathConstants<float>::twoPi * frequency / sampleRate;
const float cosw0 = std::cos(w0);
const float sinw0 = std::sin(w0);
const float alpha = sinw0 / (2.0f * Q);
const float A = std::pow(10.0f, gainDB / 40.0f); // For shelf/peak
float b0, b1, b2, a0, a1, a2;
switch (type) {
case Type::Lowpass:
b0 = (1.0f - cosw0) / 2.0f;
b1 = 1.0f - cosw0;
b2 = (1.0f - cosw0) / 2.0f;
a0 = 1.0f + alpha;
a1 = -2.0f * cosw0;
a2 = 1.0f - alpha;
break;
case Type::Highpass:
b0 = (1.0f + cosw0) / 2.0f;
b1 = -(1.0f + cosw0);
b2 = (1.0f + cosw0) / 2.0f;
a0 = 1.0f + alpha;
a1 = -2.0f * cosw0;
a2 = 1.0f - alpha;
break;
case Type::Bandpass:
b0 = alpha;
b1 = 0.0f;
b2 = -alpha;
a0 = 1.0f + alpha;
a1 = -2.0f * cosw0;
a2 = 1.0f - alpha;
break;
case Type::PeakingEQ:
b0 = 1.0f + alpha * A;
b1 = -2.0f * cosw0;
b2 = 1.0f - alpha * A;
a0 = 1.0f + alpha / A;
a1 = -2.0f * cosw0;
a2 = 1.0f - alpha / A;
break;
// Add other types as needed...
}
// Normalize coefficients
coeffs.b0 = b0 / a0;
coeffs.b1 = b1 / a0;
coeffs.b2 = b2 / a0;
coeffs.a1 = a1 / a0;
coeffs.a2 = a2 / a0;
}
float processSample(float input) {
const float output = coeffs.b0 * input
+ coeffs.b1 * z1
+ coeffs.b2 * z2
- coeffs.a1 * y1
- coeffs.a2 * y2;
// Update state
z2 = z1;
z1 = input;
y2 = y1;
y1 = output;
return output;
}
void reset() {
z1 = z2 = y1 = y2 = 0.0f;
}
private:
struct Coefficients {
float b0 = 1.0f, b1 = 0.0f, b2 = 0.0f;
float a1 = 0.0f, a2 = 0.0f;
} coeffs;
float z1 = 0.0f, z2 = 0.0f; // Input delays
float y1 = 0.0f, y2 = 0.0f; // Output delays
};
```
**Usage:**
```cpp
BiquadFilter filter;
filter.setCoefficients(BiquadFilter::Type::Lowpass, 1000.0f, 48000.0f, 0.707f);
for (int i = 0; i < buffer.getNumSamples(); ++i) {
float input = buffer.getSample(0, i);
float output = filter.processSample(input);
buffer.setSample(0, i, output);
}
```
### State Variable Filter (SVF)
**Use for**: Smooth parameter changes, multimode filters
```cpp
class StateVariableFilter {
public:
enum class Mode { Lowpass, Highpass, Bandpass };
void prepare(double sampleRate) {
this->sampleRate = sampleRate;
}
void setParameters(float cutoff, float resonance, Mode mode) {
this->mode = mode;
// Calculate coefficients (Chamberlin SVF)
const float g = std::tan(juce::MathConstants<float>::pi * cutoff / sampleRate);
const float k = 2.0f - 2.0f * resonance; // resonance 0-1
a1 = 1.0f / (1.0f + g * (g + k));
a2 = g * a1;
a3 = g * a2;
}
float processSample(float input) {
const float v3 = input - ic2eq;
const float v1 = a1 * ic1eq + a2 * v3;
const float v2 = ic2eq + a2 * ic1eq + a3 * v3;
ic1eq = 2.0f * v1 - ic1eq;
ic2eq = 2.0f * v2 - ic2eq;
switch (mode) {
case Mode::Lowpass: return v2;
case Mode::Highpass: return input - k * v1 - v2;
case Mode::Bandpass: return v1;
default: return v2;
}
}
void reset() {
ic1eq = ic2eq = 0.0f;
}
private:
Mode mode = Mode::Lowpass;
double sampleRate = 44100.0;
float a1 = 0.0f, a2 = 0.0f, a3 = 0.0f;
float ic1eq = 0.0f, ic2eq = 0.0f; // Integrator state
};
```
---
## Dynamics Processors
### Compressor
**Use for**: Dynamics control, leveling, punchy mixes
```cpp
class Compressor {
public:
void prepare(double sampleRate) {
this->sampleRate = sampleRate;
envelope = 0.0f;
}
void setParameters(float thresholdDB, float ratio, float attackMs, float releaseMs) {
threshold = juce::Decibels::decibelsToGain(thresholdDB);
this->ratio = ratio;
// Calculate time constants
attackCoeff = std::exp(-1.0f / (attackMs * 0.001f * sampleRate));
releaseCoeff = std::exp(-1.0f / (releaseMs * 0.001f * sampleRate));
}
float processSample(float input) {
const float inputLevel = std::abs(input);
// Envelope follower
if (inputLevel > envelope)
envelope = attackCoeff * envelope + (1.0f - attackCoeff) * inputLevel;
else
envelope = releaseCoeff * envelope + (1.0f - releaseCoeff) * inputLevel;
// Compute gain reduction
float gainReduction = 1.0f;
if (envelope > threshold) {
const float excess = envelope / threshold;
gainReduction = std::pow(excess, 1.0f / ratio - 1.0f);
}
return input * gainReduction;
}
float getGainReductionDB() const {
return juce::Decibels::gainToDecibels(envelope > threshold
? std::pow(envelope / threshold, 1.0f / ratio - 1.0f)
: 1.0f);
}
void reset() {
envelope = 0.0f;
}
private:
double sampleRate = 44100.0;
float threshold = 1.0f;
float ratio = 4.0f;
float attackCoeff = 0.0f;
float releaseCoeff = 0.0f;
float envelope = 0.0f;
};
```
### Limiter (Look-Ahead)
```cpp
class Limiter {
public:
void prepare(double sampleRate, int maxBlockSize) {
this->sampleRate = sampleRate;
// Look-ahead buffer (5ms typical)
const int lookAheadSamples = static_cast<int>(0.005 * sampleRate);
delayBuffer.setSize(2, lookAheadSamples);
delayBuffer.clear();
writePos = 0;
}
void setThreshold(float thresholdDB) {
threshold = juce::Decibels::decibelsToGain(thresholdDB);
}
float processSample(float input, int channel) {
// Write to delay buffer
delayBuffer.setSample(channel, writePos, input);
// Read delayed sample
const float delayed = delayBuffer.getSample(channel, writePos);
// Analyze future peak
float peak = 0.0f;
for (int i = 0; i < delayBuffer.getNumSamples(); ++i) {
peak = std::max(peak, std::abs(delayBuffer.getSample(channel, i)));
}
// Calculate gain
float gain = 1.0f;
if (peak > threshold) {
gain = threshold / peak;
}
writePos = (writePos + 1) % delayBuffer.getNumSamples();
return delayed * gain;
}
void reset() {
delayBuffer.clear();
writePos = 0;
}
private:
double sampleRate = 44100.0;
float threshold = 1.0f;
juce::AudioBuffer<float> delayBuffer;
int writePos = 0;
};
```
---
## Modulation Effects
### Chorus
```cpp
class Chorus {
public:
void prepare(double sampleRate, int maxBlockSize) {
this->sampleRate = sampleRate;
// Delay line (50ms max)
const int bufferSize = static_cast<int>(0.05 * sampleRate);
delayBuffer.setSize(2, bufferSize);
delayBuffer.clear();
writePos = 0;
lfo.setSampleRate(sampleRate);
}
void setParameters(float rate, float depth, float mix) {
lfo.setFrequency(rate);
this->depth = depth;
this->mix = mix;
}
float processSample(float input, int channel) {
// Write to delay buffer
delayBuffer.setSample(channel, writePos, input);
// Calculate modulated delay time
const float lfoValue = lfo.processSample();
const float baseDelay = 0.010f * sampleRate; // 10ms base
const float modDelay = baseDelay + depth * 0.005f * sampleRate * lfoValue;
// Read from delay buffer with linear interpolation
const float readPos = writePos - modDelay;
const float delayed = readDelayBuffer(channel, readPos);
writePos = (writePos + 1) % delayBuffer.getNumSamples();
// Mix dry and wet
return input * (1.0f - mix) + delayed * mix;
}
void reset() {
delayBuffer.clear();
writePos = 0;
lfo.reset();
}
private:
float readDelayBuffer(int channel, float position) {
// Wrap position
while (position < 0)
position += delayBuffer.getNumSamples();
const int pos1 = static_cast<int>(position) % delayBuffer.getNumSamples();
const int pos2 = (pos1 + 1) % delayBuffer.getNumSamples();
const float frac = position - std::floor(position);
const float samp1 = delayBuffer.getSample(channel, pos1);
const float samp2 = delayBuffer.getSample(channel, pos2);
// Linear interpolation
return samp1 + frac * (samp2 - samp1);
}
double sampleRate = 44100.0;
float depth = 0.5f;
float mix = 0.5f;
juce::AudioBuffer<float> delayBuffer;
int writePos = 0;
// Simple LFO
struct LFO {
void setSampleRate(double sr) { sampleRate = sr; }
void setFrequency(float freq) { frequency = freq; }
float processSample() {
const float output = std::sin(phase);
phase += juce::MathConstants<float>::twoPi * frequency / sampleRate;
if (phase >= juce::MathConstants<float>::twoPi)
phase -= juce::MathConstants<float>::twoPi;
return output;
}
void reset() { phase = 0.0f; }
double sampleRate = 44100.0;
float frequency = 1.0f;
float phase = 0.0f;
} lfo;
};
```
---
## Delay-Based Effects
### Simple Delay
```cpp
class SimpleDelay {
public:
void prepare(double sampleRate) {
this->sampleRate = sampleRate;
// Max delay: 2 seconds
const int bufferSize = static_cast<int>(2.0 * sampleRate);
delayBuffer.setSize(2, bufferSize);
delayBuffer.clear();
writePos = 0;
}
void setParameters(float delayTimeMs, float feedback, float mix) {
delaySamples = static_cast<int>(delayTimeMs * 0.001f * sampleRate);
this->feedback = juce::jlimit(0.0f, 0.95f, feedback); // Prevent runaway
this->mix = mix;
}
float processSample(float input, int channel) {
// Read delayed sample
const int readPos = (writePos - delaySamples + delayBuffer.getNumSamples())
% delayBuffer.getNumSamples();
const float delayed = delayBuffer.getSample(channel, readPos);
// Write input + feedback
const float toWrite = input + delayed * feedback;
delayBuffer.setSample(channel, writePos, toWrite);
writePos = (writePos + 1) % delayBuffer.getNumSamples();
// Mix
return input * (1.0f - mix) + delayed * mix;
}
void reset() {
delayBuffer.clear();
writePos = 0;
}
private:
double sampleRate = 44100.0;
int delaySamples = 0;
float feedback = 0.0f;
float mix = 0.5f;
juce::AudioBuffer<float> delayBuffer;
int writePos = 0;
};
```
---
## Saturation & Distortion
### Soft Clipper
```cpp
inline float softClip(float input, float threshold = 0.7f) {
if (std::abs(input) < threshold)
return input;
const float sign = input > 0.0f ? 1.0f : -1.0f;
const float abs = std::abs(input);
// Soft knee above threshold
return sign * (threshold + (1.0f - threshold) * std::tanh((abs - threshold) / (1.0f - threshold)));
}
```
### Waveshaper (Polynomial)
```cpp
inline float waveshape(float input, float drive) {
const float x = input * drive;
// Cubic waveshaping: y = x - (x^3)/3
return x - (x * x * x) / 3.0f;
}
```
### Tube-Style Saturation
```cpp
inline float tubeSaturation(float input, float drive) {
const float x = input * drive;
// Hyperbolic tangent - smooth saturation
return std::tanh(x) / drive;
}
```
---
## Parameter Smoothing
### Linear Smoother
```cpp
class ParameterSmoother {
public:
void reset(double sampleRate, double rampTimeSeconds) {
this->sampleRate = sampleRate;
rampSamples = static_cast<int>(rampTimeSeconds * sampleRate);
currentSample = rampSamples;
}
void setTargetValue(float target) {
if (target != targetValue) {
startValue = currentValue;
targetValue = target;
currentSample = 0;
}
}
float getNextValue() {
if (currentSample >= rampSamples)
return targetValue;
const float alpha = static_cast<float>(currentSample) / rampSamples;
currentValue = startValue + alpha * (targetValue - startValue);
++currentSample;
return currentValue;
}
private:
double sampleRate = 44100.0;
int rampSamples = 0;
int currentSample = 0;
float startValue = 0.0f;
float targetValue = 0.0f;
float currentValue = 0.0f;
};
```
### Exponential Smoother (One-Pole)
```cpp
class ExponentialSmoother {
public:
void reset(double sampleRate, double timeConstantSeconds) {
coeff = std::exp(-1.0 / (timeConstantSeconds * sampleRate));
currentValue = 0.0f;
}
void setTargetValue(float target) {
targetValue = target;
}
float getNextValue() {
currentValue = coeff * currentValue + (1.0f - coeff) * targetValue;
return currentValue;
}
private:
float coeff = 0.0f;
float targetValue = 0.0f;
float currentValue = 0.0f;
};
```
---
## Utility Functions
### Decibel Conversion
```cpp
inline float dBToGain(float dB) {
return std::pow(10.0f, dB / 20.0f);
}
inline float gainToDB(float gain) {
return 20.0f * std::log10(gain);
}
```
### Frequency to MIDI Note
```cpp
inline float frequencyToMIDI(float frequency) {
return 69.0f + 12.0f * std::log2(frequency / 440.0f);
}
inline float midiToFrequency(float midiNote) {
return 440.0f * std::pow(2.0f, (midiNote - 69.0f) / 12.0f);
}
```
### Denormal Prevention
```cpp
inline float preventDenormal(float value) {
static constexpr float denormalFix = 1.0e-20f;
return value + denormalFix;
}
// Or use JUCE's built-in
juce::FloatVectorOperations::disableDenormalisedNumberSupport();
```
### Peak Meter (with ballistics)
```cpp
class PeakMeter {
public:
void prepare(double sampleRate) {
// Attack: instantaneous
// Release: 300ms typical
releaseCoeff = std::exp(-1.0 / (0.3 * sampleRate));
peak = 0.0f;
}
float processSample(float input) {
const float absInput = std::abs(input);
if (absInput > peak) {
peak = absInput; // Attack
} else {
peak = releaseCoeff * peak + (1.0f - releaseCoeff) * absInput; // Release
}
return peak;
}
float getPeakDB() const {
return juce::Decibels::gainToDecibels(peak);
}
void reset() {
peak = 0.0f;
}
private:
float releaseCoeff = 0.0f;
float peak = 0.0f;
};
```
---
## Integration with JUCE
### Using in AudioProcessor
```cpp
class MyPluginProcessor : public juce::AudioProcessor {
public:
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
filter.prepare(sampleRate);
filter.setParameters(1000.0f, 0.707f, StateVariableFilter::Mode::Lowpass);
compressor.prepare(sampleRate);
compressor.setParameters(-20.0f, 4.0f, 10.0f, 100.0f);
}
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override {
for (int channel = 0; channel < buffer.getNumChannels(); ++channel) {
auto* data = buffer.getWritePointer(channel);
for (int sample = 0; sample < buffer.getNumSamples(); ++sample) {
// Apply filter
data[sample] = filter.processSample(data[sample]);
// Apply compression
data[sample] = compressor.processSample(data[sample]);
}
}
}
private:
StateVariableFilter filter;
Compressor compressor;
};
```
---
## References
- **Audio EQ Cookbook**: `/docs/dsp-resources/audio-eq-cookbook.html`
- **Julius O. Smith DSP Books**: `/docs/dsp-resources/julius-smith-dsp-books.md`
- **DAFX Book**: `/docs/dsp-resources/dafx-reference.md`
- **Cytomic Filters**: `/docs/dsp-resources/cytomic-filter-designs.md`
---
**Note**: All code examples are production-ready and follow realtime-safety rules. Pre-allocate buffers in `prepare()`, avoid allocations in `processSample()`, and use proper numerical stability techniques.

View File

@@ -0,0 +1,624 @@
---
name: juce-best-practices
description: 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.
allowed-tools: Read, Grep, Glob
---
# 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("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**
```cpp
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**
```cpp
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**
```cpp
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**
```cpp
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**
```cpp
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**
```cpp
// 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**
```cpp
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**
```cpp
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
```cpp
// WRONG
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&) {
// Process...
if (editor)
editor->repaint(); // BAD! UI call from audio thread
}
```
**Solution: Use AsyncUpdater**
```cpp
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
```cpp
// WRONG - assumes 44.1kHz
float delayTimeInSamples = 0.5f * 44100.0f;
```
**Solution: Update in prepareToPlay**
```cpp
void prepareToPlay(double sampleRate, int maxBlockSize) override {
delayTimeInSamples = 0.5f * sampleRate; // Correct for any sample rate
}
```
### ❌ Pitfall 3: Forgetting to Call Base Class Methods
```cpp
// WRONG
void prepareToPlay(double sr, int maxBlockSize) override {
// Forgot to call base class!
mySetup(sr, maxBlockSize);
}
```
**Solution: Always Call Base**
```cpp
void prepareToPlay(double sr, int maxBlockSize) override {
AudioProcessor::prepareToPlay(sr, maxBlockSize);
mySetup(sr, maxBlockSize);
}
```
---
## Quick Reference
### Do's ✅
- Use `AudioProcessorValueTreeState` for 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.

View File

@@ -0,0 +1,975 @@
---
name: plugin-architecture-patterns
description: Clean architecture patterns for JUCE plugins including separation of concerns, APVTS patterns, state management, preset systems, MIDI handling, and modulation routing. Use when designing plugin architecture, refactoring code structure, implementing parameter systems, building preset managers, or scaling complex audio plugins.
allowed-tools: Read, Grep, Glob
---
# Plugin Architecture Patterns
Master architectural patterns for building maintainable, testable, and scalable audio plugins using clean architecture, separation of concerns, and JUCE best practices.
## Overview
This skill provides comprehensive guidance on structuring JUCE audio plugins using proven architectural patterns. It covers separation of DSP from UI, state management, preset systems, parameter handling, MIDI routing, and modulation architectures.
## When to Use This Skill
- Designing a new plugin architecture from scratch
- Refactoring an existing plugin for better maintainability
- Implementing complex state management or modulation routing
- Planning multi-format plugin support (VST3/AU/AAX)
- Building plugins that need to scale (many parameters, voices, effects)
## Core Architectural Principles
### 1. Separation of Concerns
Audio plugins have distinct responsibilities that should be isolated:
```
┌─────────────────────────────────────────────────┐
│ Plugin Host │
└─────────────────────┬───────────────────────────┘
┌───────────┴───────────┐
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ Processor │ │ Editor │
│ (Audio) │◄────────┤ (UI) │
└─────┬──────┘ └────────────┘
┌─────▼──────┐
│ DSP Engine │
└─────┬──────┘
┌─────▼──────┬──────────┬───────────┐
│ Filter │ Envelope │ Oscillator│
└────────────┴──────────┴───────────┘
```
**Key Separations:**
- **DSP Logic** - Pure audio processing, realtime-safe
- **Parameter Management** - Value storage, automation, presets
- **UI Layer** - Rendering, user interaction (not realtime-safe)
- **State Management** - Serialization, preset loading/saving
---
## Architecture Pattern 1: Clean Architecture
### Layer Structure
```
┌──────────────────────────────────────────┐
│ Presentation Layer (UI) │ ← JUCE Components, Graphics
├──────────────────────────────────────────┤
│ Application Layer (Processor) │ ← AudioProcessor, parameter handling
├──────────────────────────────────────────┤
│ Domain Layer (DSP Core) │ ← Pure audio algorithms
├──────────────────────────────────────────┤
│ Infrastructure (JUCE Framework) │ ← JUCE modules, OS/DAW interface
└──────────────────────────────────────────┘
```
**Dependency Rule:** Outer layers depend on inner layers, never the reverse.
### Example: Clean Architecture in JUCE
```cpp
// ============================================================================
// Domain Layer - Pure DSP (no JUCE dependencies except juce::dsp)
// ============================================================================
// Source/DSP/FilterCore.h
class FilterCore {
public:
void setFrequency(float hz, float sampleRate) {
// Pure calculation, no allocations
coefficients = calculateCoefficients(hz, sampleRate);
}
float processSample(float input) noexcept {
// Realtime-safe processing
return filter.processSample(input, coefficients);
}
void reset() noexcept {
filter.reset();
}
private:
struct Coefficients { float b0, b1, b2, a1, a2; };
Coefficients coefficients;
BiquadFilter filter;
static Coefficients calculateCoefficients(float hz, float sampleRate);
};
// ============================================================================
// Application Layer - Parameter Management
// ============================================================================
// Source/PluginProcessor.h
class MyPluginProcessor : public juce::AudioProcessor {
public:
MyPluginProcessor()
: parameters(*this, nullptr, "Parameters", createParameterLayout())
{
// Connect parameters to DSP
cutoffParam = parameters.getRawParameterValue("cutoff");
}
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
filterCore.reset();
currentSampleRate = sampleRate;
}
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override {
// Update DSP from parameters (thread-safe)
float cutoff = cutoffParam->load();
filterCore.setFrequency(cutoff, currentSampleRate);
// Process audio
for (int ch = 0; ch < buffer.getNumChannels(); ++ch) {
auto* data = buffer.getWritePointer(ch);
for (int i = 0; i < buffer.getNumSamples(); ++i) {
data[i] = filterCore.processSample(data[i]);
}
}
}
void getStateInformation(juce::MemoryBlock& destData) override {
auto state = parameters.copyState();
std::unique_ptr<juce::XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, destData);
}
void setStateInformation(const void* data, int sizeInBytes) override {
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
if (xml && xml->hasTagName(parameters.state.getType()))
parameters.replaceState(juce::ValueTree::fromXml(*xml));
}
private:
juce::AudioProcessorValueTreeState parameters;
std::atomic<float>* cutoffParam;
FilterCore filterCore; // Domain layer object
double currentSampleRate = 44100.0;
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
};
// ============================================================================
// Presentation Layer - UI
// ============================================================================
// Source/PluginEditor.h
class MyPluginEditor : public juce::AudioProcessorEditor {
public:
MyPluginEditor(MyPluginProcessor& p)
: AudioProcessorEditor(&p), processor(p)
{
// Attach UI to parameters (APVTS handles thread-safety)
cutoffAttachment = std::make_unique<SliderAttachment>(
processor.getParameters(), "cutoff", cutoffSlider
);
addAndMakeVisible(cutoffSlider);
}
private:
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
MyPluginProcessor& processor;
juce::Slider cutoffSlider;
std::unique_ptr<SliderAttachment> cutoffAttachment;
};
```
**Benefits:**
- ✅ DSP is testable without JUCE (can unit test `FilterCore` standalone)
- ✅ UI changes don't affect DSP
- ✅ Easy to swap DSP implementations
- ✅ Clear separation of realtime-safe vs non-realtime code
---
## Architecture Pattern 2: Parameter-Centric Architecture
### Using AudioProcessorValueTreeState (APVTS)
JUCE's APVTS is the recommended way to manage parameters:
```cpp
// Parameters.h - Centralized parameter definitions
namespace Parameters {
inline const juce::ParameterID cutoff { "cutoff", 1 };
inline const juce::ParameterID resonance { "resonance", 1 };
inline const juce::ParameterID gain { "gain", 1 };
inline juce::AudioProcessorValueTreeState::ParameterLayout createLayout() {
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
params.push_back(std::make_unique<juce::AudioParameterFloat>(
cutoff,
"Cutoff",
juce::NormalisableRange<float>(20.0f, 20000.0f, 0.01f, 0.3f), // Skew for log
1000.0f
));
params.push_back(std::make_unique<juce::AudioParameterFloat>(
resonance,
"Resonance",
juce::NormalisableRange<float>(0.1f, 10.0f),
1.0f
));
params.push_back(std::make_unique<juce::AudioParameterFloat>(
gain,
"Gain",
juce::NormalisableRange<float>(-24.0f, 24.0f),
0.0f
));
return { params.begin(), params.end() };
}
}
// PluginProcessor.h
class MyPluginProcessor : public juce::AudioProcessor {
public:
MyPluginProcessor()
: apvts(*this, nullptr, "Parameters", Parameters::createLayout())
{
// Get raw parameter pointers for realtime access
cutoffParam = apvts.getRawParameterValue(Parameters::cutoff.getParamID());
resonanceParam = apvts.getRawParameterValue(Parameters::resonance.getParamID());
gainParam = apvts.getRawParameterValue(Parameters::gain.getParamID());
}
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override {
// Thread-safe parameter access
float cutoff = cutoffParam->load();
float resonance = resonanceParam->load();
float gain = juce::Decibels::decibelsToGain(gainParam->load());
// Use parameters in DSP...
}
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
private:
juce::AudioProcessorValueTreeState apvts;
// Cached parameter pointers (thread-safe atomics)
std::atomic<float>* cutoffParam;
std::atomic<float>* resonanceParam;
std::atomic<float>* gainParam;
};
```
**Benefits:**
- ✅ Automatic thread-safe parameter updates
- ✅ Built-in automation support
- ✅ Easy preset save/load
- ✅ UI attachment without boilerplate
---
## Architecture Pattern 3: State Management
### Plugin State Architecture
```
┌────────────────────────────────────────────────┐
│ Plugin State │
├────────────────────────────────────────────────┤
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Parameters │ │ Non-Param │ │
│ │ (APVTS) │ │ State │ │
│ ├──────────────────┤ ├──────────────────┤ │
│ │ • Cutoff │ │ • UI Size │ │
│ │ • Resonance │ │ • Preset Name │ │
│ │ • Gain │ │ • Favorited │ │
│ │ • (Automated) │ │ • (Not Automated)│ │
│ └──────────────────┘ └──────────────────┘ │
└────────────────────────────────────────────────┘
```
### Managing Non-Parameter State
Some state shouldn't be parameters (not automated):
```cpp
// PluginProcessor.h
class MyPluginProcessor : public juce::AudioProcessor {
public:
void getStateInformation(juce::MemoryBlock& destData) override {
// Create root ValueTree
juce::ValueTree state("PluginState");
// Add parameter state
state.appendChild(apvts.copyState(), nullptr);
// Add non-parameter state
juce::ValueTree nonParamState("NonParameterState");
nonParamState.setProperty("uiWidth", uiWidth, nullptr);
nonParamState.setProperty("uiHeight", uiHeight, nullptr);
nonParamState.setProperty("presetName", presetName, nullptr);
state.appendChild(nonParamState, nullptr);
// Serialize to XML
std::unique_ptr<juce::XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, destData);
}
void setStateInformation(const void* data, int sizeInBytes) override {
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
if (!xml || !xml->hasTagName("PluginState"))
return;
juce::ValueTree state = juce::ValueTree::fromXml(*xml);
// Restore parameter state
auto paramState = state.getChildWithName("Parameters");
if (paramState.isValid())
apvts.replaceState(paramState);
// Restore non-parameter state
auto nonParamState = state.getChildWithName("NonParameterState");
if (nonParamState.isValid()) {
uiWidth = nonParamState.getProperty("uiWidth", 800);
uiHeight = nonParamState.getProperty("uiHeight", 600);
presetName = nonParamState.getProperty("presetName", "").toString();
}
}
private:
juce::AudioProcessorValueTreeState apvts;
int uiWidth = 800, uiHeight = 600;
juce::String presetName;
};
```
---
## Architecture Pattern 4: Preset System Design
### User Preset Management
```cpp
// PresetManager.h
class PresetManager {
public:
PresetManager(juce::AudioProcessor& processor)
: processor(processor)
{
// Default preset location
presetDirectory = juce::File::getSpecialLocation(
juce::File::userApplicationDataDirectory
).getChildFile("MyPlugin/Presets");
presetDirectory.createDirectory();
loadPresetList();
}
void savePreset(const juce::String& name) {
juce::MemoryBlock stateData;
processor.getStateInformation(stateData);
juce::File presetFile = presetDirectory.getChildFile(name + ".preset");
presetFile.replaceWithData(stateData.getData(), stateData.getSize());
loadPresetList(); // Refresh
}
void loadPreset(const juce::String& name) {
juce::File presetFile = presetDirectory.getChildFile(name + ".preset");
if (!presetFile.existsAsFile())
return;
juce::MemoryBlock stateData;
presetFile.loadFileAsData(stateData);
processor.setStateInformation(stateData.getData(),
static_cast<int>(stateData.getSize()));
currentPresetName = name;
}
juce::StringArray getPresetList() const {
return presetNames;
}
juce::String getCurrentPresetName() const {
return currentPresetName;
}
private:
juce::AudioProcessor& processor;
juce::File presetDirectory;
juce::StringArray presetNames;
juce::String currentPresetName;
void loadPresetList() {
presetNames.clear();
auto presetFiles = presetDirectory.findChildFiles(
juce::File::findFiles, false, "*.preset"
);
for (const auto& file : presetFiles)
presetNames.add(file.getFileNameWithoutExtension());
presetNames.sort(true);
}
};
// Usage in Editor
class MyPluginEditor : public juce::AudioProcessorEditor {
void comboBoxChanged(juce::ComboBox* box) override {
if (box == &presetComboBox) {
presetManager.loadPreset(box->getText());
}
}
void saveButtonClicked() {
juce::String name = juce::AlertWindow::showInputBox(
"Save Preset", "Enter preset name:", ""
);
if (name.isNotEmpty())
presetManager.savePreset(name);
}
};
```
### Factory Presets
```cpp
// FactoryPresets.h
struct FactoryPreset {
juce::String name;
std::function<void(juce::AudioProcessorValueTreeState&)> configure;
};
namespace FactoryPresets {
inline std::vector<FactoryPreset> getPresets() {
return {
{
"Warm Filter",
[](juce::AudioProcessorValueTreeState& apvts) {
apvts.getParameter("cutoff")->setValueNotifyingHost(0.3f);
apvts.getParameter("resonance")->setValueNotifyingHost(0.7f);
}
},
{
"Bright Filter",
[](juce::AudioProcessorValueTreeState& apvts) {
apvts.getParameter("cutoff")->setValueNotifyingHost(0.8f);
apvts.getParameter("resonance")->setValueNotifyingHost(0.3f);
}
}
};
}
}
// Initialize on first launch
if (isFirstLaunch) {
for (const auto& preset : FactoryPresets::getPresets()) {
preset.configure(apvts);
presetManager.savePreset(preset.name);
}
}
```
---
## Architecture Pattern 5: MIDI Handling
### MIDI Message Processing
```cpp
// MidiProcessor.h
class MidiProcessor {
public:
struct MidiNote {
int noteNumber;
int velocity;
bool isNoteOn;
};
void processMidiBuffer(juce::MidiBuffer& midiMessages, int numSamples) {
for (const auto metadata : midiMessages) {
auto message = metadata.getMessage();
int samplePosition = metadata.samplePosition;
if (message.isNoteOn()) {
handleNoteOn(message.getNoteNumber(),
message.getVelocity(),
samplePosition);
} else if (message.isNoteOff()) {
handleNoteOff(message.getNoteNumber(), samplePosition);
} else if (message.isPitchWheel()) {
handlePitchBend(message.getPitchWheelValue(), samplePosition);
} else if (message.isController()) {
handleCC(message.getControllerNumber(),
message.getControllerValue(),
samplePosition);
}
}
}
private:
void handleNoteOn(int noteNumber, int velocity, int samplePos) {
// Trigger voice
for (auto& voice : voices) {
if (!voice.isActive()) {
voice.startNote(noteNumber, velocity, samplePos);
break;
}
}
}
void handleNoteOff(int noteNumber, int samplePos) {
for (auto& voice : voices) {
if (voice.isActive() && voice.getNoteNumber() == noteNumber) {
voice.stopNote(samplePos);
}
}
}
void handlePitchBend(int value, int samplePos) {
float bendSemitones = ((value - 8192) / 8192.0f) * 2.0f; // ±2 semitones
for (auto& voice : voices) {
if (voice.isActive())
voice.setPitchBend(bendSemitones);
}
}
void handleCC(int ccNumber, int ccValue, int samplePos) {
if (ccNumber == 1) { // Mod wheel
float modulation = ccValue / 127.0f;
for (auto& voice : voices)
if (voice.isActive())
voice.setModulation(modulation);
}
}
std::array<SynthVoice, 16> voices;
};
```
### MPE (MIDI Polyphonic Expression) Support
```cpp
class MPEProcessor {
public:
MPEProcessor() {
mpeZoneLayout.setLowerZone(15); // 15 voice channels
}
void processMidiBuffer(juce::MidiBuffer& midiMessages, int numSamples) {
for (const auto metadata : midiMessages) {
auto message = metadata.getMessage();
if (mpeZoneLayout.isNoteOn(message)) {
int noteNumber = message.getNoteNumber();
int channel = message.getChannel();
int velocity = message.getVelocity();
auto& voice = voices[channel - 1];
voice.startNote(noteNumber, velocity);
}
else if (mpeZoneLayout.isNoteOff(message)) {
auto& voice = voices[message.getChannel() - 1];
voice.stopNote();
}
else if (message.isPitchWheel()) {
// Per-note pitch bend!
auto& voice = voices[message.getChannel() - 1];
voice.setPitchBend(message.getPitchWheelValue());
}
else if (message.isChannelPressure()) {
// Per-note pressure
auto& voice = voices[message.getChannel() - 1];
voice.setPressure(message.getChannelPressureValue() / 127.0f);
}
}
}
private:
juce::MPEZoneLayout mpeZoneLayout;
std::array<SynthVoice, 15> voices; // 15 MPE voice channels
};
```
---
## Architecture Pattern 6: Modulation Routing
### Modulation Matrix Architecture
```cpp
// ModulationSystem.h
class ModulationSystem {
public:
enum class Source {
LFO1, LFO2, LFO3,
Envelope1, Envelope2,
VelocityMIDI,
ModWheelMIDI,
PitchBendMIDI
};
enum class Destination {
FilterCutoff,
FilterResonance,
OscPitch,
OscShape,
Gain
};
struct ModulationRoute {
Source source;
Destination destination;
float amount; // -1.0 to +1.0
bool enabled = true;
};
void addRoute(Source src, Destination dst, float amount) {
routes.push_back({ src, dst, amount, true });
}
void removeRoute(size_t index) {
if (index < routes.size())
routes.erase(routes.begin() + index);
}
void process(int numSamples) {
// Update modulation sources
for (int i = 0; i < numSamples; ++i) {
sourceValues[Source::LFO1] = lfo1.getNextSample();
sourceValues[Source::Envelope1] = envelope1.getNextSample();
// ... other sources
// Apply modulation to destinations
applyModulation();
}
}
float getModulatedValue(Destination dst, float baseValue) {
float total = 0.0f;
for (const auto& route : routes) {
if (route.enabled && route.destination == dst) {
total += sourceValues[route.source] * route.amount;
}
}
return baseValue + total;
}
private:
std::vector<ModulationRoute> routes;
std::unordered_map<Source, float> sourceValues;
LFO lfo1, lfo2, lfo3;
Envelope envelope1, envelope2;
void applyModulation() {
// Calculate modulated values for all destinations
}
};
// Usage in DSP
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi) {
modulationSystem.process(buffer.getNumSamples());
float baseCutoff = cutoffParam->load();
float modulatedCutoff = modulationSystem.getModulatedValue(
ModulationSystem::Destination::FilterCutoff,
baseCutoff
);
filter.setCutoff(modulatedCutoff);
}
```
### Advanced Modulation: Per-Voice Modulation
```cpp
class Voice {
public:
void startNote(int noteNumber, int velocity) {
this->noteNumber = noteNumber;
this->velocity = velocity / 127.0f;
envelope.noteOn();
isActive_ = true;
}
float processSample(float input, ModulationSystem& globalMod) {
// Per-voice envelope
float envValue = envelope.getNextSample();
// Combine global and per-voice modulation
float cutoff = globalMod.getModulatedValue(
ModulationSystem::Destination::FilterCutoff,
baseCutoff
);
cutoff += envValue * envelopeToFilterAmount; // Per-voice mod
filter.setCutoff(cutoff);
return filter.processSample(input);
}
private:
int noteNumber;
float velocity;
bool isActive_ = false;
Envelope envelope;
Filter filter;
float baseCutoff = 1000.0f;
float envelopeToFilterAmount = 500.0f; // Env mod depth
};
```
---
## Architecture Pattern 7: Voice Management (Polyphonic Synths)
### Voice Allocation Strategy
```cpp
class VoiceManager {
public:
explicit VoiceManager(int numVoices)
: voices(numVoices)
{
}
void noteOn(int noteNumber, int velocity) {
// Try to find inactive voice
Voice* voiceToUse = findInactiveVoice();
// If all voices active, steal oldest
if (!voiceToUse)
voiceToUse = findVoiceToSteal();
voiceToUse->startNote(noteNumber, velocity);
}
void noteOff(int noteNumber) {
for (auto& voice : voices) {
if (voice.isActive() && voice.getNoteNumber() == noteNumber) {
voice.stopNote();
}
}
}
void renderNextBlock(juce::AudioBuffer<float>& buffer) {
for (auto& voice : voices) {
if (voice.isActive()) {
voice.renderNextBlock(buffer);
}
}
}
private:
std::vector<Voice> voices;
Voice* findInactiveVoice() {
for (auto& voice : voices) {
if (!voice.isActive())
return &voice;
}
return nullptr;
}
Voice* findVoiceToSteal() {
// Strategy: Steal oldest note
Voice* oldest = &voices[0];
double oldestTime = oldest->getStartTime();
for (auto& voice : voices) {
if (voice.getStartTime() < oldestTime) {
oldest = &voice;
oldestTime = voice.getStartTime();
}
}
return oldest;
}
};
```
---
## Architecture Pattern 8: Multi-Format Support
### Format-Specific Code Isolation
```cpp
// PluginProcessor.h
class MyPluginProcessor : public juce::AudioProcessor {
public:
const juce::String getName() const override {
#if JucePlugin_IsSynth
return "MySynth";
#else
return "MyEffect";
#endif
}
bool acceptsMidi() const override {
#if JucePlugin_WantsMidiInput
return true;
#else
return false;
#endif
}
bool producesMidi() const override {
#if JucePlugin_ProducesMidiOutput
return true;
#else
return false;
#endif
}
bool isMidiEffect() const override {
#if JucePlugin_IsMidiEffect
return true;
#else
return false;
#endif
}
// Format-specific behavior
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi) override {
#if JucePlugin_IsSynth
// Synth: Generate audio from MIDI
buffer.clear();
processMidi(midi);
synthesizer.renderNextBlock(buffer, midi, 0, buffer.getNumSamples());
#else
// Effect: Process input audio
processAudio(buffer);
#endif
}
};
```
---
## Testing Architecture
### Unit Testing DSP Components
```cpp
// Tests/FilterTests.cpp
#include <catch2/catch_test_macros.hpp>
#include "../Source/DSP/FilterCore.h"
TEST_CASE("FilterCore processes audio correctly", "[dsp]") {
FilterCore filter;
SECTION("Impulse response") {
filter.reset();
filter.setFrequency(1000.0f, 44100.0f);
float impulse[128] = { 1.0f }; // Impulse
float output[128];
for (int i = 0; i < 128; ++i)
output[i] = filter.processSample(impulse[i]);
// Verify filter ring-down
REQUIRE(output[0] != 0.0f);
REQUIRE(std::abs(output[127]) < 0.01f); // Should decay
}
SECTION("DC blocking") {
filter.reset();
filter.setFrequency(1000.0f, 44100.0f);
// Feed DC signal
for (int i = 0; i < 1000; ++i) {
float out = filter.processSample(1.0f);
if (i > 100) // After transient
REQUIRE(std::abs(out) < 0.1f); // Should block DC
}
}
}
```
---
## Performance Considerations
### Object Lifetime and Allocation
```cpp
class MyPluginProcessor : public juce::AudioProcessor {
public:
void prepareToPlay(double sampleRate, int samplesPerBlock) override {
// ✅ Allocate buffers here (not in processBlock!)
workBuffer.setSize(2, samplesPerBlock);
delayBuffer.setSize(2, static_cast<int>(sampleRate * 2.0)); // 2 sec
// ✅ Initialize DSP
filter.prepare({ sampleRate, (juce::uint32)samplesPerBlock, 2 });
filter.reset();
}
void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override {
// ✅ No allocations here!
// ✅ Use pre-allocated buffers
// Process using workBuffer
workBuffer.makeCopyOf(buffer);
filter.process(juce::dsp::AudioBlock<float>(workBuffer));
buffer.makeCopyOf(workBuffer);
}
private:
juce::AudioBuffer<float> workBuffer;
juce::AudioBuffer<float> delayBuffer;
juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>,
juce::dsp::IIR::Coefficients<float>> filter;
};
```
---
## Summary
**Key Architectural Principles:**
- ✅ Separate DSP, parameters, state, and UI into distinct layers
- ✅ Use APVTS for parameter management
- ✅ Never allocate or lock in audio thread
- ✅ Test DSP components in isolation
- ✅ Design for multiple plugin formats (VST3/AU/AAX)
- ✅ Implement modulation routing as a separate system
- ✅ Use clean architecture patterns for maintainability
**When Designing a New Plugin:**
1. Start with domain layer (pure DSP algorithms)
2. Add application layer (parameters, processor)
3. Build presentation layer (UI)
4. Implement state management and presets
5. Add modulation routing (if needed)
6. Test each layer independently
---
## Related Resources
- **juce-best-practices** skill - Realtime safety, threading, memory management
- **dsp-cookbook** skill - DSP algorithm implementations
- **TESTING_STRATEGY.md** - Testing approach for plugins
- JUCE Documentation: ValueTreeState, AudioProcessor, AudioProcessorEditor
---
**Remember:** Good architecture is invisible to the user but makes development, testing, and maintenance exponentially easier. Invest time in architecture upfront to save countless hours debugging threading issues, state corruption, and spaghetti code later!