///|
/// Failure reason for runtime graph control APIs.
pub(all) enum GraphControlError {
InvalidNodeIndex(Int)
/// The authoring node existed but was eliminated by graph optimization.
OrphanNode(Int)
InvalidGateNode(Int, DspNodeKind)
InvalidSlotForNode(Int, DspNodeKind, GraphParamSlot)
InvalidParamValue(Int, DspNodeKind, GraphParamSlot, Double)
MissingRuntimeState(Int, DspNodeKind)
} derive(Debug)
///|
pub impl Show for GraphControlError with output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
/// Trigger `gate_on()` on an ADSR node using its original authoring index.
pub fn CompiledDsp::gate_on(
self : CompiledDsp,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_on(node_index))
}
///|
/// Trigger `gate_off()` on an ADSR node using its original authoring index.
pub fn CompiledDsp::gate_off(
self : CompiledDsp,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_off(node_index))
}
///|
/// Update a runtime parameter on a compiled node using its original authoring
/// index.
pub fn CompiledDsp::set_param(
self : CompiledDsp,
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::set_param(node_index, slot, value))
}
///|
/// Apply one runtime control message, returning a specific rejection reason.
pub fn CompiledDsp::apply_control(
self : CompiledDsp,
control : GraphControl,
) -> Result[Unit, GraphControlError] {
self.0.apply_control_impl(control)
}
///|
/// Apply a runtime control batch transactionally.
pub fn CompiledDsp::apply_controls(
self : CompiledDsp,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.apply_controls_impl(controls)
}
///|
/// Validate a runtime control batch without mutating the compiled graph.
pub fn CompiledDsp::validate_controls(
self : CompiledDsp,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.validate_controls_impl(controls)
}
///|
/// Returns true when a voice using this compiled graph can be safely reclaimed.
///
/// Two-stage check:
/// 1. All ADSR nodes must be in Idle stage (envelope has finished)
/// 2. The last output buffer must be silent (all samples below threshold)
///
/// WHY two stages: ADSR-only detection would cut voices with downstream delay
/// or feedback tails that are still audible. Energy-only detection would keep
/// voices alive during sustain (where output is non-zero but expected).
pub fn CompiledDsp::is_voice_finished(self : CompiledDsp) -> Bool {
let nodes = self.0.nodes
let env_states = self.0.env_states
for i = 0; i < nodes.length(); i = i + 1 {
if nodes[i].kind is Adsr {
match env_states[i] {
Some(adsr) => if !(adsr.stage() is EnvStage::Idle) { return false }
None => ()
}
}
}
let last_index = nodes.length() - 1
if last_index < 0 {
return true
}
let output_buf = self.0.buffers[last_index]
for i = 0; i < output_buf.length(); i = i + 1 {
if output_buf.get(i).abs() > 0.0001 {
return false
}
}
true
}
///|
/// Trigger `gate_on()` on a stereo-graph ADSR node using its original
/// authoring index.
pub fn CompiledStereoDsp::gate_on(
self : CompiledStereoDsp,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_on(node_index))
}
///|
/// Trigger `gate_off()` on a stereo-graph ADSR node using its original
/// authoring index.
pub fn CompiledStereoDsp::gate_off(
self : CompiledStereoDsp,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_off(node_index))
}
///|
/// Update a runtime parameter on a compiled stereo node using its original
/// authoring index.
pub fn CompiledStereoDsp::set_param(
self : CompiledStereoDsp,
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::set_param(node_index, slot, value))
}
///|
/// Apply one runtime control message, returning a specific rejection reason.
pub fn CompiledStereoDsp::apply_control(
self : CompiledStereoDsp,
control : GraphControl,
) -> Result[Unit, GraphControlError] {
self.0.apply_control_impl(control)
}
///|
/// Apply a runtime control batch transactionally.
pub fn CompiledStereoDsp::apply_controls(
self : CompiledStereoDsp,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.apply_controls_impl(controls)
}
///|
/// Validate a runtime control batch without mutating the compiled graph.
pub fn CompiledStereoDsp::validate_controls(
self : CompiledStereoDsp,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.validate_controls_impl(controls)
}
///|
/// Trigger runtime gate-on through a mono hot-swap wrapper.
pub fn CompiledDspHotSwap::gate_on(
self : CompiledDspHotSwap,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_on(node_index))
}
///|
/// Trigger runtime gate-off through a mono hot-swap wrapper.
pub fn CompiledDspHotSwap::gate_off(
self : CompiledDspHotSwap,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_off(node_index))
}
///|
/// Update a runtime parameter through a mono hot-swap wrapper.
pub fn CompiledDspHotSwap::set_param(
self : CompiledDspHotSwap,
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::set_param(node_index, slot, value))
}
///|
/// Apply one runtime control message through a mono hot-swap wrapper.
pub fn CompiledDspHotSwap::apply_control(
self : CompiledDspHotSwap,
control : GraphControl,
) -> Result[Unit, GraphControlError] {
self.0.apply_control_impl(control)
}
///|
/// Apply a runtime control batch transactionally through a mono hot-swap wrapper.
pub fn CompiledDspHotSwap::apply_controls(
self : CompiledDspHotSwap,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.apply_controls_impl(controls)
}
///|
/// Trigger runtime gate-on through a stereo hot-swap wrapper.
pub fn CompiledStereoDspHotSwap::gate_on(
self : CompiledStereoDspHotSwap,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_on(node_index))
}
///|
/// Trigger runtime gate-off through a stereo hot-swap wrapper.
pub fn CompiledStereoDspHotSwap::gate_off(
self : CompiledStereoDspHotSwap,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_off(node_index))
}
///|
/// Update a runtime parameter through a stereo hot-swap wrapper.
pub fn CompiledStereoDspHotSwap::set_param(
self : CompiledStereoDspHotSwap,
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::set_param(node_index, slot, value))
}
///|
/// Apply one runtime control message through a stereo hot-swap wrapper.
pub fn CompiledStereoDspHotSwap::apply_control(
self : CompiledStereoDspHotSwap,
control : GraphControl,
) -> Result[Unit, GraphControlError] {
self.0.apply_control_impl(control)
}
///|
/// Apply a runtime control batch transactionally through a stereo hot-swap wrapper.
pub fn CompiledStereoDspHotSwap::apply_controls(
self : CompiledStereoDspHotSwap,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.apply_controls_impl(controls)
}
///|
/// Trigger runtime gate-on through a mono topology controller.
pub fn CompiledDspTopologyController::gate_on(
self : CompiledDspTopologyController,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_on(node_index))
}
///|
/// Trigger runtime gate-off through a mono topology controller.
pub fn CompiledDspTopologyController::gate_off(
self : CompiledDspTopologyController,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_off(node_index))
}
///|
/// Update a runtime parameter through a mono topology controller.
pub fn CompiledDspTopologyController::set_param(
self : CompiledDspTopologyController,
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::set_param(node_index, slot, value))
}
///|
/// Apply one runtime control message through a mono topology controller.
pub fn CompiledDspTopologyController::apply_control(
self : CompiledDspTopologyController,
control : GraphControl,
) -> Result[Unit, GraphControlError] {
self.0.apply_control_impl(control)
}
///|
/// Apply a runtime control batch transactionally through a mono topology controller.
pub fn CompiledDspTopologyController::apply_controls(
self : CompiledDspTopologyController,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.apply_controls_impl(controls)
}
///|
/// Trigger runtime gate-on through a stereo topology controller.
pub fn CompiledStereoDspTopologyController::gate_on(
self : CompiledStereoDspTopologyController,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_on(node_index))
}
///|
/// Trigger runtime gate-off through a stereo topology controller.
pub fn CompiledStereoDspTopologyController::gate_off(
self : CompiledStereoDspTopologyController,
node_index : Int,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::gate_off(node_index))
}
///|
/// Update a runtime parameter through a stereo topology controller.
pub fn CompiledStereoDspTopologyController::set_param(
self : CompiledStereoDspTopologyController,
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
self.apply_control(GraphControl::set_param(node_index, slot, value))
}
///|
/// Apply one runtime control message through a stereo topology controller.
pub fn CompiledStereoDspTopologyController::apply_control(
self : CompiledStereoDspTopologyController,
control : GraphControl,
) -> Result[Unit, GraphControlError] {
self.0.apply_control_impl(control)
}
///|
/// Apply a runtime control batch transactionally through a stereo topology
/// controller.
pub fn CompiledStereoDspTopologyController::apply_controls(
self : CompiledStereoDspTopologyController,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
self.0.apply_controls_impl(controls)
}
///|
fn CompiledGraph::apply_control_impl(
self : CompiledGraph,
control : GraphControl,
) -> Result[Unit, GraphControlError] {
match control.kind {
GateOn => apply_graph_gate_control_result(self, control.node_index, true)
GateOff => apply_graph_gate_control_result(self, control.node_index, false)
SetParam =>
apply_graph_param_control_result(
self,
control.node_index,
control.slot,
control.value,
)
}
}
///|
fn CompiledGraph::apply_controls_impl(
self : CompiledGraph,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
match self.validate_controls_impl(controls) {
Ok(_) => ()
Err(error) => return Err(error)
}
for index = 0; index < controls.length(); index = index + 1 {
match self.apply_control_impl(controls[index]) {
Ok(_) => ()
Err(error) => return Err(error)
}
}
Ok(())
}
///|
fn CompiledGraph::validate_controls_impl(
self : CompiledGraph,
controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
let simulated_nodes = FixedArray::makei(self.nodes.length(), index => {
self.nodes[index]
})
for index = 0; index < controls.length(); index = index + 1 {
match valid_graph_control_result(self, simulated_nodes, controls[index]) {
Ok(_) => ()
Err(error) => return Err(error)
}
}
Ok(())
}
///|
fn CompiledGraph::compiled_index_result(
self : CompiledGraph,
node_index : Int,
) -> Result[Int, GraphControlError] {
match self.compiled_index_for(node_index) {
Some(compiled) => Ok(compiled)
None =>
if node_index < 0 || node_index >= self.index_map.length() {
Err(GraphControlError::InvalidNodeIndex(node_index))
} else {
Err(GraphControlError::OrphanNode(node_index))
}
}
}
///|
fn apply_graph_gate_control_result(
graph : CompiledGraph,
node_index : Int,
gate_on : Bool,
) -> Result[Unit, GraphControlError] {
let compiled_index = match graph.compiled_index_result(node_index) {
Ok(index) => index
Err(error) => return Err(error)
}
match graph.env_states[compiled_index] {
Some(env) => {
if gate_on {
env.gate_on()
} else {
env.gate_off()
}
Ok(())
}
None => {
let node = graph.nodes[compiled_index]
if node.kind is Adsr {
Err(GraphControlError::MissingRuntimeState(node_index, node.kind))
} else {
Err(GraphControlError::InvalidGateNode(node_index, node.kind))
}
}
}
}
///|
fn apply_graph_param_control_result(
graph : CompiledGraph,
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
let compiled_index = match graph.compiled_index_result(node_index) {
Ok(index) => index
Err(error) => return Err(error)
}
let node = graph.nodes[compiled_index]
let updated = match
updated_node_param_result(
node,
node_index,
slot,
value,
graph.compile_sample_rate,
) {
Ok(updated) => updated
Err(error) => return Err(error)
}
graph.nodes[compiled_index] = updated
if apply_runtime_param_side_effect(graph, compiled_index, updated, slot) {
Ok(())
} else {
Err(GraphControlError::MissingRuntimeState(node_index, node.kind))
}
}
///|
fn valid_graph_control_result(
graph : CompiledGraph,
simulated_nodes : FixedArray[DspNode],
control : GraphControl,
) -> Result[Unit, GraphControlError] {
match control.kind {
GateOn => valid_graph_gate_control_result(graph, control.node_index)
GateOff => valid_graph_gate_control_result(graph, control.node_index)
SetParam =>
valid_graph_param_control_result(
graph,
simulated_nodes,
control.node_index,
control.slot,
control.value,
)
}
}
///|
fn valid_graph_gate_control_result(
graph : CompiledGraph,
node_index : Int,
) -> Result[Unit, GraphControlError] {
let compiled_index = match graph.compiled_index_result(node_index) {
Ok(index) => index
Err(error) => return Err(error)
}
match graph.env_states[compiled_index] {
Some(_) => Ok(())
None => {
let node = graph.nodes[compiled_index]
if node.kind is Adsr {
Err(GraphControlError::MissingRuntimeState(node_index, node.kind))
} else {
Err(GraphControlError::InvalidGateNode(node_index, node.kind))
}
}
}
}
///|
fn valid_graph_param_control_result(
graph : CompiledGraph,
simulated_nodes : FixedArray[DspNode],
node_index : Int,
slot : GraphParamSlot,
value : Double,
) -> Result[Unit, GraphControlError] {
let compiled_index = match graph.compiled_index_result(node_index) {
Ok(index) => index
Err(error) => return Err(error)
}
let updated = match
updated_node_param_result(
simulated_nodes[compiled_index],
node_index,
slot,
value,
graph.compile_sample_rate,
) {
Ok(updated) => updated
Err(error) => return Err(error)
}
simulated_nodes[compiled_index] = updated
Ok(())
}
///|
fn updated_node_param_result(
node : DspNode,
node_index : Int,
slot : GraphParamSlot,
value : Double,
sample_rate : Double,
) -> Result[DspNode, GraphControlError] {
if !node_accepts_slot(node, slot) {
return Err(
GraphControlError::InvalidSlotForNode(node_index, node.kind, slot),
)
}
match updated_node_param(node, slot, value, sample_rate) {
Some(updated) => Ok(updated)
None =>
Err(
GraphControlError::InvalidParamValue(node_index, node.kind, slot, value),
)
}
}
///|
fn updated_node_param(
node : DspNode,
slot : GraphParamSlot,
value : Double,
sample_rate : Double,
) -> DspNode? {
match node.kind {
Constant =>
match slot {
Value0 if @dsp.is_finite(value) => Some(node_with_value0(node, value))
_ => None
}
Oscillator =>
// FM oscillators (input0 >= 0) read frequency from input buffer;
// reject Value0 updates for them since the value is unused.
if node.input0 >= 0 {
None
} else {
match slot {
Value0 if @dsp.is_finite(value) => Some(node_with_value0(node, value))
_ => None
}
}
Adsr => None
Noise => None
Biquad =>
match slot {
Value0 if valid_biquad_graph_params(sample_rate, value, node.value1) =>
Some(node_with_value0(node, value))
Value1 if valid_biquad_graph_params(sample_rate, node.value0, value) =>
Some(node_with_value1(node, value))
_ => None
}
Delay =>
match slot {
Value0 if valid_delay_feedback(value) =>
Some(node_with_value0(node, value))
DelaySamples =>
match exact_int_value(value) {
Some(ds) if valid_delay_samples(ds, node.delay_max_samples) =>
Some(node_with_delay_samples(node, ds))
_ => None
}
_ => None
}
Gain =>
match slot {
Value0 if @dsp.is_finite(value) => Some(node_with_value0(node, value))
_ => None
}
Mul => None
Mix => None
Clip =>
match slot {
Value0 if @dsp.is_finite(value) && value > 0.0 =>
Some(node_with_value0(node, value))
_ => None
}
Output => None
Pan =>
match slot {
Value0 if @dsp.is_finite(value) => Some(node_with_value0(node, value))
_ => None
}
StereoGain =>
match slot {
Value0 if @dsp.is_finite(value) => Some(node_with_value0(node, value))
_ => None
}
StereoClip =>
match slot {
Value0 if @dsp.is_finite(value) && value > 0.0 =>
Some(node_with_value0(node, value))
_ => None
}
StereoBiquad =>
match slot {
Value0 if valid_biquad_graph_params(sample_rate, value, node.value1) =>
Some(node_with_value0(node, value))
Value1 if valid_biquad_graph_params(sample_rate, node.value0, value) =>
Some(node_with_value1(node, value))
_ => None
}
StereoDelay =>
match slot {
Value0 if valid_delay_feedback(value) =>
Some(node_with_value0(node, value))
DelaySamples =>
match exact_int_value(value) {
Some(ds) if valid_delay_samples(ds, node.delay_max_samples) =>
Some(node_with_delay_samples(node, ds))
_ => None
}
_ => None
}
StereoMixDown => None
StereoOutput => None
}
}
///|
fn apply_runtime_param_side_effect(
graph : CompiledGraph,
compiled_index : Int,
node : DspNode,
slot : GraphParamSlot,
) -> Bool {
match node.kind {
Delay =>
match slot {
Value0 =>
match graph.delay_states[compiled_index] {
Some(delay) => {
delay.set_feedback(node.value0)
delay.set_delay_samples(node.delay_samples)
true
}
None => false
}
DelaySamples =>
match graph.delay_states[compiled_index] {
Some(delay) => {
delay.set_feedback(node.value0)
delay.set_delay_samples(node.delay_samples)
true
}
None => false
}
_ => false
}
Constant => true
Oscillator => true
Biquad => true
Gain => true
Clip => true
Pan => true
StereoGain => true
StereoClip => true
StereoBiquad => true
StereoDelay =>
match slot {
Value0 =>
apply_stereo_delay_param_side_effect(
graph.stereo_delay_left_states,
graph.stereo_delay_right_states,
compiled_index,
node.delay_samples,
node.value0,
)
DelaySamples =>
apply_stereo_delay_param_side_effect(
graph.stereo_delay_left_states,
graph.stereo_delay_right_states,
compiled_index,
node.delay_samples,
node.value0,
)
_ => false
}
StereoMixDown => false
_ => false
}
}
///|
fn exact_int_value(value : Double) -> Int? {
if !@dsp.is_finite(value) || value.trunc() != value {
return None
}
let int_value = value.to_int()
if Double::from_int(int_value) == value {
Some(int_value)
} else {
None
}
}
///|
fn valid_biquad_graph_params(
sample_rate : Double,
cutoff : Double,
q : Double,
) -> Bool {
let nyquist = sample_rate * NYQUIST_RATIO
@dsp.is_finite(sample_rate) &&
sample_rate > 0.0 &&
@dsp.is_finite(cutoff) &&
cutoff > 0.0 &&
cutoff < nyquist &&
@dsp.is_finite(q) &&
q > 0.0
}
///|
fn graph_default_delay_samples(value : Int) -> Int {
if value > 0 {
value
} else {
0
}
}
///|
fn valid_delay_feedback(value : Double) -> Bool {
@dsp.is_finite(value) &&
value >= -@dsp.max_feedback_amount() &&
value <= @dsp.max_feedback_amount()
}
///|
/// Shared validation for delay_samples used by both compile-time
/// (`valid_any_node_inputs`) and runtime (`updated_node_param`) paths.
fn valid_delay_samples(delay_samples : Int, delay_max_samples : Int) -> Bool {
delay_samples >= 0 && delay_samples <= delay_max_samples
}
///|
/// Structural check: does this node kind accept SetParam on the given slot?
/// Used by ControlBindingBuilder::build() to validate bindings at graph
/// construction time. Does not check value-domain constraints.
pub fn node_accepts_slot(node : DspNode, slot : GraphParamSlot) -> Bool {
match node.kind {
Constant => slot is Value0
Oscillator => node.input0 < 0 && slot is Value0
Noise => false
Adsr => false
Biquad => slot is Value0 || slot is Value1
Delay => slot is Value0 || slot is DelaySamples
Gain => slot is Value0
Mul => false
Mix => false
Clip => slot is Value0
Output => false
Pan => slot is Value0
StereoGain => slot is Value0
StereoClip => slot is Value0
StereoBiquad => slot is Value0 || slot is Value1
StereoDelay => slot is Value0 || slot is DelaySamples
StereoMixDown => false
StereoOutput => false
}
}
///|
fn apply_stereo_delay_param_side_effect(
left_states : FixedArray[DelayLine?],
right_states : FixedArray[DelayLine?],
compiled_index : Int,
delay_samples : Int,
feedback : Double,
) -> Bool {
match left_states[compiled_index] {
Some(left_delay) =>
match right_states[compiled_index] {
Some(right_delay) => {
left_delay.set_feedback(feedback)
left_delay.set_delay_samples(delay_samples)
right_delay.set_feedback(feedback)
right_delay.set_delay_samples(delay_samples)
true
}
None => false
}
None => false
}
}
///|
fn make_graph_delay_state(node : DspNode) -> DelayLine? {
match node.kind {
Delay =>
Some(
DelayLine::new(
node.delay_max_samples,
delay_samples=node.delay_samples,
feedback=node.value0,
),
)
_ => None
}
}