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, SetParameter};
47use rill_core::NodeId;
48
49use crate::automaton::LfoWaveform;
50use crate::control::{ControlEvent, Mapping, 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<SetParameter>>) -> 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(
112 id, frequency, amplitude, offset, waveform, interval, target, range, control, conflict,
113 );
114 }
115
116 pub fn add_envelope(
118 &mut self,
119 id: &str,
120 attack: f64,
121 decay: f64,
122 sustain: f64,
123 release: f64,
124 interval: Duration,
125 target: (NodeId, String),
126 range: (f64, f64),
127 control: ControlStrategy,
128 conflict: ConflictStrategy,
129 ) {
130 self.control.add_envelope_task(
131 id, attack, decay, sustain, release, interval, target, range, control, conflict,
132 );
133 }
134
135 pub fn add_mapping(&mut self, mapping: Mapping) {
137 self.control.add_mapping(mapping);
138 }
139
140 #[cfg(feature = "serde")]
145 pub fn load_document(
146 &mut self,
147 doc: &PatchbayDocument,
148 registry: &FunctionRegistry,
149 ) -> Result<(), String> {
150 doc.apply_to_async(&mut self.control, registry)
151 }
152
153 pub fn handle_event(&mut self, event: ControlEvent) {
159 self.control.handle_event(event);
160 }
161
162 pub fn attach_sequencer(
166 &mut self,
167 tel_rx: CrossbeamReceiver<Telemetry>,
168 sequencer: SnapshotSequencer,
169 ) -> SequencerHandle {
170 self.control.attach_sequencer(tel_rx, sequencer)
171 }
172
173 #[cfg(feature = "serde")]
178 pub fn load_sequencer_document(
179 &mut self,
180 tel_rx: CrossbeamReceiver<Telemetry>,
181 doc: crate::sequencer::SequencerDocument,
182 ) -> SequencerHandle {
183 let seq = doc.into_sequencer();
184 self.attach_sequencer(tel_rx, seq)
185 }
186
187 pub fn detach_sequencer(&mut self) {
189 self.control.detach_sequencer();
190 }
191
192 pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
194 self.control.sequencer_handle()
195 }
196
197 pub fn stop(&mut self) {
199 self.control.stop_all();
200 }
201
202 pub fn control(&self) -> &PatchbayControl {
204 &self.control
205 }
206
207 pub fn control_mut(&mut self) -> &mut PatchbayControl {
209 &mut self.control
210 }
211}
212
213impl Drop for PatchbayEngine {
214 fn drop(&mut self) {
215 self.stop();
216 }
217}
218
219#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::automaton::LfoWaveform;
227 use crate::control::{midi_cc, ControlEvent, Transform};
228 use crate::strategy::ControlStrategy;
229 use rill_core::queues::MpscQueue;
230 use rill_core::NodeId;
231
232 #[tokio::test]
233 async fn test_engine_creation() {
234 let queue = Arc::new(MpscQueue::new());
235 let engine = PatchbayEngine::new(queue);
236 drop(engine);
238 }
239
240 #[tokio::test]
241 async fn test_engine_add_lfo_produces_values() {
242 let queue = Arc::new(MpscQueue::with_capacity(64));
243 let mut engine = PatchbayEngine::new(queue.clone());
244
245 engine.add_lfo(
246 "test_lfo",
247 10.0,
248 1.0,
249 0.0,
250 LfoWaveform::Sine,
251 std::time::Duration::from_millis(10),
252 (NodeId(1), "cutoff".into()),
253 (0.0, 1.0),
254 ControlStrategy::Absolute,
255 crate::strategy::ConflictStrategy::LastWriteWins,
256 );
257
258 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
260
261 assert!(!queue.is_empty());
263 }
264
265 #[tokio::test]
266 async fn test_engine_handle_event_direct() {
267 let queue = Arc::new(MpscQueue::with_capacity(64));
268 let mut engine = PatchbayEngine::new(queue.clone());
269
270 engine.add_mapping(midi_cc(
271 7,
272 None,
273 NodeId(1),
274 "volume",
275 0.0,
276 1.0,
277 Transform::Linear,
278 ));
279
280 let event = ControlEvent::MidiControl {
281 channel: 0,
282 controller: 7,
283 value: 64,
284 normalized: 0.5,
285 };
286 engine.handle_event(event);
287
288 let cmd = queue.pop().unwrap();
289 assert_eq!(cmd.parameter.as_ref(), "volume");
290 assert!((cmd.value - 0.5).abs() < 1e-6);
291 }
292
293 #[tokio::test]
294 async fn test_engine_stop() {
295 let queue = Arc::new(MpscQueue::new());
296 let mut engine = PatchbayEngine::new(queue.clone());
297
298 engine.add_lfo(
299 "test_lfo",
300 1.0,
301 1.0,
302 0.0,
303 LfoWaveform::Sine,
304 std::time::Duration::from_millis(10),
305 (NodeId(1), "out".into()),
306 (0.0, 1.0),
307 ControlStrategy::Absolute,
308 crate::strategy::ConflictStrategy::LastWriteWins,
309 );
310
311 engine.stop();
312 }
314
315 #[tokio::test]
316 async fn test_engine_drop_stops_tasks() {
317 let queue = Arc::new(MpscQueue::new());
318 {
319 let mut engine = PatchbayEngine::new(queue.clone());
320 engine.add_lfo(
321 "test_lfo",
322 1.0,
323 1.0,
324 0.0,
325 LfoWaveform::Sine,
326 std::time::Duration::from_millis(10),
327 (NodeId(1), "out".into()),
328 (0.0, 1.0),
329 ControlStrategy::Absolute,
330 crate::strategy::ConflictStrategy::LastWriteWins,
331 );
332 } }
334
335 #[tokio::test]
336 async fn test_engine_no_runtime_panics() {
337 }
340}