///|
priv struct GraphTopologyEditFailure {
edit_index : Int
reason : GraphTopologyEditError
}
///|
fn apply_topology_edits_result(
nodes : Array[DspNode],
edits : Array[GraphTopologyEdit],
) -> Result[Unit, GraphTopologyEditFailure] {
for index = 0; index < edits.length(); index = index + 1 {
let edit = edits[index]
match apply_topology_edit_result(nodes, edit) {
Ok(_) => ()
Err(reason) => return Err({ edit_index: index, reason })
}
}
Ok(())
}
///|
fn apply_topology_edit_result(
nodes : Array[DspNode],
edit : GraphTopologyEdit,
) -> Result[Unit, GraphTopologyEditError] {
match edit {
ReplaceNode(node_index, replacement) =>
if node_index < 0 || node_index >= nodes.length() {
Err(GraphTopologyEditError::InvalidNodeIndex(node_index))
} else {
nodes[node_index] = replacement
Ok(())
}
RewireInput(node_index, input_slot, source_index) =>
if node_index < 0 || node_index >= nodes.length() {
Err(GraphTopologyEditError::InvalidNodeIndex(node_index))
} else if source_index < 0 || source_index >= nodes.length() {
Err(GraphTopologyEditError::InvalidSourceIndex(source_index))
} else {
apply_topology_rewire_input_result(
nodes, node_index, input_slot, source_index,
)
}
InsertNode(retarget_node_index, retarget_input_slot, inserted_node) =>
apply_topology_insert_node(
nodes, retarget_node_index, inserted_node, retarget_input_slot,
)
DeleteNode(
delete_node_index,
retarget_node_index,
retarget_input_slot,
replacement_source_index
) =>
if delete_node_index < 0 ||
delete_node_index >= nodes.length() ||
replacement_source_index < 0 ||
replacement_source_index >= nodes.length() {
if delete_node_index < 0 || delete_node_index >= nodes.length() {
Err(GraphTopologyEditError::InvalidNodeIndex(delete_node_index))
} else {
Err(
GraphTopologyEditError::InvalidSourceIndex(replacement_source_index),
)
}
} else {
apply_topology_delete_node(
nodes, delete_node_index, retarget_node_index, retarget_input_slot, replacement_source_index,
)
}
InsertChain(retarget_node, input_slot, chain_nodes) =>
apply_topology_insert_chain(nodes, retarget_node, input_slot, chain_nodes)
DeleteChain(
start_node,
end_node,
retarget_node,
input_slot,
replacement_source
) =>
apply_topology_delete_chain(
nodes, start_node, end_node, retarget_node, input_slot, replacement_source,
)
}
}
///|
fn apply_topology_insert_node(
nodes : Array[DspNode],
retarget_node_index : Int,
inserted_template : DspNode,
retarget_input_slot : GraphTopologyInputSlot,
) -> Result[Unit, GraphTopologyEditError] {
if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
}
let old_source = match
topology_node_input(nodes[retarget_node_index], retarget_input_slot) {
Some(old_source) => old_source
None =>
return Err(
GraphTopologyEditError::UnsupportedInputSlot(
retarget_node_index,
retarget_input_slot,
nodes[retarget_node_index].kind,
),
)
}
let inserted = match topology_inserted_node(inserted_template, old_source) {
Some(inserted) => inserted
None =>
return Err(
GraphTopologyEditError::UnsupportedInsertTemplate(
inserted_template.kind,
),
)
}
let inserted_index = nodes.length()
nodes.push(inserted)
match
apply_topology_rewire_input_result(
nodes, retarget_node_index, retarget_input_slot, inserted_index,
) {
Ok(_) => Ok(())
Err(error) => {
ignore(nodes.pop())
Err(error)
}
}
}
///|
fn apply_topology_delete_node(
nodes : Array[DspNode],
delete_node_index : Int,
retarget_node_index : Int,
retarget_input_slot : GraphTopologyInputSlot,
replacement_source_index : Int,
) -> Result[Unit, GraphTopologyEditError] {
let unary_input = match
topology_node_input(
nodes[delete_node_index],
GraphTopologyInputSlot::Input0,
) {
Some(unary_input) => unary_input
None =>
return Err(
GraphTopologyEditError::DeleteNodeRequiresUnary(
delete_node_index,
nodes[delete_node_index].kind,
),
)
}
if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
}
let consumer_index = match
topology_single_consumer_for_input(nodes, delete_node_index) {
Some(consumer_index) => consumer_index
_ =>
return Err(
GraphTopologyEditError::DeleteNodeRequiresSingleConsumer(
delete_node_index,
),
)
}
if consumer_index != retarget_node_index {
return Err(
GraphTopologyEditError::DeleteNodeConsumerMismatch(
consumer_index, retarget_node_index,
),
)
}
// If the caller specified the deleted node itself as the replacement source,
// substitute its upstream input instead — the deleted node won't exist.
let expected_replacement = if replacement_source_index == delete_node_index {
unary_input
} else {
replacement_source_index
}
match
apply_topology_rewire_input_result(
nodes, consumer_index, retarget_input_slot, expected_replacement,
) {
Ok(_) => ()
Err(error) => return Err(error)
}
let compacted = Array::new(capacity=nodes.length() - 1)
for index in 0.. Result[Unit, GraphTopologyEditError] {
if chain_nodes.length() == 0 {
return Err(GraphTopologyEditError::EmptyInsertChain)
}
if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
}
// Validate all chain nodes are unary (no input1)
for index = 0; index < chain_nodes.length(); index = index + 1 {
let node = chain_nodes[index]
if node.input1 >= 0 {
return Err(GraphTopologyEditError::UnsupportedChainNode(index, node.kind))
}
}
// Get original source of the retarget node's input
let old_source = match
topology_node_input(nodes[retarget_node_index], retarget_input_slot) {
Some(old_source) => old_source
None =>
return Err(
GraphTopologyEditError::UnsupportedInputSlot(
retarget_node_index,
retarget_input_slot,
nodes[retarget_node_index].kind,
),
)
}
// For each node in the chain: append to authoring array
let mut prev_source = old_source
let mut last_index = -1
let first_chain_index = nodes.length()
for chain_node in chain_nodes {
let inserted = match topology_inserted_node(chain_node, prev_source) {
Some(inserted) => inserted
None => {
// Roll back: remove all chain nodes appended so far
while nodes.length() > first_chain_index {
ignore(nodes.pop())
}
return Err(
GraphTopologyEditError::UnsupportedInsertTemplate(chain_node.kind),
)
}
}
last_index = nodes.length()
nodes.push(inserted)
prev_source = last_index
}
// Retarget the downstream node's input to the last chain node
match
apply_topology_rewire_input_result(
nodes, retarget_node_index, retarget_input_slot, last_index,
) {
Ok(_) => Ok(())
Err(error) => {
// Roll back
while nodes.length() > first_chain_index {
ignore(nodes.pop())
}
Err(error)
}
}
}
///|
fn apply_topology_delete_chain(
nodes : Array[DspNode],
start_node_index : Int,
end_node_index : Int,
retarget_node_index : Int,
retarget_input_slot : GraphTopologyInputSlot,
replacement_source_index : Int,
) -> Result[Unit, GraphTopologyEditError] {
// Basic bounds check
if start_node_index < 0 || start_node_index >= nodes.length() {
return Err(GraphTopologyEditError::InvalidNodeIndex(start_node_index))
}
if end_node_index < 0 || end_node_index >= nodes.length() {
return Err(GraphTopologyEditError::InvalidNodeIndex(end_node_index))
}
if start_node_index > end_node_index {
return Err(
GraphTopologyEditError::InvalidDeleteRange(
start_node_index, end_node_index,
),
)
}
if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
}
if replacement_source_index < 0 || replacement_source_index >= nodes.length() {
return Err(
GraphTopologyEditError::InvalidSourceIndex(replacement_source_index),
)
}
// Reject if replacement_source is inside the deleted span
if replacement_source_index >= start_node_index &&
replacement_source_index <= end_node_index {
return Err(
GraphTopologyEditError::ReplacementSourceInDeletedRange(
replacement_source_index,
),
)
}
// Validate: each node in the chain must be unary (input1 < 0) and not
// Output/StereoOutput, and each must feed exactly one downstream consumer
for index = start_node_index; index <= end_node_index; index = index + 1 {
let node = nodes[index]
if node.input1 >= 0 {
return Err(
GraphTopologyEditError::DeleteChainRequiresUnary(index, node.kind),
)
}
if node.kind is Output || node.kind is StereoOutput {
return Err(
GraphTopologyEditError::DeleteChainRequiresUnary(index, node.kind),
)
}
let consumer = match topology_single_consumer_for_input(nodes, index) {
Some(consumer) => consumer
None =>
return Err(
GraphTopologyEditError::DeleteChainRequiresSingleConsumer(index),
)
}
// Each chain node except the last must feed the next node in the chain
if index < end_node_index {
if consumer != index + 1 {
return Err(
GraphTopologyEditError::DeleteChainConsumerMismatch(
index,
consumer,
index + 1,
),
)
}
// Last chain node must feed the retarget node
} else if consumer != retarget_node_index {
return Err(
GraphTopologyEditError::DeleteChainConsumerMismatch(
index, consumer, retarget_node_index,
),
)
}
}
// Retarget the downstream node's input to the replacement source
match
apply_topology_rewire_input_result(
nodes, retarget_node_index, retarget_input_slot, replacement_source_index,
) {
Ok(_) => ()
Err(error) => return Err(error)
}
// Build compacted array excluding all chain nodes, adjusting indices
// We need to delete nodes from start_node_index to end_node_index inclusive
let chain_count = end_node_index - start_node_index + 1
let compacted = Array::new(capacity=nodes.length() - chain_count)
for index in 0.. end_node_index {
let node = topology_chain_compacted_node_inputs(
nodes[index],
start_node_index,
end_node_index,
)
compacted.push(node)
}
}
nodes.clear()
for node in compacted {
nodes.push(node)
}
Ok(())
}
///|
fn topology_chain_compacted_node_inputs(
node : DspNode,
start_deleted : Int,
end_deleted : Int,
) -> DspNode {
DspNode::new(
node.kind,
topology_chain_compacted_input(node.input0, start_deleted, end_deleted),
topology_chain_compacted_input(node.input1, start_deleted, end_deleted),
node.value0,
node.value1,
node.value2,
node.value3,
node.waveform,
node.filter_mode,
node.delay_max_samples,
node.delay_samples,
node.seed,
)
}
///|
fn topology_chain_compacted_input(
input : Int,
start_deleted : Int,
end_deleted : Int,
) -> Int {
if input < 0 {
input
} else if input > end_deleted {
input - (end_deleted - start_deleted + 1)
} else if input >= start_deleted {
// This should not happen for valid graphs (we already rewired away from
// deleted nodes), but return -1 as a safety measure
-1
} else {
input
}
}
///|
fn topology_single_consumer_for_input(
nodes : Array[DspNode],
source_index : Int,
) -> Int? {
let mut consumer = -1
for index in 0..= 0 {
return None
}
consumer = index
}
}
if consumer >= 0 {
Some(consumer)
} else {
None
}
}
///|
fn topology_compacted_node_inputs(
node : DspNode,
deleted_index : Int,
) -> DspNode {
DspNode::new(
node.kind,
topology_compacted_input(node.input0, deleted_index),
topology_compacted_input(node.input1, deleted_index),
node.value0,
node.value1,
node.value2,
node.value3,
node.waveform,
node.filter_mode,
node.delay_max_samples,
node.delay_samples,
node.seed,
)
}
///|
fn topology_compacted_input(input : Int, deleted_index : Int) -> Int {
if input >= 0 && input > deleted_index {
input - 1
} else {
input
}
}
///|
fn topology_node_input(
node : DspNode,
input_slot : GraphTopologyInputSlot,
) -> Int? {
match (node.kind, input_slot) {
(DspNodeKind::Mul, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::Mul, GraphTopologyInputSlot::Input1) => Some(node.input1)
(DspNodeKind::Mix, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::Mix, GraphTopologyInputSlot::Input1) => Some(node.input1)
(DspNodeKind::Gain, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::Clip, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::Output, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::Pan, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::StereoGain, GraphTopologyInputSlot::Input0) =>
Some(node.input0)
(DspNodeKind::StereoClip, GraphTopologyInputSlot::Input0) =>
Some(node.input0)
(DspNodeKind::StereoMixDown, GraphTopologyInputSlot::Input0) =>
Some(node.input0)
(DspNodeKind::StereoOutput, GraphTopologyInputSlot::Input0) =>
Some(node.input0)
(DspNodeKind::Biquad, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::Delay, GraphTopologyInputSlot::Input0) => Some(node.input0)
(DspNodeKind::StereoBiquad, GraphTopologyInputSlot::Input0) =>
Some(node.input0)
(DspNodeKind::StereoDelay, GraphTopologyInputSlot::Input0) =>
Some(node.input0)
_ => None
}
}
///|
fn topology_inserted_node(template : DspNode, input_source : Int) -> DspNode? {
match template.kind {
DspNodeKind::Gain => Some(DspNode::gain(input_source, template.value0))
DspNodeKind::Clip => Some(DspNode::clip(input_source, template.value0))
DspNodeKind::Pan => Some(DspNode::pan(input_source, template.value0))
DspNodeKind::StereoGain =>
Some(DspNode::stereo_gain(input=input_source, amount=template.value0))
DspNodeKind::StereoClip =>
Some(DspNode::stereo_clip(input=input_source, threshold=template.value0))
DspNodeKind::StereoMixDown => Some(DspNode::stereo_mixdown(input_source))
DspNodeKind::Output => Some(DspNode::output(input_source))
DspNodeKind::StereoOutput => Some(DspNode::stereo_output(input_source))
DspNodeKind::Biquad =>
Some(
DspNode::biquad(
input=input_source,
mode=template.filter_mode,
cutoff_hz=template.value0,
q=template.value1,
),
)
DspNodeKind::Delay =>
Some(
DspNode::delay(
input=input_source,
max_delay_samples=template.delay_max_samples,
delay_samples=template.delay_samples,
feedback=template.value0,
),
)
DspNodeKind::StereoBiquad =>
Some(
DspNode::stereo_biquad(
input=input_source,
mode=template.filter_mode,
cutoff_hz=template.value0,
q=template.value1,
),
)
DspNodeKind::StereoDelay =>
Some(
DspNode::stereo_delay(
input=input_source,
max_delay_samples=template.delay_max_samples,
delay_samples=template.delay_samples,
feedback=template.value0,
),
)
_ => None
}
}
///|
fn apply_topology_rewire_input_result(
nodes : Array[DspNode],
node_index : Int,
input_slot : GraphTopologyInputSlot,
source_index : Int,
) -> Result[Unit, GraphTopologyEditError] {
let node = nodes[node_index]
let rewired = match (node.kind, input_slot) {
(DspNodeKind::Gain, GraphTopologyInputSlot::Input0) =>
Some(DspNode::gain(source_index, node.value0))
(DspNodeKind::Mul, GraphTopologyInputSlot::Input0) =>
Some(DspNode::mul(source_index, node.input1))
(DspNodeKind::Mul, GraphTopologyInputSlot::Input1) =>
Some(DspNode::mul(node.input0, source_index))
(DspNodeKind::Mix, GraphTopologyInputSlot::Input0) =>
Some(DspNode::mix(source_index, node.input1))
(DspNodeKind::Mix, GraphTopologyInputSlot::Input1) =>
Some(DspNode::mix(node.input0, source_index))
(DspNodeKind::Clip, GraphTopologyInputSlot::Input0) =>
Some(DspNode::clip(source_index, node.value0))
(DspNodeKind::Output, GraphTopologyInputSlot::Input0) =>
Some(DspNode::output(source_index))
(DspNodeKind::Pan, GraphTopologyInputSlot::Input0) =>
Some(DspNode::pan(source_index, node.value0))
(DspNodeKind::StereoGain, GraphTopologyInputSlot::Input0) =>
Some(DspNode::stereo_gain(input=source_index, amount=node.value0))
(DspNodeKind::StereoClip, GraphTopologyInputSlot::Input0) =>
Some(DspNode::stereo_clip(input=source_index, threshold=node.value0))
(DspNodeKind::StereoMixDown, GraphTopologyInputSlot::Input0) =>
Some(DspNode::stereo_mixdown(source_index))
(DspNodeKind::StereoOutput, GraphTopologyInputSlot::Input0) =>
Some(DspNode::stereo_output(source_index))
(DspNodeKind::Biquad, GraphTopologyInputSlot::Input0) =>
Some(
DspNode::biquad(
input=source_index,
mode=node.filter_mode,
cutoff_hz=node.value0,
q=node.value1,
),
)
(DspNodeKind::Delay, GraphTopologyInputSlot::Input0) =>
Some(
DspNode::delay(
input=source_index,
max_delay_samples=node.delay_max_samples,
delay_samples=node.delay_samples,
feedback=node.value0,
),
)
(DspNodeKind::StereoBiquad, GraphTopologyInputSlot::Input0) =>
Some(
DspNode::stereo_biquad(
input=source_index,
mode=node.filter_mode,
cutoff_hz=node.value0,
q=node.value1,
),
)
(DspNodeKind::StereoDelay, GraphTopologyInputSlot::Input0) =>
Some(
DspNode::stereo_delay(
input=source_index,
max_delay_samples=node.delay_max_samples,
delay_samples=node.delay_samples,
feedback=node.value0,
),
)
_ => None
}
match rewired {
Some(rewired) => {
nodes[node_index] = rewired
Ok(())
}
None =>
Err(
GraphTopologyEditError::UnsupportedInputSlot(
node_index,
input_slot,
node.kind,
),
)
}
}