Skip to content

🤝 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


✅ 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

  1. Read the spec:

    code SPEC.md
    

  2. See example:

    code examples/SimpleProcessor.cpp
    

  3. Write your plugin:

    #include "IPluginProcessor.hpp"
    
    class MyPlugin : public IPluginProcessor {
        // Implement methods...
    };
    

  4. Test it:

    TEST_CASE("MyPlugin initializes") {
        MyPlugin plugin;
        REQUIRE_NOTHROW(plugin.initialize(48000.0f, 512));
    }
    


🆕 Recent Updates (2025-10-09)

✅ Phase 1 Refactoring Complete

All plugin interfaces have been successfully refactored to inherit from 04_CORE interfaces:

  1. IParameterHost → Now inherits from IObservable + IMessageable
  2. IModulationTarget → Now inherits from IObservable
  3. 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. 🤝