Skip to content

05_11_00_graph_core - Graph System Foundation

Overview

The Graph Core is the foundational layer of the AudioLab Graph System, providing the essential building blocks for constructing and executing audio processing graphs.

A graph in this context is a network of interconnected audio processors (nodes) that process audio signals in a defined order. This architecture enables:

  • Flexible signal routing: Connect processors in any valid topology
  • Modular design: Each processor is independent and reusable
  • Real-time performance: Zero-copy optimization and efficient buffer management
  • Type safety: Strong-typed IDs prevent connection errors
  • Latency compensation: Automatic handling of processing delays

Status: ✅ Core Implementation Complete (Phase 1) Version: 1.0.0 Priority: CRITICAL (⭐⭐⭐⭐⭐) Last Updated: 2025-10-15


Architecture

┌─────────────────────────────────────────────────────────────┐
│                      AudioGraph                              │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  Node Management   Edge Management   Processing       │  │
│  │  - Add/Remove      - Connect         - Prepare        │  │
│  │  - Lookup          - Disconnect      - Process        │  │
│  │  - Validation      - Query           - Reset          │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
        ┌─────────────────┴─────────────────┐
        ↓                                    ↓
   ┌─────────┐                          ┌─────────┐
   │  Node   │ ←──── Edge ───────────→  │  Node   │
   │  (DSP)  │                          │  (DSP)  │
   └─────────┘                          └─────────┘
        ↓                                    ↓
   IDSPProcessor                        IDSPProcessor

Key Components

  1. GraphTypes.hpp: Core type definitions
  2. NodeID, PortID, EdgeID - Strong-typed identifiers
  3. AudioBuffer - Zero-copy audio data view
  4. ProcessingContext - Sample rate, block size, tempo
  5. Result<T> - Error handling template

  6. GraphNode.hpp: Processing units

  7. IDSPProcessor - Abstract interface for DSP algorithms
  8. GraphNode - Node container with port management
  9. Port system (inputs/outputs, typed, multi-channel)
  10. Statistics tracking (CPU, peak levels, clipping)

  11. GraphEdge.hpp: Connections between nodes

  12. Signal transfer with gain/mute/delay
  13. Zero-copy optimization
  14. Latency compensation via delay buffers
  15. Peak level monitoring

  16. AudioGraph.hpp: Main graph container

  17. Node lifecycle management
  18. Connection topology management
  19. Processing order calculation
  20. Buffer allocation and management

Quick Start

1. Include Headers

#include "GraphTypes.hpp"
#include "GraphNode.hpp"
#include "GraphEdge.hpp"
#include "AudioGraph.hpp"

2. Create Custom Processor

class MyProcessor : public IDSPProcessor {
public:
    void prepare(const ProcessingContext& context) override {
        sampleRate_ = context.sampleRate;
        blockSize_ = context.blockSize;
        // Allocate resources, calculate coefficients, etc.
    }

    void process(const std::vector<AudioBuffer>& inputs,
                std::vector<AudioBuffer>& outputs,
                int numSamples) override {
        if (inputs.empty() || outputs.empty()) return;

        const AudioBuffer& in = inputs[0];
        AudioBuffer& out = outputs[0];

        // Process audio
        for (int ch = 0; ch < in.numChannels; ++ch) {
            const float* inData = in.getChannelData(ch);
            float* outData = out.getChannelData(ch);

            for (int i = 0; i < numSamples; ++i) {
                outData[i] = processOneSample(inData[i]);
            }
        }
    }

    void reset() override {
        // Clear internal state
    }

private:
    double sampleRate_ = 48000.0;
    int blockSize_ = 512;
};

3. Build Graph

using namespace audiolab::graph;

// Create graph
AudioGraph graph;

// Create nodes
auto node1 = std::make_unique<GraphNode>(NodeID{1}, "Input");
node1->addOutputPort("output", PortType::AUDIO, 2);  // Stereo
node1->setProcessor(std::make_unique<MyProcessor>());
NodeID id1 = graph.addNode(std::move(node1));

auto node2 = std::make_unique<GraphNode>(NodeID{2}, "Effect");
node2->addInputPort("input", PortType::AUDIO, 2);
node2->addOutputPort("output", PortType::AUDIO, 2);
node2->setProcessor(std::make_unique<MyProcessor>());
NodeID id2 = graph.addNode(std::move(node2));

// Connect nodes
auto* n1 = graph.getNode(id1);
auto* n2 = graph.getNode(id2);
PortID out1 = n1->findOutputPort("output");
PortID in2 = n2->findInputPort("input");

graph.connect(id1, out1, id2, in2);

4. Process Audio

// Prepare for processing
ProcessingContext context;
context.sampleRate = 48000.0;
context.blockSize = 512;
graph.prepare(context);

// Allocate I/O buffers
const int numChannels = 2;
const int blockSize = 512;
float* inputL = new float[blockSize];
float* inputR = new float[blockSize];
float* outputL = new float[blockSize];
float* outputR = new float[blockSize];

