use std::collections::HashMap;
use bbx_core::StackVec;
use crate::{
block::{BlockCategory, BlockId, BlockType},
blocks::{effectors::mixer::MixerBlock, io::output::OutputBlock},
buffer::{AudioBuffer, Buffer},
channel::ChannelLayout,
context::DspContext,
parameter::Parameter,
sample::Sample,
};
pub const MAX_BLOCK_INPUTS: usize = 16;
pub const MAX_BLOCK_OUTPUTS: usize = 16;
#[derive(Debug, Clone)]
pub struct Connection {
pub from: BlockId,
pub from_output: usize,
pub to: BlockId,
pub to_input: usize,
}
#[derive(Debug, Clone)]
pub struct BlockSnapshot {
pub id: usize,
pub name: String,
pub category: BlockCategory,
pub input_count: usize,
pub output_count: usize,
}
#[derive(Debug, Clone)]
pub struct ConnectionSnapshot {
pub from_block: usize,
pub from_output: usize,
pub to_block: usize,
pub to_input: usize,
}
#[derive(Debug, Clone)]
pub struct ModulationConnectionSnapshot {
pub from_block: usize,
pub to_block: usize,
pub parameter_name: String,
}
#[derive(Debug, Clone)]
pub struct GraphTopologySnapshot {
pub blocks: Vec<BlockSnapshot>,
pub connections: Vec<ConnectionSnapshot>,
pub modulation_connections: Vec<ModulationConnectionSnapshot>,
}
pub struct Graph<S: Sample> {
blocks: Vec<BlockType<S>>,
connections: Vec<Connection>,
execution_order: Vec<BlockId>,
output_block: Option<BlockId>,
audio_buffers: Vec<AudioBuffer<S>>,
modulation_values: Vec<S>,
block_buffer_start: Vec<usize>,
buffer_size: usize,
context: DspContext,
block_input_buffers: Vec<Vec<usize>>,
}
impl<S: Sample> Graph<S> {
pub fn new(sample_rate: f64, buffer_size: usize, num_channels: usize) -> Self {
let context = DspContext {
sample_rate,
buffer_size,
num_channels,
current_sample: 0,
channel_layout: ChannelLayout::default(),
};
Self {
blocks: Vec::new(),
connections: Vec::new(),
execution_order: Vec::new(),
output_block: None,
audio_buffers: Vec::new(),
modulation_values: Vec::new(),
block_buffer_start: Vec::new(),
buffer_size,
context,
block_input_buffers: Vec::new(),
}
}
#[inline]
pub fn context(&self) -> &DspContext {
&self.context
}
#[inline]
pub fn get_block(&self, id: BlockId) -> Option<&BlockType<S>> {
self.blocks.get(id.0)
}
#[inline]
pub fn get_block_mut(&mut self, id: BlockId) -> Option<&mut BlockType<S>> {
self.blocks.get_mut(id.0)
}
pub fn add_block(&mut self, block: BlockType<S>) -> BlockId {
let block_id = BlockId(self.blocks.len());
self.block_buffer_start.push(self.audio_buffers.len());
self.blocks.push(block);
let output_count = self.blocks[block_id.0].output_count();
for _ in 0..output_count {
self.audio_buffers.push(AudioBuffer::new(self.buffer_size));
}
block_id
}
pub fn add_output_block(&mut self) -> BlockId {
let block = BlockType::Output(OutputBlock::<S>::new(self.context.num_channels));
let block_id = self.add_block(block);
self.output_block = Some(block_id);
block_id
}
pub fn connect(&mut self, from: BlockId, from_output: usize, to: BlockId, to_input: usize) {
self.connections.push(Connection {
from,
from_output,
to,
to_input,
})
}
pub fn prepare_for_playback(&mut self) {
self.execution_order = self.topological_sort();
self.modulation_values.resize(self.blocks.len(), S::ZERO);
self.block_input_buffers = vec![Vec::new(); self.blocks.len()];
for conn in &self.connections {
let buffer_idx = self.get_buffer_index(conn.from, conn.from_output);
self.block_input_buffers[conn.to.0].push(buffer_idx);
}
#[cfg(debug_assertions)]
self.validate_buffer_indices();
}
#[cfg(debug_assertions)]
fn validate_buffer_indices(&self) {
for block_id in 0..self.blocks.len() {
let input_indices = &self.block_input_buffers[block_id];
let output_count = self.blocks[block_id].output_count();
let output_start = self.block_buffer_start[block_id];
for output_idx in 0..output_count {
let buffer_idx = output_start + output_idx;
debug_assert!(
!input_indices.contains(&buffer_idx),
"Block {block_id} has overlapping input/output buffer index {buffer_idx}. \
This would cause undefined behavior in process_block_unsafe()."
);
}
}
}
fn topological_sort(&self) -> Vec<BlockId> {
let mut in_degree = vec![0; self.blocks.len()];
let mut adjacency_list: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
for connection in &self.connections {
adjacency_list.entry(connection.from).or_default().push(connection.to);
in_degree[connection.to.0] += 1;
}
let mut queue = Vec::new();
let mut result = Vec::new();
for (i, °ree) in in_degree.iter().enumerate() {
if degree == 0 {
queue.push(BlockId(i));
}
}
while let Some(block) = queue.pop() {
result.push(block);
if let Some(neighbors) = adjacency_list.get(&block) {
for &neighbor in neighbors {
in_degree[neighbor.0] -= 1;
if in_degree[neighbor.0] == 0 {
queue.push(neighbor);
}
}
}
}
result
}
#[inline]
pub fn process_buffers(&mut self, output_buffers: &mut [&mut [S]]) {
for buffer in &mut self.audio_buffers {
buffer.zeroize();
}
for i in 0..self.execution_order.len() {
let block_id = self.execution_order[i];
self.process_block_unsafe(block_id);
self.collect_modulation_values(block_id);
}
self.copy_to_output_buffer(output_buffers);
}
#[inline]
fn process_block_unsafe(&mut self, block_id: BlockId) {
let input_indices = &self.block_input_buffers[block_id.0];
let mut output_indices: StackVec<usize, MAX_BLOCK_OUTPUTS> = StackVec::new();
let output_count = self.blocks[block_id.0].output_count();
debug_assert!(
output_count <= MAX_BLOCK_OUTPUTS,
"Block output count {output_count} exceeds MAX_BLOCK_OUTPUTS {MAX_BLOCK_OUTPUTS}"
);
for output_index in 0..output_count {
let buffer_index = self.get_buffer_index(block_id, output_index);
output_indices.push_unchecked(buffer_index);
}
unsafe {
let buffers_ptr = self.audio_buffers.as_mut_ptr();
let mut input_slices: StackVec<&[S], MAX_BLOCK_INPUTS> = StackVec::new();
let input_count = input_indices.len();
debug_assert!(
input_count <= MAX_BLOCK_INPUTS,
"Block input count {input_count} exceeds MAX_BLOCK_INPUTS {MAX_BLOCK_INPUTS}"
);
for &index in input_indices {
let buffer_ptr = buffers_ptr.add(index);
let slice = std::slice::from_raw_parts((*buffer_ptr).as_ptr(), (*buffer_ptr).len());
input_slices.push_unchecked(slice);
}
let mut output_slices: StackVec<&mut [S], MAX_BLOCK_OUTPUTS> = StackVec::new();
for &index in output_indices.as_slice() {
let buffer_ptr = buffers_ptr.add(index);
let slice = std::slice::from_raw_parts_mut((*buffer_ptr).as_mut_ptr(), (*buffer_ptr).len());
output_slices.push_unchecked(slice);
}
self.blocks[block_id.0].process(
input_slices.as_slice(),
output_slices.as_mut_slice(),
&self.modulation_values,
&self.context,
);
}
}
#[inline]
fn collect_modulation_values(&mut self, block_id: BlockId) {
if block_id.0 >= self.blocks.len() {
return;
}
let has_modulation = !self.blocks[block_id.0].modulation_outputs().is_empty();
if has_modulation {
let buffer_index = self.get_buffer_index(block_id, 0);
if let (Some(&first_sample), Some(mod_val)) = (
self.audio_buffers.get(buffer_index).and_then(|b| b.as_slice().first()),
self.modulation_values.get_mut(block_id.0),
) {
*mod_val = first_sample;
}
}
}
fn copy_to_output_buffer(&self, output_buffer: &mut [&mut [S]]) {
if let Some(output_block_id) = self.output_block {
let output_count = self.blocks[output_block_id.0].output_count();
for channel in 0..output_count.min(output_buffer.len()) {
let internal_buffer_index = self.get_buffer_index(output_block_id, channel);
let internal_buffer = &self.audio_buffers[internal_buffer_index];
let copy_length = internal_buffer.len().min(output_buffer[channel].len());
output_buffer[channel][..copy_length].copy_from_slice(&internal_buffer.as_slice()[..copy_length]);
}
}
}
#[inline]
fn get_buffer_index(&self, block_id: BlockId, output_index: usize) -> usize {
self.block_buffer_start[block_id.0] + output_index
}
}
pub struct GraphBuilder<S: Sample> {
graph: Graph<S>,
}
impl<S: Sample> GraphBuilder<S> {
pub fn new(sample_rate: f64, buffer_size: usize, num_channels: usize) -> Self {
Self {
graph: Graph::new(sample_rate, buffer_size, num_channels),
}
}
pub fn with_layout(sample_rate: f64, buffer_size: usize, layout: ChannelLayout) -> Self {
let num_channels = layout.channel_count();
let mut builder = Self {
graph: Graph::new(sample_rate, buffer_size, num_channels),
};
builder.graph.context.channel_layout = layout;
builder
}
pub fn add<B: Into<BlockType<S>>>(&mut self, block: B) -> BlockId {
self.graph.add_block(block.into())
}
pub fn connect(&mut self, from: BlockId, from_output: usize, to: BlockId, to_input: usize) -> &mut Self {
self.graph.connect(from, from_output, to, to_input);
self
}
pub fn modulate(&mut self, source: BlockId, target: BlockId, parameter: &str) -> &mut Self {
if let Err(e) = self.graph.blocks[target.0].set_parameter(parameter, Parameter::Modulated(source)) {
eprintln!("Modulation error: {e}");
}
self
}
pub fn capture_topology(&self) -> GraphTopologySnapshot {
let blocks = self
.graph
.blocks
.iter()
.enumerate()
.map(|(id, block)| BlockSnapshot {
id,
name: block.name().to_string(),
category: block.category(),
input_count: block.input_count(),
output_count: block.output_count(),
})
.collect();
let connections = self
.graph
.connections
.iter()
.map(|conn| ConnectionSnapshot {
from_block: conn.from.0,
from_output: conn.from_output,
to_block: conn.to.0,
to_input: conn.to_input,
})
.collect();
let modulation_connections = self
.graph
.blocks
.iter()
.enumerate()
.flat_map(|(target_id, block)| {
block
.get_modulated_parameters()
.into_iter()
.map(move |(param_name, source_id)| ModulationConnectionSnapshot {
from_block: source_id.0,
to_block: target_id,
parameter_name: param_name.to_string(),
})
})
.collect();
GraphTopologySnapshot {
blocks,
connections,
modulation_connections,
}
}
pub fn build(mut self) -> Graph<S> {
let num_channels = self.graph.context.num_channels;
let existing_output = self.graph.blocks.iter().position(|b| b.is_output()).map(BlockId);
let terminal_blocks: Vec<BlockId> = self
.graph
.blocks
.iter()
.enumerate()
.filter(|(idx, block)| {
let block_id = BlockId(*idx);
let has_outgoing = self.graph.connections.iter().any(|c| c.from == block_id);
!has_outgoing && !block.is_modulator() && !block.is_output()
})
.map(|(idx, _)| BlockId(idx))
.collect();
let explicit_mixer = self.find_explicit_mixer(&terminal_blocks);
let output_id = existing_output.unwrap_or_else(|| self.graph.add_output_block());
match (explicit_mixer, terminal_blocks.len()) {
(Some(mixer_id), _) => {
let mixer_has_outgoing = self.graph.connections.iter().any(|c| c.from == mixer_id);
if !mixer_has_outgoing {
self.connect_block_to_output(mixer_id, output_id, num_channels);
}
}
(None, 0) => {}
(None, 1) => {
let block_id = terminal_blocks[0];
self.connect_block_to_output(block_id, output_id, num_channels);
}
(None, num_sources) => {
let mixer_id = self
.graph
.add_block(BlockType::Mixer(MixerBlock::new(num_sources, num_channels)));
for (source_idx, &block_id) in terminal_blocks.iter().enumerate() {
let block_output_count = self.graph.blocks[block_id.0].output_count();
for ch in 0..num_channels.min(block_output_count) {
let mixer_input = source_idx * num_channels + ch;
self.connect(block_id, ch, mixer_id, mixer_input);
}
}
self.connect_block_to_output(mixer_id, output_id, num_channels);
}
}
self.graph.prepare_for_playback();
for (idx, block) in self.graph.blocks.iter().enumerate() {
let connected_inputs = self.graph.block_input_buffers[idx].len();
let output_count = block.output_count();
assert!(
connected_inputs <= MAX_BLOCK_INPUTS,
"Block {idx} has {connected_inputs} connected inputs, exceeding MAX_BLOCK_INPUTS ({MAX_BLOCK_INPUTS})"
);
assert!(
output_count <= MAX_BLOCK_OUTPUTS,
"Block {idx} has {output_count} outputs, exceeding MAX_BLOCK_OUTPUTS ({MAX_BLOCK_OUTPUTS})"
);
}
self.graph
}
fn find_explicit_mixer(&self, terminal_blocks: &[BlockId]) -> Option<BlockId> {
for (idx, block) in self.graph.blocks.iter().enumerate() {
let is_mixer = matches!(block, BlockType::Mixer(_) | BlockType::MatrixMixer(_));
if !is_mixer {
continue;
}
let block_id = BlockId(idx);
let has_terminal_input = self
.graph
.connections
.iter()
.any(|c| c.to == block_id && terminal_blocks.contains(&c.from));
if has_terminal_input {
return Some(block_id);
}
}
None
}
fn connect_block_to_output(&mut self, from: BlockId, to: BlockId, num_channels: usize) {
let output_count = self.graph.blocks[from.0].output_count();
for ch in 0..num_channels.min(output_count) {
self.connect(from, ch, to, ch);
}
}
}