Skip to content

Cell Architecture Documentation

Overview

The Cell Architecture provides the foundational framework for all L2 cells in the AudioLab system. It defines interfaces, base classes, and infrastructure that all cells inherit.

Core Concepts

What is a Cell?

A Cell L2 is a molecular-level DSP component that combines multiple L1 atoms into a functional unit. Think of it as:

  • Atoms L1 = amino acids (basic building blocks)
  • Cells L2 = proteins (complex functional units)

Examples: - Synthesis Cell: Oscillator + Filter + Envelopes + VCA = Complete synth voice - Effect Cell: Delay lines + Filters + Modulation = Complex reverb - Modulation Cell: Sources + Routing + Transformers = Modulation matrix

Key Principles

  1. Encapsulation: Internal complexity hidden behind clean interface
  2. Composability: Cells combine atoms, cells combine into engines
  3. Real-time Safety: Zero allocations, lock-free where possible
  4. Thread Safety: Safe parameter updates from any thread
  5. State Management: Complete capture/restore capability

Architecture Components

┌─────────────────────────────────────────────────────────────┐
│                        ICellL2                              │
│              (Interface all cells implement)                │
└─────────────────────────────────────────────────────────────┘
                            │ inherits
┌─────────────────────────────────────────────────────────────┐
│                        CellBase                             │
│        (Base implementation with infrastructure)            │
│                                                             │
│  ┌───────────────┐  ┌──────────────┐  ┌─────────────────┐ │
│  │ Atom Registry │  │ Routing      │  │ Parameter       │ │
│  │               │  │ Matrix       │  │ Mapper          │ │
│  └───────────────┘  └──────────────┘  └─────────────────┘ │
│                                                             │
│  ┌───────────────┐  ┌──────────────┐  ┌─────────────────┐ │
│  │ Buffer Pool   │  │ Modulation   │  │ Lifecycle       │ │
│  │               │  │ Pool         │  │ Manager         │ │
│  └───────────────┘  └──────────────┘  └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                            │ inherits
        ┌───────────────────┼───────────────────┐
        │                   │                   │
┌───────────────┐  ┌────────────────┐  ┌──────────────────┐
│ SubtractiveSyn│  │ ReverbCell     │  │ ModMatrixCell    │
│ thCell        │  │                │  │                  │
└───────────────┘  └────────────────┘  └──────────────────┘
  (Concrete cell)    (Concrete cell)      (Concrete cell)

Interface: ICellL2

The ICellL2 interface defines the contract all cells must fulfill.

Lifecycle Methods

// Prepare for processing
void prepare(double sampleRate, int blockSize);

// Reset to initial state (keep resources)
void reset();

// Release all resources
void release();

// Get current state
CellLifecycleState getState() const;

Lifecycle States:

UNINITIALIZED → prepare() → PREPARED → processBlock() → PROCESSING
                                ↓                           ↓
                            reset() ←───────────────────────┘
                            release() → RELEASED

Processing Methods (Real-Time Thread)

// Process audio block (MUST be RT-safe!)
void processBlock(AudioBuffer& buffer);

// Process MIDI events (MUST be RT-safe!)
void processMidi(MidiBuffer& midi);

Real-Time Safety Requirements: - ✅ No memory allocations - ✅ No system calls - ✅ No locks (or bounded lock-free) - ✅ Bounded execution time - ✅ No virtual calls in hot path (where possible)

Parameter Methods (Thread-Safe)

// Set parameter (RT-safe, can be called from any thread)
void setParameter(ParamID id, float value);

// Get parameter (thread-safe)
float getParameter(ParamID id) const;

// Get parameter metadata
ParameterRange getParameterRange(ParamID id) const;

State Methods

// Capture complete state
CellState captureState() const;

// Restore from captured state
void restoreState(const CellState& state);

Routing Methods

// Get internal routing configuration
AudioRouting getInternalRouting() const;

// Set internal routing configuration
void setInternalRouting(const AudioRouting& routing);

Performance Methods