float* inputChannels[] = {inputL, inputR};
float* outputChannels[] = {outputL, outputR};

AudioBuffer inputBuffer(inputChannels, numChannels, blockSize);
AudioBuffer outputBuffer(outputChannels, numChannels, blockSize);

// Process loop
while (isRunning) {
    // Fill inputBuffer with audio from interface
    // ...

    // Process through graph
    graph.process(inputBuffer, outputBuffer);

    // Send outputBuffer to audio interface
    // ...
}

// Cleanup
delete[] inputL;
delete[] inputR;
delete[] outputL;
delete[] outputR;

Design Patterns

Strong Typing for Safety

// Compile-time safety - can't mix up IDs
NodeID nodeId{1};
PortID portId{1};
EdgeID edgeId{1};

graph.getNode(nodeId);      // ✓ Correct
graph.getNode(portId);      // ✗ Compile error

Strategy Pattern for DSP

// Swap processing algorithms at runtime
node->setProcessor(std::make_unique<GainProcessor>());
// Later...
node->setProcessor(std::make_unique<FilterProcessor>());

Zero-Copy Optimization

// Edges can avoid copying when possible
edge->transfer(sourceBuffer, destBuffer);
// Internally checks if zero-copy is safe:
// - Same channel count
// - Same buffer size
// - No delay required
// - No gain applied

Dirty Flag Pattern

// Graph structure changes mark it as dirty
graph.addNode(...);     // isDirty_ = true
graph.connect(...);     // isDirty_ = true

// Processing order recalculated only when needed
graph.process(...);     // Recalculates if isDirty_

Real-Time Safety

The Graph Core is designed for real-time audio processing:

✓ Safe for Audio Thread

  • process() - No allocations, deterministic
  • reset() - Clears state, no allocations
  • setBypass() / setMixLevel() - Simple flag/value writes
  • Edge transfer() - Fixed buffer operations

✗ NOT Safe for Audio Thread

  • addNode() / removeNode() - Allocates/deallocates
  • connect() / disconnect() - Modifies topology
  • prepare() - Allocates buffers
  • Any operation that modifies graph structure

Rule: Build and configure graph on main thread, then only call process() on audio thread.


Port System

Port Types

enum class PortType : uint8_t {
    AUDIO,      // Audio signals (typically 44.1-192 kHz)
    CONTROL,    // Modulation signals (can be audio rate or lower)
    MIDI,       // MIDI event streams
    SIDECHAIN   // Audio used for analysis (not main signal path)
};

Multi-Channel Support

// Mono
node->addInputPort("in", PortType::AUDIO, 1);

// Stereo
node->addInputPort("in", PortType::AUDIO, 2);

// Surround (5.1)
node->addInputPort("in", PortType::AUDIO, 6);

Port Lookup

// By ID (fast)
const PortDescriptor* port = node->getInputPort(portId);

// By name (convenient)
PortID portId = node->findInputPort("input");

Statistics and Monitoring

Each node tracks performance metrics:

const NodeStats& stats = node->getStatistics();

std::cout << "Process calls: " << stats.processCallCount << "\n";
std::cout << "CPU time: " << stats.totalCpuTime << " ms\n";
std::cout << "Peak level: " << stats.peakLevel << "\n";
std::cout << "Clipped: " << (stats.hasClipped ? "Yes" : "No") << "\n";

Each edge tracks signal levels:

float peakDb = edge->getPeakLevelDb();
edge->resetPeakLevel();

Latency Compensation

Edges can introduce delay to compensate for processing latency:

// Processor reports its latency
int processorLatency = processor->getLatencySamples();

// Edge compensates for latency difference
edge->setDelaySamples(latencyToCompensate);
edge->prepareDelayBuffer(2, latencyToCompensate);  // Stereo

Error Handling

// Check if operation succeeded
NodeID id = graph.addNode(std::move(node));
if (!id.isValid()) {
    // Handle error
}

// Result<T> for detailed error info
Result<NodeID> result = graph.validateAndAddNode(node);
if (!result.isSuccess()) {
    std::cerr << "Error: " << result.message << "\n";
}

Examples

See examples/simple_graph_demo.cpp for a complete working example that demonstrates:

  • Creating custom DSP processors (Gain, Lowpass, Sine Generator)
  • Building a simple signal chain
  • Processing audio blocks
  • Runtime parameter changes
  • Statistics reporting

Build and run:

g++ -std=c++17 simple_graph_demo.cpp -I../include -o simple_graph_demo
./simple_graph_demo

Performance Characteristics

Operation Complexity Notes
Add Node O(1) Hash map insertion
Remove Node O(E) Must remove connected edges
Get Node O(1) Hash map lookup
Connect O(1) Vector append
Disconnect O(E) Linear search in edges
Process O(N + E) Visit each node + edge once

