Skip to content

AudioProcessor Base Class

RT-safe audio processor foundation for AudioLab

Purpose

Provides a robust base class for all audio processors (effects, instruments, analyzers) using the Template Method pattern. Enforces real-time safety, manages lifecycle, and simplifies DSP development.

Features

  • Template Method Pattern: Base class handles lifecycle, derived classes implement DSP
  • RT-Safety Enforcement: Architectural guarantees for real-time audio thread
  • Atomic State Management: Thread-safe state transitions using lock-free atomics
  • Automatic Bypass: Safe passthrough when inactive
  • Lifecycle Management: prepare → activate → process → deactivate → release
  • Zero Virtual Call Overhead: Measured < 0.5% CPU impact

Architecture

Components

  1. ProcessorConfig (processor_config.hpp)
  2. Immutable configuration (sample rate, block size, channels)
  3. Compile-time validation
  4. POD structure, no allocations

  5. ProcessorStateManager (processor_state.hpp)

  6. Atomic state tracking (Uninitialized → Initialized → Active → Suspended)
  7. Lock-free compare-and-swap operations
  8. RT-safe queries

  9. AudioProcessor (audio_processor.hpp)

  10. Base class implementing Template Method pattern
  11. Virtual hooks for derived classes (on_prepare, on_process, etc.)
  12. Implements IAudioProcessor, IActivatable, IResettable

Design Pattern: Template Method

