1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! ProgramRunner — pure signal transform for DSL-compiled programs.
//!
//! Thin wrapper around a compiled graph engine. It has no I/O knowledge —
//! the caller feeds input buffers and receives output buffers.
//!
//! # Safety
//!
//! `!Send + !Sync` (via `PhantomData<*const ()>`) — must stay on the I/O
//! callback thread.
use std::marker::PhantomData;
use rill_core::queues::CommandEnum;
use rill_core::time::ClockTick;
use rill_core_actor::ActorRef;
use crate::graph_engine::CompiledGraphEngine;
/// Pure signal transform: `&[&[f32]]` → `&mut [&mut [f32]]`.
///
/// The caller is responsible for wiring I/O backends and managing the
/// driver lifecycle. Use [`apply`](ProgramRunner::apply) inside the
/// process callback set on the driver.
pub struct ProgramRunner<const BUF_SIZE: usize> {
engine: CompiledGraphEngine<f32, BUF_SIZE>,
parent_ref: Option<ActorRef<CommandEnum>>,
_not_send_sync: PhantomData<*const ()>,
}
impl<const BUF_SIZE: usize> ProgramRunner<BUF_SIZE> {
/// Create a new runner wrapping a compiled graph engine.
pub fn new(
engine: CompiledGraphEngine<f32, BUF_SIZE>,
parent_ref: Option<ActorRef<CommandEnum>>,
) -> Self {
Self {
engine,
parent_ref,
_not_send_sync: PhantomData,
}
}
/// Handle for sending `SetParameter` commands from control threads.
pub fn handle(&self) -> ActorRef<CommandEnum> {
self.engine.handle()
}
/// Reference to the underlying compiled engine.
pub fn engine(&self) -> &CompiledGraphEngine<f32, BUF_SIZE> {
&self.engine
}
/// Process one tick: transform `inputs` into `outputs`.
///
/// Called from the driver's process callback. The caller provides
/// the input data (from `IoCapture`) and receives the output
/// (destined for `IoPlayback`). Clock events are forwarded to the
/// engine's mailbox and the optional parent actor.
pub fn apply(&mut self, inputs: &[&[f32]], outputs: &mut [&mut [f32]], tick: &ClockTick) {
let chunk_end = tick.sample_pos + tick.samples_since_last as u64;
let _ = self.engine.process_tick(inputs, outputs, chunk_end);
if tick.is_final {
self.engine
.handle()
.send(CommandEnum::ClockTick(tick.clone()));
if let Some(ref parent) = self.parent_ref {
parent.send(CommandEnum::ClockTick(tick.clone()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rill_core::time::ClockTick;
#[test]
fn apply_produces_finite_output() {
use crate::builtin::Registry;
use crate::compile_graph;
let engine = compile_graph::<f32, 64>("main = _ * 0.5", &Registry::new(), 44100.0).unwrap();
let mut runner = ProgramRunner::new(engine, None);
let tick = ClockTick {
sample_pos: 0,
samples_since_last: 4,
sample_rate: 44100.0,
speed_ratio: 1.0,
io_quantum: 4,
source: "test".into(),
is_new_block: true,
is_final: true,
tempo: None,
};
let input = [0.5f32; 4];
let mut output = [0.0f32; 4];
runner.apply(&[&input[..]], &mut [&mut output[..]], &tick);
for v in &output {
assert!(v.is_finite());
}
}
}