Graph Framework
Real-time graph-based media processing
Graph Framework
Graph-based processing infrastructure for real-time audio and video. Provides directed-graph topology, automatic scheduling, and zero-allocation execution. Originally developed in libbahn (music tracker), generalized into libmediacore for all media consumers.
Location
src/libs/libmediacore/mediacore/graph/
Graph.hpp / .cpp
GraphNode.hpp / .cpp
Connection.hpp / .cpp
Buffer.hpp / .cpp
BufferPool.hpp / .cpp
ProcessorCharacter.hpp / .cpp
Schedule.hpp / .cpp
ScheduleFactory.hpp / .cpp
Job.hpp / .cpp
Architecture
The framework has three layers:
+---------------------------------------------------------------------+
| GRAPH LAYER |
| Graph -> GraphNode -> Connection |
| - DFS-based cycle detection |
| - Topology change tracking |
| - Spatial queries (lasso selection) |
+---------------------------------------------------------------------+
|
v (on topology change)
+---------------------------------------------------------------------+
| COMPILATION LAYER |
| ScheduleFactory -> Schedule -> Job[] |
| - BFS traversal for job ordering |
| - Buffer allocation via "Dowry" pattern |
| - Zero-copy buffer inheritance |
+---------------------------------------------------------------------+
|
v (during playback)
+---------------------------------------------------------------------+
| EXECUTION LAYER |
| Engine.process() -> Schedule.process() -> Job.execute() |
| - Single contiguous memory block |
| - 32-byte aligned buffers (SIMD-ready) |
| - No allocation during processing |
+---------------------------------------------------------------------+
Graph layer
Graph
Container for nodes and connections. Tracks topology changes and provides queries.
| Method | Description |
|---|---|
addNode(GraphNode*) |
Add a processing node |
removeNode(GraphNode*) |
Remove a processing node |
connect(output, input) |
Create a connection between nodes |
disconnect(Connection*) |
Remove a connection |
topologyChanged() |
Returns true if graph was modified since last check |
nodesInRect(QRectF) |
Spatial query for UI lasso selection |
GraphNode
Base class for all processing nodes. Each node declares its input/output character via ProcessorCharacter.
| Method | Description |
|---|---|
character() |
Returns input/output capabilities |
inputConnections() |
List of incoming connections |
outputConnections() |
List of outgoing connections |
position() |
Node position for graph editor UI |
ProcessorCharacter
Declares a node's role in the graph.
| Property | Description |
|---|---|
takesInput |
Node accepts input connections |
givesOutput |
Node produces output connections |
Derived convenience methods:
| Method | Meaning |
|---|---|
isSource() |
Output only (no input) |
isSink() |
Input only (no output) |
isProcessor() |
Both input and output |
isIsolated() |
Neither input nor output |
Connection
Directed edge between two nodes. The graph layer validates that connections don't create cycles using DFS traversal.
Compilation layer
ScheduleFactory
Compiles a Graph into an executable Schedule by:
- BFS traversal from source nodes to determine processing order
- Buffer allocation using the "Dowry" pattern -- each node receives pre-allocated buffer slots
- Zero-copy buffer inheritance where possible (output buffer of one node reused as input of the next)
Schedule
An ordered list of Job entries backed by a single contiguous memory block.
| Method | Description |
|---|---|
process() |
Execute all jobs in order |
resizeMemory(count, alignment) |
Allocate the contiguous buffer |
jobCount() |
Number of jobs in the schedule |
Job
A single processing step: a node reference plus its buffer assignment.
| Field | Description |
|---|---|
node |
The GraphNode to process |
type |
Source, processor, or sink |
buffer |
Pointer into the contiguous memory block |
Execution layer
Memory layout
All buffers are allocated as a single contiguous block with 32-byte alignment:
static const size_t BUFFER_ALIGNMENT_BYTES{32};
32-byte alignment enables AVX/AVX2 SIMD operations. Misaligned access causes 2-10x slowdown on x86.
Zero-allocation guarantee
During processing, no memory allocation occurs:
- All buffers are pre-allocated at compilation time
- Job list is a flat array
- Node processing writes directly into pre-assigned buffer slots
This is critical for real-time audio where allocation can cause glitches.
Buffer
SIMD-aligned buffer with external memory ownership (the Schedule owns the memory, Buffer provides a typed view).
| Method | Description |
|---|---|
data() |
Pointer to buffer samples |
size() |
Number of samples |
clear() |
Zero the buffer |
Extension: libbahn
The music tracker (libbahn) extends this framework with audio-specific concepts:
| libmediacore | libbahn extension | Added functionality |
|---|---|---|
GraphNode |
AudioNode |
process(AudioContext&) |
AudioNode |
Processor |
Tracker state, parameters, serialization |
| - | ProcessorContext |
Transport state, sequencer position |
| - | ProcessorFactory |
Registry of available processor types |
Applications that need the graph engine but not the tracker (e.g., singularity's jingle system) can use AudioNode directly without importing tracker-specific code.
Usage example
// Build a simple graph
Graph graph;
auto* source = new OscNode(440.0); // 440 Hz sine
auto* gain = new GainNode(0.5); // 50% volume
auto* output = new OutputNode();
graph.addNode(source);
graph.addNode(gain);
graph.addNode(output);
graph.connect(source, gain);
graph.connect(gain, output);
// Compile to schedule
ScheduleFactory factory;
Schedule schedule = factory.compile(graph);
// Process one buffer
schedule.process();
See Also
- Media Architecture - Design rationale
- Device Management - Device enumeration and caching
- Video Streaming - Vision system and streaming