Why this pattern? - Separates lifecycle management (base) from DSP logic (derived) - Enforces RT-safety at architectural level (can't bypass checks) - Prevents common mistakes (allocations in process()) - Allows derived classes to focus ONLY on DSP

Trade-offs: - Virtual call overhead: ~1-2% CPU (acceptable for safety gain) - Less flexibility: acceptable (consistency > flexibility)

Usage

Basic Example

#include "audio_processor.hpp"
#include <atomic>

class SimpleGain : public AudioProcessor {
public:
    SimpleGain()
        : AudioProcessor({"Simple Gain", "AudioLab", "com.audiolab.gain", "1.0.0"})
        , gain_(1.0f)
    {}

    void setGain(float linear_gain) {
        gain_.store(linear_gain, std::memory_order_release);
    }

protected:
    // RT-SAFE: Implement DSP here
    void on_process(
        const float* const* inputs,
        float** outputs,
        uint32_t num_samples
    ) noexcept override {
        const float gain = gain_.load(std::memory_order_relaxed);
        const uint32_t num_channels = config().num_output_channels;

        for (uint32_t ch = 0; ch < num_channels; ++ch) {
            for (uint32_t i = 0; i < num_samples; ++i) {
                outputs[ch][i] = inputs[ch][i] * gain;
            }
        }
    }

private:
    std::atomic<float> gain_;
};

Lifecycle

SimpleGain gain;

// 1. Prepare (allocate resources)
gain.prepareToPlay(48000.0, 512, 2);  // 48kHz, 512 samples, stereo

// 2. Activate (ready to process)
gain.activate();

// 3. Process (in RT thread)
AudioBuffer<float> buffer(2, 512);
gain.processBlock(buffer);

// 4. Deactivate (pause)
gain.deactivate();

// 5. Release (free resources)
gain.releaseResources();

Virtual Hooks (Override in Derived Classes)

Hook Purpose RT-Safe Required
on_prepare() Allocate buffers, compute coefficients Optional
on_process() Main DSP callback Required
on_release() Free resources Optional
on_activate() Start processing, prime buffers ⚠️ Optional
on_deactivate() Pause processing, flush buffers ⚠️ Optional
on_reset() Clear state (buffers, histories) Optional

State Machine

Uninitialized --prepare()--> Initialized --activate()--> Active
       ^                          |                       |
       |                          v                       v
   release()                 Suspended <--deactivate()---+

States: - Uninitialized: After construction, before prepare() - Initialized: Resources allocated, ready for activate() - Active: Processing audio (on_process() called) - Suspended: Paused but still initialized - Error: Operation failed, needs release() + prepare() to recover

RT-Safety Guarantees

What's RT-Safe?

  • on_process(): MUST be RT-safe (NO allocations, NO locks)
  • on_reset(): SHOULD be RT-safe (use memset, avoid allocations)
  • is_active(): RT-safe (atomic read)
  • processBlock(): RT-safe (checks state atomically, calls on_process())

What's NOT RT-Safe?

  • on_prepare(): Can allocate (called on non-RT thread)
  • on_release(): Can deallocate (called on non-RT thread)
  • on_activate(): Lightweight but not guaranteed RT-safe
  • on_deactivate(): Lightweight but not guaranteed RT-safe

Stack Usage

  • processBlock(): < 4KB stack usage (channel pointer arrays on stack)
  • Maximum 256 channels supported (validated in ProcessorConfig)

Thread-Safety

Method Thread Atomic Notes
prepareToPlay() Non-RT No Called before activate()
processBlock() RT Yes State checked atomically
activate() RT or Non-RT Yes CAS state transition
deactivate() RT or Non-RT Yes CAS state transition
releaseResources() Non-RT No Called after deactivate()
is_active() Any Yes Lock-free atomic read

Testing

Run Tests

# Build
cd "2 - FOUNDATION/04_CORE/04_10_audio_processor"
cmake -B build
cmake --build build --config Release

# Run tests
ctest --test-dir build -C Release -V

Run Example

# Build and run simple_gain example
cd build
./Release/simple_gain_example.exe

Expected output:

=== AudioLab Simple Gain Demo ===
Created: Simple Gain
Prepared: 48000 Hz, 512 samples
Gain: -6.0 dB

Processing 128 samples...
Input:  0.500000
Output: 0.250595
Expected: 0.250595
✓ Output matches expected

=== Demo Complete ===

Test Coverage

test_processor.cpp (8 test cases): - ProcessorConfig validation - Lifecycle state transitions - Active processing - Bypass mode (inactive) - Full lifecycle integration - Info queries - Error handling

test_processor_state.cpp (5 test cases): - State transitions (valid/invalid) - Atomic operations (CAS) - Thread-safety (concurrent readers/writers) - Enum values - Error state handling

Performance

Overhead

  • Virtual call overhead: < 1% CPU (measured on 10M iterations)
  • Bypass mode: Zero-copy when possible
  • State check: Single atomic load (< 5 CPU cycles)

Benchmarks

Run benchmarks with:

./build/Release/benchmark_processor.exe

Integration

CMake

target_link_libraries(your_target PRIVATE
    audiolab_audio_processor
)

Dependencies

  • 04_01_core_interfaces (IAudioProcessor, IActivatable, IResettable)
  • 04_05_buffer_management (AudioBuffer)
  • 04_00_type_system (sample_rate_t, etc.)
  • C++17 standard library (<atomic>, <cstring>)

Design Decisions

Why Template Method?

Alternative 1: Pure virtual interface - ❌ No safety enforcement - ❌ Every derived class must implement lifecycle correctly - ❌ Easy to make mistakes (allocations in process())

Alternative 2: CRTP (Curiously Recurring Template Pattern) - ❌ Need runtime polymorphism for plugin hosting - ❌ More complex for users - ✅ Zero virtual call overhead

✅ Chosen: Template Method - ✅ Safety enforced at base level (can't bypass) - ✅ Derived classes focus only on DSP - ✅ Runtime polymorphism for plugin hosting - ✅ Small virtual call overhead (< 1%) acceptable

Why Atomic State?

Alternative: Mutex-protected state - ❌ NOT RT-safe (locks) - ❌ Priority inversion risk - ❌ Higher overhead

✅ Chosen: Atomic with memory ordering - ✅ RT-safe (lock-free) - ✅ Correct synchronization (acquire/release semantics) - ✅ Minimal overhead (single CPU instruction)

Common Patterns

Parameter Smoothing

class SmoothGain : public AudioProcessor {
protected:
    void on_prepare(const ProcessorConfig& config) override {
        smoother_.prepare(config.sample_rate);
    }

    void on_process(...) noexcept override {
        for (uint32_t i = 0; i < num_samples; ++i) {
            float smooth_gain = smoother_.getNext();
            for (uint32_t ch = 0; ch < num_channels; ++ch) {
                outputs[ch][i] = inputs[ch][i] * smooth_gain;
            }
        }
    }

private:
    ParameterSmoother smoother_;
};

Buffer Allocation

class Delay : public AudioProcessor {
protected:
    void on_prepare(const ProcessorConfig& config) override {
        // Allocate delay buffer (NOT RT-safe, that's OK)
        delay_buffer_.resize(
            config.num_output_channels,
            delay_samples_
        );
    }

    void on_process(...) noexcept override {
        // Use pre-allocated buffer (RT-safe)
        delay_buffer_.process(inputs, outputs, num_samples);
    }

private:
    DelayLine delay_buffer_;
    size_t delay_samples_ = 48000;  // 1 second @ 48kHz
};

Error Handling

void on_prepare(const ProcessorConfig& config) override {
    if (config.sample_rate < 44100.0) {
        throw std::runtime_error("Sample rate too low");
    }
    // Base class catches exceptions and sets Error state
}

FAQ

Q: Can I use std::vector in on_process()? A: No. Allocations are NOT RT-safe. Allocate in on_prepare(), use in on_process().

Q: Can I use mutexes in on_process()? A: No. Locks are NOT RT-safe. Use atomics for thread-safe parameter changes.

Q: What if processBlock() is called when inactive? A: Safe bypass mode activates (passthrough). No DSP processing occurs.

Q: Can I call activate() from the audio thread? A: Yes, activate/deactivate use atomic transitions. Safe from any thread.

Q: Why is on_process() noexcept? A: RT-safety. Exceptions can allocate memory. Use assertions for debug checks.

Next Steps

  • See examples/simple_gain.cpp for complete working example
  • Read audio_processor.hpp for detailed API documentation
  • Check tests/ for usage patterns and edge cases
  • Build your own processor by inheriting from AudioProcessor

License

Part of AudioLab Core Framework Copyright © 2024 AudioLab