Initial commit
This commit is contained in:
924
skills/cross-platform-builds/SKILL.md
Normal file
924
skills/cross-platform-builds/SKILL.md
Normal 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
|
||||
992
skills/daw-compatibility-guide/SKILL.md
Normal file
992
skills/daw-compatibility-guide/SKILL.md
Normal 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.
|
||||
704
skills/dsp-cookbook/SKILL.md
Normal file
704
skills/dsp-cookbook/SKILL.md
Normal 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.
|
||||
624
skills/juce-best-practices/SKILL.md
Normal file
624
skills/juce-best-practices/SKILL.md
Normal 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.
|
||||
975
skills/plugin-architecture-patterns/SKILL.md
Normal file
975
skills/plugin-architecture-patterns/SKILL.md
Normal 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!
|
||||
Reference in New Issue
Block a user