// Set quality mode (Eco/Normal/Pristine/Auto)
void setQualityMode(QualityMode mode);

// Get CPU usage estimate
CPULoad estimateCPULoad() const;

Base Class: CellBase

CellBase provides common infrastructure that all cells inherit.

What CellBase Provides

  1. Atom Management
  2. Create internal atoms
  3. Register and access by ID
  4. Automatic lifecycle coordination

  5. Routing Matrix

  6. Connect atoms internally
  7. Compute processing order
  8. Detect feedback loops

  9. Parameter Mapping

  10. Map cell params → atom params
  11. Support scale/offset transforms
  12. Automatic propagation

  13. Resource Pooling

  14. Buffer pool for temporary storage
  15. Modulation source sharing
  16. Automatic cleanup

  17. Thread Safety

  18. Lock-free parameter updates
  19. Atomic state management
  20. Safe cross-thread communication

  21. Performance Monitoring

  22. CPU usage tracking
  23. Block time measurement
  24. Automatic quality adaptation

Creating a Custom Cell

Step 1: Inherit from CellBase

class MyCustomCell : public CellBase {
public:
    // Define parameter IDs
    enum Params : ParamID {
        PARAM_GAIN = 1,
        PARAM_CUTOFF = 2
    };

    MyCustomCell();
    std::string getCellType() const override;

protected:
    void buildInternalGraph() override;
    void processInternal(AudioBuffer& buffer, MidiBuffer& midi) override;
};

Step 2: Configure in Constructor

MyCustomCell::MyCustomCell() {
    // Configure I/O
    inputChannels = 2;
    outputChannels = 2;
    acceptMidi = false;
    produceMidi = false;

    // Register parameters
    ParameterRange gainParam;
    gainParam.id = PARAM_GAIN;
    gainParam.name = "Gain";
    gainParam.unit = "dB";
    gainParam.type = ParameterType::FLOAT;
    gainParam.curve = ParameterCurve::LOGARITHMIC;
    gainParam.minValue = -60.0f;
    gainParam.maxValue = 12.0f;
    gainParam.defaultValue = 0.0f;
    gainParam.automatable = true;

    registerParameter(gainParam);
}

Step 3: Build Internal Graph

void MyCustomCell::buildInternalGraph() {
    // Create internal atoms
    auto osc = createAtom<Oscillator>("osc1");
    auto filter = createAtom<Filter>("filter1");
    auto vca = createAtom<VCA>("vca1");

    // Connect atoms
    connectAtoms("osc1", 0, "filter1", 0);
    connectAtoms("filter1", 0, "vca1", 0);

    // Map parameters
    mapParameter(PARAM_CUTOFF, "filter1", Filter::PARAM_CUTOFF);
    mapParameter(PARAM_GAIN, "vca1", VCA::PARAM_GAIN);
}

Step 4: Implement Processing

void MyCustomCell::processInternal(AudioBuffer& buffer, MidiBuffer& midi) {
    // CellBase handles routing atoms through the graph
    // This is where you add any cell-level processing

    // For most cells, atoms handle all processing
    // Just need to handle cell-level state/logic here
}

Data Structures

AudioBuffer

Multi-channel audio storage:

AudioBuffer buffer(2, 512);  // 2 channels, 512 samples
float* left = buffer.getChannelData(0);
float* right = buffer.getChannelData(1);

MidiBuffer

MIDI event storage:

MidiBuffer midi;
midi.addMessage(MidiMessage::noteOn(1, 60, 100));

for (const auto& msg : midi.getMessages()) {
    if (msg.isNoteOn()) {
        handleNoteOn(msg.getNote(), msg.getVelocity());
    }
}

CellState

Complete state snapshot:

// Capture
CellState state = cell->captureState();

// Save to file
std::vector<uint8_t> data = state.serialize();
saveToFile(data);

// Load from file
std::vector<uint8_t> data = loadFromFile();
CellState state = CellState::deserialize(data);
cell->restoreState(state);

AudioRouting

Internal routing configuration:

