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¶
- GraphTypes.hpp: Core type definitions
NodeID,PortID,EdgeID- Strong-typed identifiersAudioBuffer- Zero-copy audio data viewProcessingContext- Sample rate, block size, tempo-
Result<T>- Error handling template -
GraphNode.hpp: Processing units
IDSPProcessor- Abstract interface for DSP algorithmsGraphNode- Node container with port management- Port system (inputs/outputs, typed, multi-channel)
-
Statistics tracking (CPU, peak levels, clipping)
-
GraphEdge.hpp: Connections between nodes
- Signal transfer with gain/mute/delay
- Zero-copy optimization
- Latency compensation via delay buffers
-
Peak level monitoring
-
AudioGraph.hpp: Main graph container
- Node lifecycle management
- Connection topology management
- Processing order calculation
- 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, deterministicreset()- Clears state, no allocationssetBypass()/setMixLevel()- Simple flag/value writes- Edge
transfer()- Fixed buffer operations
✗ NOT Safe for Audio Thread¶
addNode()/removeNode()- Allocates/deallocatesconnect()/disconnect()- Modifies topologyprepare()- 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:
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:
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:
- No topological sorting (TAREA 3)
- Currently processes nodes in insertion order
-
Works for simple chains, may fail for complex graphs
-
Basic buffer management (TAREA 5)
- Allocates separate buffers per node
-
No buffer pooling or sharing
-
No cycle detection (TAREA 2)
- Doesn't prevent feedback loops
-
Can cause infinite recursion
-
No automatic type checking (TAREA 2)
- Doesn't verify port types match on connection
-
Runtime errors possible
-
Single-threaded processing (TAREA 9)
- Processes nodes sequentially
- 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:
- Maintain real-time safety in
process()path - Keep strong typing (no raw integers for IDs)
- Write tests for new functionality
- Update documentation
- 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