///|
/// Shared internal graph state for both mono and stereo compiled DSP graphs.
/// Both CompiledDsp and CompiledStereoDsp wrap this struct as newtypes.
///
/// All three buffer families (buffers, left_buffers, right_buffers) are allocated
/// for every node regardless of mono/stereo variant. Mono nodes write only to
/// `buffers`; stereo nodes write only to `left_buffers`/`right_buffers`. This
/// uniform layout enables hot-swap state copy without knowing the variant.
priv struct CompiledGraph {
compile_sample_rate : Double
nodes : FixedArray[DspNode]
index_map : FixedArray[Int]
// Mono signal path buffers — one per node
buffers : FixedArray[AudioBuffer]
// Stereo signal path buffers — one pair per node (populated only for stereo-aware nodes)
left_buffers : FixedArray[AudioBuffer]
right_buffers : FixedArray[AudioBuffer]
osc_states : FixedArray[Oscillator?]
noise_states : FixedArray[Noise?]
env_states : FixedArray[Adsr?]
biquad_states : FixedArray[Biquad?]
stereo_biquad_left_states : FixedArray[Biquad?]
stereo_biquad_right_states : FixedArray[Biquad?]
delay_states : FixedArray[DelayLine?]
stereo_delay_left_states : FixedArray[DelayLine?]
stereo_delay_right_states : FixedArray[DelayLine?]
feedback_edges : FixedArray[(Int, Int, Int)]
// Feedback graph per-sample self-registers — one value per node, updated each sample
self_values : FixedArray[Double]
self_left_values : FixedArray[Double]
self_right_values : FixedArray[Double]
self_enabled : FixedArray[Bool]
back_edge_input0_source : FixedArray[Int]
back_edge_input1_source : FixedArray[Int]
// Pre-allocated scratch arrays for feedback processing (avoid hot-path alloc)
sample_values : FixedArray[Double]
left_sample_values : FixedArray[Double]
right_sample_values : FixedArray[Double]
// Pre-computed pan gains — trig is done once per block, not per sample
pan_left_gains : FixedArray[Double]
pan_right_gains : FixedArray[Double]
mut last_sanitized_count : Int
mut debug_validate : Bool
mut last_validation_errors : Array[GraphValidationError]
}
///|
/// Executable buffer-based DSP graph compiled from `DspNode`s.
struct CompiledDsp(CompiledGraph)
///|
/// Executable terminal-stereo graph compiled from `DspNode`s.
struct CompiledStereoDsp(CompiledGraph)
///|
/// Number of non-finite samples replaced with 0.0 during the most recent
/// process() call. Returns 0 if output was clean.
pub fn CompiledDsp::last_sanitized_count(self : CompiledDsp) -> Int {
self.0.last_sanitized_count
}
///|
pub fn CompiledStereoDsp::last_sanitized_count(self : CompiledStereoDsp) -> Int {
self.0.last_sanitized_count
}
///|
/// True when the compiled graph contains feedback (back) edges that require
/// per-sample processing with self-registers.
pub fn CompiledDsp::has_feedback_edges(self : CompiledDsp) -> Bool {
self.0.feedback_edges.length() > 0
}
///|
pub fn CompiledStereoDsp::has_feedback_edges(self : CompiledStereoDsp) -> Bool {
self.0.feedback_edges.length() > 0
}
///|
/// Compile a declarative mono graph into an executable buffer graph.
///
/// Accepts a `CompiledTemplate` (the runtime exchange boundary per
/// ADR-0010) and returns `None` if the template's topology is rejected
/// (e.g., no reachable `Output`, invalid feedback cycle, missing node
/// inputs). Produce the input via `CompiledTemplate::analyze(nodes)` or
/// `GraphBuilder::analyze`.
pub fn CompiledDsp::compile(
compiled_template : CompiledTemplate,
context : DspContext,
) -> CompiledDsp? {
CompiledDsp::compile_internal(
compiled_template.optimized,
context,
compiled_template.template.length(),
compiled_template.index_map,
)
}
///|
/// Compile without optimization — used by topology controllers where authoring
/// indices must be preserved for runtime controls.
fn CompiledDsp::compile_raw(
nodes : Array[DspNode],
context : DspContext,
) -> CompiledDsp? {
let count = nodes.length()
let identity_map = FixedArray::makei(count, fn(i) { i })
CompiledDsp::compile_internal(nodes, context, count, identity_map)
}
///|
/// Shared compilation logic for both mono and stereo graphs.
///
/// WHY callbacks instead of an enum: the validation logic differs not just by
/// mono/stereo but also by presence of feedback edges (simple vs feedback
/// variant). A `validate` callback lets the caller compose both checks in
/// one closure without adding a separate parameter for feedback mode.
/// `compile_plan` selects the mono or stereo topological sort + feedback detection.
fn compile_graph_impl(
nodes : Array[DspNode],
context : DspContext,
original_count : Int,
opt_map : FixedArray[Int],
compile_plan : (Array[DspNode], DspContext) -> (
Array[Int],
Array[(Int, Int, Int)],
)?,
validate : (FixedArray[DspNode], FixedArray[(Int, Int, Int)]) -> Bool,
) -> CompiledGraph? {
let plan = match compile_plan(nodes, context) {
Some(plan) => plan
None => return None
}
let order = plan.0
let node_count = nodes.length()
let block_size = compiled_block_size(context)
let compile_sample_rate = context.sample_rate()
let topo_map = FixedArray::make(node_count, -1)
for new_index = 0; new_index < order.length(); new_index = new_index + 1 {
topo_map[order[new_index]] = new_index
}
// Compose: original index -> optimized index -> compiled index
let index_map = FixedArray::makei(original_count, fn(i) {
let opt_i = opt_map[i]
if opt_i < 0 {
-1
} else {
topo_map[opt_i]
}
})
let fixed_nodes = FixedArray::makei(node_count, index => {
remap_node_inputs(nodes[order[index]], topo_map)
})
let feedback_edges = remapped_feedback_edges(plan.1, topo_map)
if !validate(fixed_nodes, feedback_edges) {
return None
}
let buffers = FixedArray::makei(node_count, _ => {
AudioBuffer::filled(block_size)
})
let left_buffers = FixedArray::makei(node_count, _ => {
AudioBuffer::filled(block_size)
})
let right_buffers = FixedArray::makei(node_count, _ => {
AudioBuffer::filled(block_size)
})
let osc_states = FixedArray::makei(node_count, index => {
make_graph_osc_state(fixed_nodes[index])
})
let noise_states = FixedArray::makei(node_count, index => {
make_graph_noise_state(fixed_nodes[index])
})
let env_states = FixedArray::makei(node_count, index => {
make_graph_env_state(fixed_nodes[index])
})
let biquad_states = FixedArray::makei(node_count, index => {
make_graph_biquad_state(fixed_nodes[index])
})
let stereo_biquad_left_states = FixedArray::makei(node_count, index => {
make_graph_stereo_biquad_state(fixed_nodes[index])
})
let stereo_biquad_right_states = FixedArray::makei(node_count, index => {
make_graph_stereo_biquad_state(fixed_nodes[index])
})
let delay_states = FixedArray::makei(node_count, index => {
make_graph_delay_state(fixed_nodes[index])
})
let stereo_delay_left_states = FixedArray::makei(node_count, index => {
make_graph_stereo_delay_state(fixed_nodes[index])
})
let stereo_delay_right_states = FixedArray::makei(node_count, index => {
make_graph_stereo_delay_state(fixed_nodes[index])
})
let self_values = FixedArray::make(node_count, 0.0)
let self_left_values = FixedArray::make(node_count, 0.0)
let self_right_values = FixedArray::make(node_count, 0.0)
let self_enabled = FixedArray::make(node_count, false)
let back_edge_input0_source = FixedArray::make(node_count, -1)
let back_edge_input1_source = FixedArray::make(node_count, -1)
for edge in feedback_edges {
let source_idx = edge.0
let target_idx = edge.1
let slot = edge.2
guard source_idx >= 0 &&
source_idx < node_count &&
target_idx >= 0 &&
target_idx < node_count else {
return None
}
self_enabled[source_idx] = true
if slot == 0 {
if back_edge_input0_source[target_idx] >= 0 {
return None
}
back_edge_input0_source[target_idx] = source_idx
} else {
if back_edge_input1_source[target_idx] >= 0 {
return None
}
back_edge_input1_source[target_idx] = source_idx
}
}
Some({
compile_sample_rate,
nodes: fixed_nodes,
index_map,
buffers,
left_buffers,
right_buffers,
osc_states,
noise_states,
env_states,
biquad_states,
stereo_biquad_left_states,
stereo_biquad_right_states,
delay_states,
stereo_delay_left_states,
stereo_delay_right_states,
feedback_edges,
self_values,
self_left_values,
self_right_values,
self_enabled,
back_edge_input0_source,
back_edge_input1_source,
sample_values: FixedArray::make(node_count, 0.0),
left_sample_values: FixedArray::make(node_count, 0.0),
right_sample_values: FixedArray::make(node_count, 0.0),
pan_left_gains: FixedArray::make(node_count, 0.0),
pan_right_gains: FixedArray::make(node_count, 0.0),
last_sanitized_count: 0,
debug_validate: false,
last_validation_errors: [],
})
}
///|
fn CompiledDsp::compile_internal(
nodes : Array[DspNode],
context : DspContext,
original_count : Int,
opt_map : FixedArray[Int],
) -> CompiledDsp? {
compile_graph_impl(
nodes,
context,
original_count,
opt_map,
mono_compile_plan,
fn(fixed_nodes, feedback_edges) {
if feedback_edges.length() == 0 {
valid_terminal_mono_shapes(fixed_nodes)
} else {
valid_feedback_terminal_mono_graph(fixed_nodes, feedback_edges)
}
},
).map(fn(graph) { CompiledDsp(graph) })
}
///|
/// Compile a declarative terminal-stereo graph into an executable
/// stereo graph.
///
/// Accepts a `CompiledTemplate` (the runtime exchange boundary per
/// ADR-0010) and returns `None` if the template's topology is rejected
/// — including the stereo-specific requirement of a single reachable
/// `StereoOutput`.
pub fn CompiledStereoDsp::compile(
compiled_template : CompiledTemplate,
context : DspContext,
) -> CompiledStereoDsp? {
CompiledStereoDsp::compile_internal(
compiled_template.optimized,
context,
compiled_template.template.length(),
compiled_template.index_map,
)
}
///|
fn CompiledStereoDsp::compile_raw(
nodes : Array[DspNode],
context : DspContext,
) -> CompiledStereoDsp? {
let count = nodes.length()
let identity_map = FixedArray::makei(count, fn(i) { i })
CompiledStereoDsp::compile_internal(nodes, context, count, identity_map)
}
///|
fn CompiledStereoDsp::compile_internal(
nodes : Array[DspNode],
context : DspContext,
original_count : Int,
opt_map : FixedArray[Int],
) -> CompiledStereoDsp? {
compile_graph_impl(
nodes,
context,
original_count,
opt_map,
stereo_compile_plan,
fn(fixed_nodes, feedback_edges) {
if feedback_edges.length() == 0 {
valid_terminal_stereo_shapes(fixed_nodes)
} else {
valid_feedback_terminal_stereo_graph(fixed_nodes, feedback_edges)
}
},
).map(fn(graph) { CompiledStereoDsp(graph) })
}
///|
fn stereo_compile_plan(
nodes : Array[DspNode],
context : DspContext,
) -> (Array[Int], Array[(Int, Int, Int)])? {
let sample_rate = context.sample_rate()
if !@dsp.is_finite_positive(sample_rate) {
return None
}
let output_index = match
find_single_output_index_of_kind(nodes, DspNodeKind::StereoOutput) {
Some(output_index) => output_index
None => return None
}
if nodes.is_empty() {
return None
}
for index = 0; index < nodes.length(); index = index + 1 {
let node = nodes[index]
if !valid_stereo_node_inputs(node, nodes.length(), sample_rate) {
return None
}
}
let marks = FixedArray::make(nodes.length(), 0)
let order = Array::new()
let feedback_edges = Array::new()
visit_graph_node_with_feedback(
output_index, nodes, marks, order, feedback_edges,
)
if order.length() != nodes.length() {
return None
}
Some((order, feedback_edges))
}
///|
fn CompiledGraph::compiled_buffer_capacity(self : CompiledGraph) -> Int {
if self.buffers.length() == 0 {
0
} else {
self.buffers[0].length()
}
}
///|
fn CompiledGraph::compiled_index_for(
self : CompiledGraph,
node_index : Int,
) -> Int? {
if node_index < 0 || node_index >= self.index_map.length() {
None
} else {
let compiled = self.index_map[node_index]
if compiled < 0 {
None
} else {
Some(compiled)
}
}
}
///|
fn CompiledDsp::compiled_buffer_capacity(self : CompiledDsp) -> Int {
self.0.compiled_buffer_capacity()
}
///|
fn CompiledStereoDsp::compiled_buffer_capacity(self : CompiledStereoDsp) -> Int {
self.0.compiled_buffer_capacity()
}
///|
fn mono_compile_plan(
nodes : Array[DspNode],
context : DspContext,
) -> (Array[Int], Array[(Int, Int, Int)])? {
let sample_rate = context.sample_rate()
if !@dsp.is_finite_positive(sample_rate) {
return None
}
let output_index = match find_single_output_index(nodes) {
Some(output_index) => output_index
None => return None
}
for index = 0; index < nodes.length(); index = index + 1 {
if !valid_node_inputs(nodes[index], nodes.length(), sample_rate) {
return None
}
}
let marks = FixedArray::make(nodes.length(), 0)
let order = Array::new()
let feedback_edges = Array::new()
visit_graph_node_with_feedback(
output_index, nodes, marks, order, feedback_edges,
)
if order.length() != nodes.length() {
return None
}
Some((order, feedback_edges))
}
///|
fn visit_graph_node_with_feedback(
index : Int,
nodes : Array[DspNode],
marks : FixedArray[Int],
order : Array[Int],
feedback_edges : Array[(Int, Int, Int)],
) -> Unit {
if marks[index] != 0 {
return
}
marks[index] = 1
visit_graph_dependencies_with_feedback(
index, nodes, marks, order, feedback_edges,
)
marks[index] = 2
order.push(index)
}
///|
fn visit_graph_dependencies_with_feedback(
index : Int,
nodes : Array[DspNode],
marks : FixedArray[Int],
order : Array[Int],
feedback_edges : Array[(Int, Int, Int)],
) -> Unit {
let node = nodes[index]
match node.kind {
Constant => ()
Oscillator =>
if node.input0 >= 0 {
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
}
Noise => ()
Adsr => ()
Biquad =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
Delay =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
Gain => {
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
if node.input1 >= 0 {
visit_graph_input_with_feedback(
node.input1,
index,
1,
nodes,
marks,
order,
feedback_edges,
)
}
}
Mul => {
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
visit_graph_input_with_feedback(
node.input1,
index,
1,
nodes,
marks,
order,
feedback_edges,
)
}
Mix => {
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
visit_graph_input_with_feedback(
node.input1,
index,
1,
nodes,
marks,
order,
feedback_edges,
)
}
Clip =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
Output =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
Pan =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
StereoGain =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
StereoClip =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
StereoBiquad =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
StereoDelay =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
StereoMixDown =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
StereoOutput =>
visit_graph_input_with_feedback(
node.input0,
index,
0,
nodes,
marks,
order,
feedback_edges,
)
}
}
///|
fn visit_graph_input_with_feedback(
input : Int,
target_index : Int,
target_slot : Int,
nodes : Array[DspNode],
marks : FixedArray[Int],
order : Array[Int],
feedback_edges : Array[(Int, Int, Int)],
) -> Unit {
match marks[input] {
0 =>
visit_graph_node_with_feedback(input, nodes, marks, order, feedback_edges)
1 => feedback_edges.push((input, target_index, target_slot))
_ => ()
}
}
///|
fn find_single_output_index(nodes : Array[DspNode]) -> Int? {
find_single_output_index_of_kind(nodes, DspNodeKind::Output)
}
///|
fn find_single_output_index_of_kind(
nodes : Array[DspNode],
kind : DspNodeKind,
) -> Int? {
let mut output_index = -1
for index = 0; index < nodes.length(); index = index + 1 {
if nodes[index].kind == kind {
if output_index >= 0 {
return None
}
output_index = index
}
}
if output_index >= 0 {
Some(output_index)
} else {
None
}
}
///|
fn remapped_feedback_edges(
edges : Array[(Int, Int, Int)],
remap : FixedArray[Int],
) -> FixedArray[(Int, Int, Int)] {
FixedArray::makei(edges.length(), index => {
let edge = edges[index]
(remap[edge.0], remap[edge.1], edge.2)
})
}
///|
fn compiled_block_size(context : DspContext) -> Int {
if context.block_size() > 0 {
context.block_size()
} else {
1
}
}
///|
fn make_graph_osc_state(node : DspNode) -> Oscillator? {
match node.kind {
Oscillator => Some(Oscillator::new())
_ => None
}
}
///|
fn make_graph_noise_state(node : DspNode) -> Noise? {
match node.kind {
Noise => Some(Noise::new(node.seed))
_ => None
}
}
///|
fn make_graph_env_state(node : DspNode) -> Adsr? {
match node.kind {
Adsr =>
Some(
Adsr::new(
attack_ms=node.value0,
decay_ms=node.value1,
sustain=node.value2,
release_ms=node.value3,
),
)
_ => None
}
}
///|
fn make_graph_biquad_state(node : DspNode) -> Biquad? {
match node.kind {
Biquad => Some(Biquad::new())
_ => None
}
}
///|
fn make_graph_stereo_biquad_state(node : DspNode) -> Biquad? {
match node.kind {
StereoBiquad => Some(Biquad::new())
_ => None
}
}
///|
fn make_graph_stereo_delay_state(node : DspNode) -> DelayLine? {
match node.kind {
StereoDelay =>
Some(
DelayLine::new(
node.delay_max_samples,
delay_samples=node.delay_samples,
feedback=node.value0,
),
)
_ => None
}
}
///|