🤝 08_00_00_contracts - Plugin Interface Contracts¶
📖 Overview¶
This directory contains abstract base interfaces (contracts) that define the fundamental API for all AudioLab plugins. Think of these as the "constitutional law" for plugins — every L4 and L5 plugin must implement these contracts.
🎯 What Are Contracts?¶
┌─────────────────────────────────────────────────────┐
│ CONTRACTS = Pure Abstract Interfaces │
│ │
│ ✅ Define WHAT plugins must do │
│ ❌ Don't specify HOW they do it │
│ │
│ → Enables polymorphism │
│ → Enforces consistency │
│ → Facilitates testing │
│ → Future-proof design │
└─────────────────────────────────────────────────────┘
📂 Files in This Directory¶
08_00_00_contracts/
├── README.md ← You are here
├── SPEC.md ← Detailed specification
│
├── IPluginProcessor.hpp ← Core lifecycle & processing
├── IStateManager.hpp ← State persistence
├── IParameterHost.hpp ← Parameter management
├── IAudioEngine.hpp ← DSP processing abstraction
├── IModulationTarget.hpp ← Modulation system
├── IUIController.hpp ← GUI communication
│
└── examples/
├── MockProcessor.hpp ← Example mock for testing
└── SimpleProcessor.cpp ← Example concrete implementation
🏗️ The Six Core Interfaces¶
1️⃣ IPluginProcessor (Foundation)¶
Purpose: Universal plugin lifecycle and identification
class IPluginProcessor {
virtual void initialize(float sampleRate, int maxBlockSize) = 0;
virtual void process(AudioBuffer& buffer) = 0;
virtual const char* getName() const = 0;
// ... lifecycle methods
};
Used By: Every L4 and L5 plugin Thread Context: Mixed (main thread for init, audio thread for process)
2️⃣ IStateManager (Persistence)¶
Purpose: Save/load plugin state for sessions and presets
class IStateManager {
virtual bool saveState(MemoryBlock& destData) = 0;
virtual bool loadState(const void* data, size_t sizeInBytes) = 0;
virtual bool migrateState(int fromVersion, int toVersion, ...) = 0;
// ... versioning and migration
};
Used By: All plugins that need to persist state Thread Context: Main thread only (I/O operations)
3️⃣ IParameterHost (Control)¶
Purpose: Parameter registration, automation, and value management
class IParameterHost {
virtual void registerParameter(Parameter* param) = 0;
virtual float getParameterValue(const std::string& id) const = 0;
virtual void beginParameterGesture(const std::string& id) = 0;
// ... automation and listeners
};
Used By: All plugins with controllable parameters Thread Context: Lock-free communication between threads
4️⃣ IAudioEngine (DSP Core)¶
Purpose: Sample-accurate audio processing abstraction
class IAudioEngine {
virtual void prepareToPlay(float sampleRate, int blockSize) = 0;
virtual void processBlock(AudioBuffer& buffer) = 0;
virtual int getLatencySamples() const = 0;
virtual bool isRealTimeSafe() const = 0;
// ... processing characteristics
};
Used By: L3 engines, L4 plugins Thread Context: Real-time audio thread (no allocations!)
5️⃣ IModulationTarget (Modulation)¶
Purpose: Allow parameters to be modulated by LFOs, envelopes, etc.
class IModulationTarget {
virtual void addModulationSource(const std::string& paramId,
IModulationSource* source,
float depth) = 0;
virtual bool isModulatable(const std::string& paramId) const = 0;
// ... modulation routing
};
Used By: Plugins supporting modulation (most L4/L5) Thread Context: Mixed (setup on main, apply on audio thread)
6️⃣ IUIController (GUI Bridge)¶
Purpose: Thread-safe communication between DSP and UI
class IUIController {
virtual void notifyParameterChanged(const std::string& id, float value) = 0;
virtual bool getMeterValues(float* levels, int numChannels) = 0;
virtual void postMessage(const UIMessage& message) = 0;
// ... visualization data
};
Used By: All plugins with GUI Thread Context: Thread-safe messaging queue
🎓 Usage Examples¶
Example 1: Concrete Implementation¶
// MyCompressor.hpp
#include "IPluginProcessor.hpp"
#include "IAudioEngine.hpp"
class MyCompressor : public IPluginProcessor,
public IAudioEngine {
public:
// Implement IPluginProcessor
void initialize(float sampleRate, int maxBlockSize) override {
m_sampleRate = sampleRate;
// Allocate resources
}
void process(AudioBuffer& buffer) override {
processBlock(buffer); // Delegate to IAudioEngine
}
const char* getName() const override { return "MyCompressor"; }
const char* getVersion() const override { return "1.0.0"; }
const char* getVendor() const override { return "AudioLab"; }
// Implement IAudioEngine
void prepareToPlay(float sr, int bs) override {
// Configure DSP
}
void processBlock(AudioBuffer& buffer) override {
// DSP implementation
for (int ch = 0; ch < buffer.getNumChannels(); ++ch) {
float* channelData = buffer.getWritePointer(ch);
for (int i = 0; i < buffer.getNumSamples(); ++i) {
channelData[i] = applyCompression(channelData[i]);
}
}
}
int getLatencySamples() const override { return 0; }
bool isRealTimeSafe() const override { return true; }
private:
float m_sampleRate = 48000.0f;
float applyCompression(float sample) {
// Compression algorithm
return sample;
}
};
Example 2: Mock for Testing¶
// MockProcessor.hpp (for unit tests)
#include "IPluginProcessor.hpp"
class MockProcessor : public IPluginProcessor {
public:
MOCK_METHOD(void, initialize, (float, int), (override));
MOCK_METHOD(void, process, (AudioBuffer&), (override));
MOCK_METHOD(const char*, getName, (), (const, override));
// ... mock all methods
};
// Usage in test:
TEST_CASE("Host loads plugin correctly") {
MockProcessor mockPlugin;
EXPECT_CALL(mockPlugin, initialize(48000.0f, 512));
PluginHost host;
host.loadPlugin(&mockPlugin);
}
Example 3: Polymorphic Usage¶
// Plugin manager uses interfaces polymorphically
class PluginManager {
public:
void registerPlugin(IPluginProcessor* plugin) {
m_plugins.push_back(plugin);
// Works with ANY plugin that implements IPluginProcessor
std::cout << "Registered: " << plugin->getName() << std::endl;
}
void initializeAll(float sampleRate, int blockSize) {
for (auto* plugin : m_plugins) {
plugin->initialize(sampleRate, blockSize);
}
}
private:
std::vector<IPluginProcessor*> m_plugins;
};
// Usage:
PluginManager manager;
manager.registerPlugin(new MyCompressor()); // L4 plugin
manager.registerPlugin(new MyReverb()); // L4 plugin
manager.registerPlugin(new MyMasterSuite()); // L5 suite
manager.initializeAll(48000.0f, 512);
🧪 Testing Strategy¶
Unit Tests¶
Located in: 03_INFRA/03_04_testing_framework/tests/integration/08_plugins/test_contracts.cpp
TEST_CASE("All interfaces are abstract", "[contracts]") {
REQUIRE(std::is_abstract<IPluginProcessor>::value);
REQUIRE(std::is_abstract<IStateManager>::value);
// ...
}
TEST_CASE("Interfaces have virtual destructors", "[contracts]") {
REQUIRE(std::has_virtual_destructor<IPluginProcessor>::value);
// ...
}
Integration Tests¶
TEST_CASE("Concrete plugin implements interfaces", "[contracts]") {
class TestPlugin : public IPluginProcessor {
// ... implement all methods
};
TestPlugin plugin;
plugin.initialize(48000.0f, 512);
AudioBuffer buffer(2, 512);
REQUIRE_NOTHROW(plugin.process(buffer));
}
📊 Design Decisions¶
Why Pure Virtual Interfaces?¶
✅ Pros: - Clear separation of interface and implementation - Easy to mock for testing - Polymorphic behavior - Future-proof (can add non-virtual helper methods in base classes)
❌ Cons: - Slight virtual function call overhead (negligible in practice) - Requires explicit implementation of all methods
Verdict: Pros far outweigh cons for plugin architecture.
Why Multiple Small Interfaces?¶
Interface Segregation Principle (ISP):
✅ GOOD: Multiple focused interfaces
Plugin can implement only what it needs
❌ BAD: One giant interface
Forces all plugins to implement everything
Example:
- Simple gain plugin → Only needs IPluginProcessor
- Complex synth → Needs IPluginProcessor + IParameterHost + IModulationTarget + IUIController
Thread Safety Considerations¶
Each interface documents thread context:
| Interface | Main Thread | Audio Thread | Notes |
|---|---|---|---|
| IPluginProcessor::initialize() | ✅ | ❌ | One-time setup |
| IPluginProcessor::process() | ❌ | ✅ | Real-time critical |
| IStateManager::saveState() | ✅ | ❌ | I/O operations |
| IParameterHost::setParameterValue() | 🔀 | 🔀 | Lock-free queue |
| IAudioEngine::processBlock() | ❌ | ✅ | No allocations! |
| IUIController::postMessage() | 🔀 | 🔀 | Thread-safe queue |
🔗 Related Documentation¶
- Detailed Spec: SPEC.md
- Testing Strategy: ../../TESTING_STRATEGY.md
- L4 Framework: ../08_00_01_l4_framework/
- L5 Framework: ../08_00_02_l5_framework/
✅ Checklist for Implementing a Contract¶
When creating a new concrete plugin:
- Inherit from
IPluginProcessor(mandatory) - Inherit from other interfaces as needed
- Implement ALL pure virtual methods
- Add override keyword to all implementations
- Document thread-safety assumptions
- Write unit tests for your implementation
- Verify no raw pointers without ownership docs
- Test with mock hosts
🚀 Quick Start¶
-
Read the spec:
-
See example:
-
Write your plugin:
-
Test it:
🆕 Recent Updates (2025-10-09)¶
✅ Phase 1 Refactoring Complete¶
All plugin interfaces have been successfully refactored to inherit from 04_CORE interfaces:
- IParameterHost → Now inherits from
IObservable+IMessageable - IModulationTarget → Now inherits from
IObservable - IUIController → Now inherits from
IObservable+IMessageable
Namespace Migration¶
All interfaces now use the audiolab::plugins namespace (previously audiolab).
// OLD (before refactoring):
namespace audiolab {
class IParameterHost { ... };
}
// NEW (after refactoring):
namespace audiolab {
namespace plugins {
class IParameterHost : public core::IObservable,
public core::IMessageable { ... };
}
}
Benefits¶
- ✅ Full inheritance from atomic core interfaces
- ✅ Consistent namespace structure
- ✅ Better composition via
04_CORE - ✅ Documented inherited methods
- ✅ All tests passing
Status: ✅ Interfaces Refactored | 🟢 L4 Framework Complete | 🟢 L5 Framework Complete Last Updated: 2025-10-09 Maintainer: AudioLab Core Team
Contracts are promises. Keep them. 🤝