AudioRouting routing;
routing.topology = RoutingTopology::SERIAL;

AudioConnection conn;
conn.sourceAtom = "osc1";
conn.sourceOutput = 0;
conn.destAtom = "filter1";
conn.destInput = 0;
conn.gain = 1.0f;

routing.connections.push_back(conn);

if (routing.isValid()) {
    cell->setInternalRouting(routing);
}

Usage Examples

Simple Gain Cell

auto gainCell = std::make_unique<SimpleGainCell>();
gainCell->prepare(44100.0, 512);
gainCell->setParameter(SimpleGainCell::PARAM_GAIN, 0.5f);

AudioBuffer buffer(2, 512);
// ... fill buffer with input ...
gainCell->processBlock(buffer);
// ... buffer now contains gained output ...

Synthesis Cell with MIDI

auto synthCell = std::make_unique<SimpleSynthCell>();
synthCell->prepare(44100.0, 512);
synthCell->setParameter(SimpleSynthCell::PARAM_FILTER_CUTOFF, 0.7f);

MidiBuffer midi;
midi.addMessage(MidiMessage::noteOn(1, 60, 100));

AudioBuffer buffer(2, 512);
buffer.clear();  // Synth generates audio
synthCell->processMidi(midi);
synthCell->processBlock(buffer);
// ... buffer contains synthesized audio ...

State Management

// Save state
CellState state = cell->captureState();
std::vector<uint8_t> data = state.serialize();
savePreset("my_preset.state", data);

// Load state
std::vector<uint8_t> data = loadPreset("my_preset.state");
CellState state = CellState::deserialize(data);
cell->restoreState(state);

Performance Considerations

Quality Modes

// Eco: Minimum CPU
cell->setQualityMode(QualityMode::ECO);

// Normal: Balanced
cell->setQualityMode(QualityMode::NORMAL);

// Pristine: Maximum quality
cell->setQualityMode(QualityMode::PRISTINE);

// Auto: CPU-adaptive
cell->setQualityMode(QualityMode::AUTO);

CPU Monitoring

CPULoad load = cell->estimateCPULoad();
std::cout << "CPU: " << (load.percentage * 100.0f) << "%\n";
std::cout << "Avg: " << load.averageBlockTime << "ms\n";
std::cout << "Peak: " << load.peakBlockTime << "ms\n";

if (load.percentage > 0.8f) {
    // Switch to eco mode
    cell->setQualityMode(QualityMode::ECO);
}

Thread Safety

Safe Operations from Any Thread

// These are safe from any thread:
cell->setParameter(id, value);      // RT-safe atomic update
float val = cell->getParameter(id);  // Thread-safe read
CPULoad load = cell->estimateCPULoad();

Non-RT Thread Only

// These MUST be called from non-RT thread:
cell->prepare(sampleRate, blockSize);
cell->reset();
cell->release();
cell->setInternalRouting(routing);
CellState state = cell->captureState();
cell->restoreState(state);

RT Audio Thread Only

// These MUST be called from RT audio thread:
cell->processBlock(buffer);
cell->processMidi(midi);

Best Practices

DO:

✅ Keep cells modular (max 10 atoms) ✅ Use CellBase infrastructure ✅ Make processInternal() RT-safe ✅ Register all parameters in constructor ✅ Use parameter mapping for atom params ✅ Validate routing in buildInternalGraph() ✅ Write comprehensive tests

DON'T:

❌ Allocate in processInternal() ❌ Use locks in audio thread ❌ Create monolithic cells (>10 atoms) ❌ Hardcode routing ❌ Skip state versioning ❌ Forget to check feedback loops ❌ Ignore thread safety

Next Steps

  1. Review ICellL2.h for complete interface
  2. Study CellBase.h for base implementation
  3. Check examples/SimpleGainCell.h for minimal example
  4. Read implementation docs in src/ when available
  5. Write tests using patterns in tests/ when available

Version: 1.0.0 Date: 2025-10-14 Status: Foundation Complete - Implementation In Progress