Skip to main content

rill_patchbay/
engine.rs

1//! # PatchbayEngine — референсный оркестратор
2//!
3//! Высокоуровневая обёртка над `PatchbayControl`, `PortCombiner`
4//! и green threads. Скрывает детали спавна задач, отмены и маршрутизации.
5//!
6//! ## Использование
7//!
8//! ```no_run
9//! use std::sync::Arc;
10//! use std::time::Duration;
11//! use rill_core::queues::MpscQueue;
12//! use rill_core::NodeId;
13//! use rill_patchbay::prelude::*;
14//!
15//! let queue = Arc::new(MpscQueue::new());
16//! let mut engine = PatchbayEngine::new(queue.clone());
17//!
18//! // LFO как green thread
19//! engine.add_lfo(
20//!     "lfo1", 0.3, 0.5, 0.0, LfoWaveform::Sine,
21//!     Duration::from_millis(10),
22//!     (NodeId(2), "cutoff".into()), (100.0, 1000.0),
23//!     ControlStrategy::Absolute,
24//!     ConflictStrategy::BasePlusModulation,
25//! );
26//!
27//! // Маппинг MIDI → параметр
28//! engine.add_mapping(midi_cc(
29//!     21, None, NodeId(2), "cutoff", 100.0, 1000.0, Transform::Linear,
30//! ));
31//!
32//! // Обработка внешнего события
33//! engine.handle_event(ControlEvent::MidiControl {
34//!     channel: 1, controller: 21, value: 64, normalized: 0.5,
35//! });
36//!
37//! // Остановка всех задач
38//! engine.stop();
39//! ```
40
41use 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
58/// High-level orchestrator for the patchbay system.
59///
60/// Manages automaton green threads, port combiners with conflict
61/// resolution strategies, event mappings, and graceful shutdown.
62///
63/// All automaton management is delegated to [`PatchbayControl`];
64/// this struct provides a simplified API and ensures proper cleanup.
65pub struct PatchbayEngine {
66    control: PatchbayControl,
67}
68
69impl PatchbayEngine {
70    /// Create a new engine on the current tokio runtime.
71    ///
72    /// Requires an active tokio runtime (e.g. `#[tokio::main]`).
73    /// Panics if `tokio::runtime::Handle::try_current()` fails.
74    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    /// Add an automaton as a green thread with PortCombiner.
83    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    /// Add an LFO as a green thread.
98    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    /// Add an ADSR envelope as a green thread.
117    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    /// Add an event mapping (MIDI/OSC → parameter).
136    pub fn add_mapping(&mut self, mapping: Mapping) {
137        self.control.add_mapping(mapping);
138    }
139
140    /// Load a serialized patchbay document and apply it with async tasks.
141    ///
142    /// Servos with `async_interval_ms: Some(...)` become green threads;
143    /// others fall back to sync mode.
144    #[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    /// Route an external event through active mappings.
154    ///
155    /// If a `PortCombiner` exists for the target parameter, the
156    /// value is routed there for conflict resolution. Otherwise
157    /// it goes directly to the command queue.
158    pub fn handle_event(&mut self, event: ControlEvent) {
159        self.control.handle_event(event);
160    }
161
162    /// Attach a parameter-lock sequencer driven by audio-thread clock ticks.
163    ///
164    /// See [`PatchbayControl::attach_sequencer`] for details.
165    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    /// Load a serialised sequencer document and attach it.
174    ///
175    /// Convenience wrapper: deserialises the document into a
176    /// [`SnapshotSequencer`], then calls [`attach_sequencer`](Self::attach_sequencer).
177    #[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    /// Detach the sequencer: abort its task and drop the handle.
188    pub fn detach_sequencer(&mut self) {
189        self.control.detach_sequencer();
190    }
191
192    /// Get a reference to the sequencer handle, if attached.
193    pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
194        self.control.sequencer_handle()
195    }
196
197    /// Stop all automaton green threads and clear mappings.
198    pub fn stop(&mut self) {
199        self.control.stop_all();
200    }
201
202    /// Borrow the inner control.
203    pub fn control(&self) -> &PatchbayControl {
204        &self.control
205    }
206
207    /// Mutably borrow the inner control.
208    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// =============================================================================
220// Тесты
221// =============================================================================
222
223#[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        // Just verifying no panic
237        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        // Let automaton run for a bit
259        tokio::time::sleep(std::time::Duration::from_millis(30)).await;
260
261        // Should have produced values
262        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        // After stop, no panic = green threads stopped cleanly
313    }
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        } // drop → stop_all
333    }
334
335    #[tokio::test]
336    async fn test_engine_no_runtime_panics() {
337        // This test verifies that creating the engine outside tokio panics.
338        // We can't easily test this in #[tokio::test], so we just note it.
339    }
340}