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;
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
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<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    /// 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(id, frequency, amplitude, offset, waveform, interval, target, range, control, conflict);
112    }
113
114    /// Add an ADSR envelope as a green thread.
115    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    /// Add an event mapping (MIDI/OSC → parameter).
132    pub fn add_mapping(&mut self, mapping: Mapping) {
133        self.control.add_mapping(mapping);
134    }
135
136    /// Load a serialized patchbay document and apply it with async tasks.
137    ///
138    /// Servos with `async_interval_ms: Some(...)` become green threads;
139    /// others fall back to sync mode.
140    #[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    /// Route an external event through active mappings.
150    ///
151    /// If a `PortCombiner` exists for the target parameter, the
152    /// value is routed there for conflict resolution. Otherwise
153    /// it goes directly to the command queue.
154    pub fn handle_event(&mut self, event: ControlEvent) {
155        self.control.handle_event(event);
156    }
157
158    /// Attach a parameter-lock sequencer driven by audio-thread clock ticks.
159    ///
160    /// See [`PatchbayControl::attach_sequencer`] for details.
161    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    /// Load a serialised sequencer document and attach it.
170    ///
171    /// Convenience wrapper: deserialises the document into a
172    /// [`SnapshotSequencer`], then calls [`attach_sequencer`](Self::attach_sequencer).
173    #[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    /// Detach the sequencer: abort its task and drop the handle.
184    pub fn detach_sequencer(&mut self) {
185        self.control.detach_sequencer();
186    }
187
188    /// Get a reference to the sequencer handle, if attached.
189    pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
190        self.control.sequencer_handle()
191    }
192
193    /// Stop all automaton green threads and clear mappings.
194    pub fn stop(&mut self) {
195        self.control.stop_all();
196    }
197
198    /// Borrow the inner control.
199    pub fn control(&self) -> &PatchbayControl {
200        &self.control
201    }
202
203    /// Mutably borrow the inner control.
204    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// =============================================================================
216// Тесты
217// =============================================================================
218
219#[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        // Just verifying no panic
233        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        // Let automaton run for a bit
255        tokio::time::sleep(std::time::Duration::from_millis(30)).await;
256
257        // Should have produced values
258        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        // After stop, no panic = green threads stopped cleanly
309    }
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        } // drop → stop_all
329    }
330
331    #[tokio::test]
332    async fn test_engine_no_runtime_panics() {
333        // This test verifies that creating the engine outside tokio panics.
334        // We can't easily test this in #[tokio::test], so we just note it.
335    }
336}