1use std::sync::Arc;
42use std::time::Duration;
43
44use crossbeam_channel::Receiver as CrossbeamReceiver;
45use rill_core::queues::telemetry::Telemetry;
46use rill_core::queues::MpscQueue;
47use rill_core::NodeId;
48
49use crate::automaton::LfoWaveform;
50use crate::control::{ControlEvent, Mapping, ParameterCommand, PatchbayControl};
51#[cfg(feature = "serde")]
52use crate::document::PatchbayDocument;
53#[cfg(feature = "serde")]
54use crate::function_registry::FunctionRegistry;
55use crate::sequencer::{SequencerHandle, SnapshotSequencer};
56use crate::strategy::{ConflictStrategy, ControlStrategy};
57
58pub struct PatchbayEngine {
66 control: PatchbayControl,
67}
68
69impl PatchbayEngine {
70 pub fn new(command_queue: Arc<MpscQueue<ParameterCommand>>) -> Self {
75 let _ = tokio::runtime::Handle::try_current()
76 .expect("PatchbayEngine requires an active tokio runtime");
77 Self {
78 control: PatchbayControl::new(command_queue),
79 }
80 }
81
82 pub fn add_automaton<A: crate::control::Automaton + 'static>(
84 &mut self,
85 id: &str,
86 automaton: A,
87 interval: Duration,
88 target: (NodeId, String),
89 range: (f64, f64),
90 control: ControlStrategy,
91 conflict: ConflictStrategy,
92 ) {
93 self.control
94 .add_automaton_task(id, automaton, interval, target, range, control, conflict);
95 }
96
97 pub fn add_lfo(
99 &mut self,
100 id: &str,
101 frequency: f64,
102 amplitude: f64,
103 offset: f64,
104 waveform: LfoWaveform,
105 interval: Duration,
106 target: (NodeId, String),
107 range: (f64, f64),
108 control: ControlStrategy,
109 conflict: ConflictStrategy,
110 ) {
111 self.control.add_lfo_task(id, frequency, amplitude, offset, waveform, interval, target, range, control, conflict);
112 }
113
114 pub fn add_envelope(
116 &mut self,
117 id: &str,
118 attack: f64,
119 decay: f64,
120 sustain: f64,
121 release: f64,
122 interval: Duration,
123 target: (NodeId, String),
124 range: (f64, f64),
125 control: ControlStrategy,
126 conflict: ConflictStrategy,
127 ) {
128 self.control.add_envelope_task(id, attack, decay, sustain, release, interval, target, range, control, conflict);
129 }
130
131 pub fn add_mapping(&mut self, mapping: Mapping) {
133 self.control.add_mapping(mapping);
134 }
135
136 #[cfg(feature = "serde")]
141 pub fn load_document(
142 &mut self,
143 doc: &PatchbayDocument,
144 registry: &FunctionRegistry,
145 ) -> Result<(), String> {
146 doc.apply_to_async(&mut self.control, registry)
147 }
148
149 pub fn handle_event(&mut self, event: ControlEvent) {
155 self.control.handle_event(event);
156 }
157
158 pub fn attach_sequencer(
162 &mut self,
163 tel_rx: CrossbeamReceiver<Telemetry>,
164 sequencer: SnapshotSequencer,
165 ) -> SequencerHandle {
166 self.control.attach_sequencer(tel_rx, sequencer)
167 }
168
169 #[cfg(feature = "serde")]
174 pub fn load_sequencer_document(
175 &mut self,
176 tel_rx: CrossbeamReceiver<Telemetry>,
177 doc: crate::sequencer::SequencerDocument,
178 ) -> SequencerHandle {
179 let seq = doc.into_sequencer();
180 self.attach_sequencer(tel_rx, seq)
181 }
182
183 pub fn detach_sequencer(&mut self) {
185 self.control.detach_sequencer();
186 }
187
188 pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
190 self.control.sequencer_handle()
191 }
192
193 pub fn stop(&mut self) {
195 self.control.stop_all();
196 }
197
198 pub fn control(&self) -> &PatchbayControl {
200 &self.control
201 }
202
203 pub fn control_mut(&mut self) -> &mut PatchbayControl {
205 &mut self.control
206 }
207}
208
209impl Drop for PatchbayEngine {
210 fn drop(&mut self) {
211 self.stop();
212 }
213}
214
215#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::automaton::LfoWaveform;
223 use crate::control::{midi_cc, ControlEvent, Transform};
224 use crate::strategy::ControlStrategy;
225 use rill_core::queues::MpscQueue;
226 use rill_core::NodeId;
227
228 #[tokio::test]
229 async fn test_engine_creation() {
230 let queue = Arc::new(MpscQueue::new());
231 let engine = PatchbayEngine::new(queue);
232 drop(engine);
234 }
235
236 #[tokio::test]
237 async fn test_engine_add_lfo_produces_values() {
238 let queue = Arc::new(MpscQueue::with_capacity(64));
239 let mut engine = PatchbayEngine::new(queue.clone());
240
241 engine.add_lfo(
242 "test_lfo",
243 10.0,
244 1.0,
245 0.0,
246 LfoWaveform::Sine,
247 std::time::Duration::from_millis(10),
248 (NodeId(1), "cutoff".into()),
249 (0.0, 1.0),
250 ControlStrategy::Absolute,
251 crate::strategy::ConflictStrategy::LastWriteWins,
252 );
253
254 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
256
257 assert!(!queue.is_empty());
259 }
260
261 #[tokio::test]
262 async fn test_engine_handle_event_direct() {
263 let queue = Arc::new(MpscQueue::with_capacity(64));
264 let mut engine = PatchbayEngine::new(queue.clone());
265
266 engine.add_mapping(midi_cc(
267 7,
268 None,
269 NodeId(1),
270 "volume",
271 0.0,
272 1.0,
273 Transform::Linear,
274 ));
275
276 let event = ControlEvent::MidiControl {
277 channel: 0,
278 controller: 7,
279 value: 64,
280 normalized: 0.5,
281 };
282 engine.handle_event(event);
283
284 let cmd = queue.pop().unwrap();
285 assert_eq!(cmd.param, "volume");
286 assert!((cmd.value - 0.5).abs() < 1e-6);
287 }
288
289 #[tokio::test]
290 async fn test_engine_stop() {
291 let queue = Arc::new(MpscQueue::new());
292 let mut engine = PatchbayEngine::new(queue.clone());
293
294 engine.add_lfo(
295 "test_lfo",
296 1.0,
297 1.0,
298 0.0,
299 LfoWaveform::Sine,
300 std::time::Duration::from_millis(10),
301 (NodeId(1), "out".into()),
302 (0.0, 1.0),
303 ControlStrategy::Absolute,
304 crate::strategy::ConflictStrategy::LastWriteWins,
305 );
306
307 engine.stop();
308 }
310
311 #[tokio::test]
312 async fn test_engine_drop_stops_tasks() {
313 let queue = Arc::new(MpscQueue::new());
314 {
315 let mut engine = PatchbayEngine::new(queue.clone());
316 engine.add_lfo(
317 "test_lfo",
318 1.0,
319 1.0,
320 0.0,
321 LfoWaveform::Sine,
322 std::time::Duration::from_millis(10),
323 (NodeId(1), "out".into()),
324 (0.0, 1.0),
325 ControlStrategy::Absolute,
326 crate::strategy::ConflictStrategy::LastWriteWins,
327 );
328 } }
330
331 #[tokio::test]
332 async fn test_engine_no_runtime_panics() {
333 }
336}