///|
/// A tagless interpretation that builds an `Array[DspNode]` graph.
///
/// Each `GraphBuilder` value carries a mutable node array and the index
/// of the "current" node within that array. Source nodes (`constant`,
/// `noise`, `adsr`) start a fresh array, unary ops append to the
/// existing array, and binary ops merge two arrays when they originate
/// from different sources.
pub struct GraphBuilder {
priv nodes_ : Array[DspNode]
priv index : Int
}
///|
/// Return the accumulated node array.
pub fn GraphBuilder::nodes(self : GraphBuilder) -> Array[DspNode] {
self.nodes_
}
///|
/// Merge `source` nodes into `target`, remapping indices by the target
/// length. Returns the offset applied (0 when arrays are identical).
fn merge_into(target : Array[DspNode], source : Array[DspNode]) -> Int {
if physical_equal(target, source) {
return 0
}
let offset = target.length()
for i = 0; i < source.length(); i = i + 1 {
target.push(remap_node_by_offset(source[i], offset))
}
offset
}
///|
/// Create a copy of `node` with `input0` and `input1` shifted by `offset`.
/// Inputs that are `-1` (meaning "no input") are left unchanged.
fn remap_node_by_offset(node : DspNode, offset : Int) -> DspNode {
let new_input0 = if node.input0 >= 0 {
node.input0 + offset
} else {
node.input0
}
let new_input1 = if node.input1 >= 0 {
node.input1 + offset
} else {
node.input1
}
DspNode::new(
node.kind,
new_input0,
new_input1,
node.value0,
node.value1,
node.value2,
node.value3,
node.waveform,
node.filter_mode,
node.delay_max_samples,
node.delay_samples,
node.seed,
)
}
// ---------------------------------------------------------------------------
// ArithSym
// ---------------------------------------------------------------------------
///|
pub impl ArithSym for GraphBuilder with constant(v) -> GraphBuilder {
let nodes : Array[DspNode] = []
nodes.push(DspNode::constant(v))
{ nodes_: nodes, index: 0 }
}
///|
pub impl ArithSym for GraphBuilder with mul(a, b) -> GraphBuilder {
let b_offset = merge_into(a.nodes_, b.nodes_)
let b_index = b.index + b_offset
let idx = a.nodes_.length()
a.nodes_.push(DspNode::mul(a.index, b_index))
{ nodes_: a.nodes_, index: idx }
}
///|
pub impl ArithSym for GraphBuilder with mix(a, b) -> GraphBuilder {
let b_offset = merge_into(a.nodes_, b.nodes_)
let b_index = b.index + b_offset
let idx = a.nodes_.length()
a.nodes_.push(DspNode::mix(a.index, b_index))
{ nodes_: a.nodes_, index: idx }
}
///|
pub impl ArithSym for GraphBuilder with clip(self, threshold) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(DspNode::clip(self.index, threshold))
{ nodes_: self.nodes_, index: idx }
}
// ---------------------------------------------------------------------------
// DspSym
// ---------------------------------------------------------------------------
///|
pub impl DspSym for GraphBuilder with oscillator(freq, waveform) -> GraphBuilder {
let idx = freq.nodes_.length()
freq.nodes_.push(DspNode::oscillator_from(freq.index, waveform))
{ nodes_: freq.nodes_, index: idx }
}
///|
pub impl DspSym for GraphBuilder with noise(seed) -> GraphBuilder {
let nodes : Array[DspNode] = []
nodes.push(DspNode::noise(seed))
{ nodes_: nodes, index: 0 }
}
///|
pub impl DspSym for GraphBuilder with adsr(a, d, s, r) -> GraphBuilder {
let nodes : Array[DspNode] = []
nodes.push(DspNode::adsr(attack_ms=a, decay_ms=d, sustain=s, release_ms=r))
{ nodes_: nodes, index: 0 }
}
///|
pub impl DspSym for GraphBuilder with gain(self, amount) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(DspNode::gain(self.index, amount))
{ nodes_: self.nodes_, index: idx }
}
///|
pub impl DspSym for GraphBuilder with output(self) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(DspNode::output(self.index))
{ nodes_: self.nodes_, index: idx }
}
// ---------------------------------------------------------------------------
// FilterSym
// ---------------------------------------------------------------------------
///|
pub impl FilterSym for GraphBuilder with biquad(self, mode, cutoff, q) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(
DspNode::biquad(input=self.index, mode~, cutoff_hz=cutoff, q~),
)
{ nodes_: self.nodes_, index: idx }
}
// ---------------------------------------------------------------------------
// DelaySym
// ---------------------------------------------------------------------------
///|
pub impl DelaySym for GraphBuilder with delay(
self,
max_samples,
delay_samples,
feedback,
) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(
DspNode::delay(
input=self.index,
max_delay_samples=max_samples,
delay_samples~,
feedback~,
),
)
{ nodes_: self.nodes_, index: idx }
}
// ---------------------------------------------------------------------------
// StereoSym
// ---------------------------------------------------------------------------
///|
pub impl StereoSym for GraphBuilder with pan(self, position) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(DspNode::pan(self.index, position))
{ nodes_: self.nodes_, index: idx }
}
///|
pub impl StereoSym for GraphBuilder with stereo_gain(self, amount) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(DspNode::stereo_gain(input=self.index, amount~))
{ nodes_: self.nodes_, index: idx }
}
///|
pub impl StereoSym for GraphBuilder with stereo_clip(self, threshold) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(DspNode::stereo_clip(input=self.index, threshold~))
{ nodes_: self.nodes_, index: idx }
}
///|
pub impl StereoSym for GraphBuilder with stereo_mixdown(a) -> GraphBuilder {
let idx = a.nodes_.length()
a.nodes_.push(DspNode::stereo_mixdown(a.index))
{ nodes_: a.nodes_, index: idx }
}
///|
pub impl StereoSym for GraphBuilder with stereo_output(a) -> GraphBuilder {
let idx = a.nodes_.length()
a.nodes_.push(DspNode::stereo_output(a.index))
{ nodes_: a.nodes_, index: idx }
}
// ---------------------------------------------------------------------------
// StereoFilterSym
// ---------------------------------------------------------------------------
///|
pub impl StereoFilterSym for GraphBuilder with stereo_biquad(
self,
mode,
cutoff,
q,
) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(
DspNode::stereo_biquad(input=self.index, mode~, cutoff_hz=cutoff, q~),
)
{ nodes_: self.nodes_, index: idx }
}
// ---------------------------------------------------------------------------
// StereoDelaySym
// ---------------------------------------------------------------------------
///|
pub impl StereoDelaySym for GraphBuilder with stereo_delay(
self,
max_samples,
delay_samples,
feedback,
) -> GraphBuilder {
let idx = self.nodes_.length()
self.nodes_.push(
DspNode::stereo_delay(
input=self.index,
max_delay_samples=max_samples,
delay_samples~,
feedback~,
),
)
{ nodes_: self.nodes_, index: idx }
}
// ---------------------------------------------------------------------------
// Replay bridge
// ---------------------------------------------------------------------------
///|
/// Walk a `DspNode` array and reconstruct the DSP graph through the tagless
/// API, enabling round-trip testing (GraphBuilder -> nodes -> replay -> Eval).
/// Returns `None` for empty arrays or invalid node references.
pub fn[
T : ArithSym + DspSym + FilterSym + DelaySym + StereoSym + StereoFilterSym + StereoDelaySym,
] replay(
nodes : Array[DspNode],
) -> T? {
let len = nodes.length()
if len == 0 {
return None
}
let results : FixedArray[T?] = FixedArray::make(len, None)
for i = 0; i < len; i = i + 1 {
let node = nodes[i]
let value : T? = match node.kind {
Constant => Some(T::constant(node.value0))
Oscillator =>
if node.input0 >= 0 {
match replay_get_result(results, node.input0) {
Some(v) => Some(T::oscillator(v, node.waveform))
None => None
}
} else {
Some(T::oscillator(T::constant(node.value0), node.waveform))
}
Noise => Some(T::noise(node.seed))
Adsr => Some(T::adsr(node.value0, node.value1, node.value2, node.value3))
Biquad =>
match replay_get_result(results, node.input0) {
Some(v) =>
Some(T::biquad(v, node.filter_mode, node.value0, node.value1))
None => None
}
Delay =>
match replay_get_result(results, node.input0) {
Some(v) =>
Some(
T::delay(
v,
node.delay_max_samples,
node.delay_samples,
node.value0,
),
)
None => None
}
Gain =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::gain(v, node.value0))
None => None
}
Mul =>
match
(
replay_get_result(results, node.input0),
replay_get_result(results, node.input1),
) {
(Some(a), Some(b)) => Some(T::mul(a, b))
_ => None
}
Mix =>
match
(
replay_get_result(results, node.input0),
replay_get_result(results, node.input1),
) {
(Some(a), Some(b)) => Some(T::mix(a, b))
_ => None
}
Clip =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::clip(v, node.value0))
None => None
}
Output =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::output(v))
None => None
}
Pan =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::pan(v, node.value0))
None => None
}
StereoGain =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::stereo_gain(v, node.value0))
None => None
}
StereoClip =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::stereo_clip(v, node.value0))
None => None
}
StereoBiquad =>
match replay_get_result(results, node.input0) {
Some(v) =>
Some(
T::stereo_biquad(v, node.filter_mode, node.value0, node.value1),
)
None => None
}
StereoDelay =>
match replay_get_result(results, node.input0) {
Some(v) =>
Some(
T::stereo_delay(
v,
node.delay_max_samples,
node.delay_samples,
node.value0,
),
)
None => None
}
StereoMixDown =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::stereo_mixdown(v))
None => None
}
StereoOutput =>
match replay_get_result(results, node.input0) {
Some(v) => Some(T::stereo_output(v))
None => None
}
}
match value {
Some(v) => results[i] = Some(v)
None => return None
}
}
results[len - 1]
}
///|
/// Produce a CompiledTemplate from the builder's current node list.
/// Sugar over `CompiledTemplate::analyze(self.nodes())` — exists
/// because GraphBuilder is the canonical entry point and the runtime
/// boundary is CompiledTemplate. See ADR-0010.
pub fn GraphBuilder::analyze(self : GraphBuilder) -> CompiledTemplate {
CompiledTemplate::analyze(self.nodes())
}
///|
/// Safely access a replay result by index, returning None for out-of-bounds
/// or missing results.
fn[T] replay_get_result(results : FixedArray[T?], index : Int) -> T? {
if index >= 0 && index < results.length() {
results[index]
} else {
None
}
}