Where: - N = number of nodes - E = number of edges


Thread Safety

NOT thread-safe by design.

Graph modifications (add/remove nodes, connect/disconnect) must happen on a single thread (typically the main/UI thread).

For thread-safe reconfiguration during playback, use DynamicGraphManager (TAREA 9 - future).


File Structure

05_11_00_graph_core/
├── include/
│   ├── GraphTypes.hpp       # ✅ Core type definitions (400 lines)
│   ├── GraphNode.hpp        # ✅ Node structure (450 lines)
│   ├── GraphEdge.hpp        # ✅ Edge structure (350 lines)
│   └── AudioGraph.hpp       # ✅ Graph container (500 lines)
├── src/                     # Implementation files (future)
│   ├── GraphNode.cpp
│   ├── GraphEdge.cpp
│   └── AudioGraph.cpp
├── tests/                   # Unit tests (future)
│   ├── test_node_creation.cpp
│   ├── test_edge_creation.cpp
│   ├── test_graph_operations.cpp
│   ├── test_port_validation.cpp
│   └── test_processing.cpp
├── examples/
│   └── simple_graph_demo.cpp  # ✅ Complete working demo (450 lines)
└── docs/
    ├── API_REFERENCE.md       # API documentation
    └── ARCHITECTURE.md        # Architecture details

Deliverables Status

Phase 1 - Core Implementation ✅

  • GraphTypes.hpp - Strong-typed IDs and core structures
  • GraphNode.hpp - Node container with port management
  • GraphEdge.hpp - Edge connections with gain/delay
  • AudioGraph.hpp - Main graph container
  • simple_graph_demo.cpp - Working demonstration

Phase 2 - Testing (In Progress) 🔄

  • Unit tests (20+ tests, >95% coverage target)
  • Integration tests (10+ tests)
  • Performance benchmarks
  • Stress tests

Phase 3 - Documentation 🔄

  • README.md - Overview and quick start
  • API_REFERENCE.md - Complete API documentation
  • ARCHITECTURE.md - Design decisions and patterns
  • EXAMPLES.md - Additional usage examples

Dependencies

Required

  • C++17 compiler (or newer)
  • Standard library

Optional (for tests/examples)

  • Catch2 (unit tests)
  • Google Benchmark (performance tests)

Limitations (Current Implementation)

These will be addressed in future tasks:

  1. No topological sorting (TAREA 3)
  2. Currently processes nodes in insertion order
  3. Works for simple chains, may fail for complex graphs

  4. Basic buffer management (TAREA 5)

  5. Allocates separate buffers per node
  6. No buffer pooling or sharing

  7. No cycle detection (TAREA 2)

  8. Doesn't prevent feedback loops
  9. Can cause infinite recursion

  10. No automatic type checking (TAREA 2)

  11. Doesn't verify port types match on connection
  12. Runtime errors possible

  13. Single-threaded processing (TAREA 9)

  14. Processes nodes sequentially
  15. No parallel execution

Future Extensions

See PLAN_DE_DESARROLLO.md for complete roadmap (18-month project, 76 weeks):

  • TAREA 2: Topology validation (cycles, types) - 4-5 weeks
  • TAREA 3: Topological sorting (Kahn's algorithm) - 5-6 weeks
  • TAREA 4: Advanced latency management - 6-7 weeks
  • TAREA 5: Buffer pooling and sharing - 7-8 weeks
  • TAREA 6: Routing matrices - 5-6 weeks
  • TAREA 7: Multi-rate processing - 8-9 weeks
  • TAREA 8: Subgraph abstraction - 6-7 weeks
  • TAREA 9: Parallel processing - 9-10 weeks
  • TAREA 10: Dynamic reconfiguration - 7-8 weeks
  • TAREA 11: Graph optimization - 6-7 weeks
  • TAREA 12: Visualization system - 5-6 weeks

API Reference

See docs/API_REFERENCE.md for complete API documentation.


Architecture Details

See docs/ARCHITECTURE.md for in-depth architecture discussion.


Contributing

When adding new features to Graph Core:

  1. Maintain real-time safety in process() path
  2. Keep strong typing (no raw integers for IDs)
  3. Write tests for new functionality
  4. Update documentation
  5. Benchmark performance-critical code

Success Metrics

Functionality

  • ✅ Create/destroy nodes without leaks
  • ✅ Connect nodes in type-safe manner
  • ✅ Process audio correctly
  • ⏳ Bypass works without glitches

Performance

  • ⏳ <1μs overhead per node
  • ⏳ Zero allocations in audio thread
  • ⏳ Cache-friendly memory layout

Quality

  • ⏳ >95% test coverage
  • ⏳ Zero memory leaks
  • ⏳ Thread-safe where specified

License

Part of the AudioLab project.


Created: 2025-10-14 Last Updated: 2025-10-15 Version: 1.0.0 Status: Phase 1 Complete, Phase 2 In Progress