Skip to main content

rill_patchbay/
manager.rs

1//! Patchbay manager — central coordinator (DEPRECATED).
2//!
3//! `PatchbayManager` is a legacy component. It runs in a dedicated
4//! `std::thread` at a fixed update rate. Superseded by the async model:
5//! `PatchbayControl::add_automaton_task()` + tokio tasks.
6//!
7//! Old functionality:
8//! - Automata (LFO, envelopes, sequencers)
9//! - Event mappings (MIDI/OSC)
10//! - Servos (automaton-to-parameter bridge)
11//!
12//! Recommended replacements:
13//! - `PatchbayControl::add_lfo_task()`
14//! - `PatchbayControl::add_automaton_task()`
15//! - `PatchbayControl::handle_event()`
16
17use rill_core::prelude::*;
18use rill_core::queues::{MpscQueue, SetParameter, SignalSource};
19use std::collections::HashMap;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::Arc;
22use std::time::{Duration, Instant};
23
24use crate::automaton::{
25    EnvelopeAutomaton, FunctionAutomaton, LfoAutomaton, LfoWaveform, SequencerAutomaton, Step,
26};
27use crate::control::{
28    midi_cc, osc_address, AnyServo, Automaton, BoxedServo, ControlEvent, EventPattern, Mapping,
29    ParameterMapping, Transform,
30};
31
32// =============================================================================
33// Event for logging and debugging
34// =============================================================================
35
36/// Events emitted by the patchbay manager for logging and debugging.
37#[derive(Debug, Clone)]
38pub enum PatchbayEvent {
39    /// An automaton was updated with a new value.
40    AutomatonUpdated {
41        /// Automaton identifier.
42        id: String,
43        /// Current output value.
44        value: f64,
45        /// Current time.
46        time: f64,
47    },
48    /// A mapping was triggered by an incoming event.
49    MappingTriggered {
50        /// Matched event pattern description.
51        pattern: String,
52        /// Target parameter description.
53        target: String,
54        /// Mapped and transformed value.
55        value: f32,
56    },
57    /// A command was sent to the audio thread.
58    CommandSent(SetParameter),
59    /// An error occurred.
60    Error(String),
61}
62
63// =============================================================================
64// Patchbay statistics
65// =============================================================================
66
67/// Runtime statistics for the patchbay.
68#[derive(Debug, Clone, Default)]
69pub struct PatchbayStats {
70    /// Number of active automata.
71    pub automaton_count: usize,
72    /// Number of active mappings.
73    pub mapping_count: usize,
74    /// Total commands sent to the audio thread.
75    pub commands_sent: u64,
76    /// Duration of the last update cycle.
77    pub last_update: Option<Duration>,
78    /// Average update time in microseconds.
79    pub avg_update_time_us: f64,
80    /// Maximum update time in microseconds.
81    pub max_update_time_us: f64,
82    /// Total error count.
83    pub error_count: u64,
84}
85
86impl PatchbayStats {
87    /// Update statistics with the measured duration of one update cycle.
88    pub fn update(&mut self, update_duration: Duration) {
89        let us = update_duration.as_micros() as f64;
90        self.avg_update_time_us = self.avg_update_time_us * 0.9 + us * 0.1;
91        self.max_update_time_us = self.max_update_time_us.max(us);
92        self.last_update = Some(update_duration);
93    }
94}
95
96// =============================================================================
97// Patchbay configuration
98// =============================================================================
99
100/// Configuration for the patchbay.
101#[derive(Debug, Clone)]
102pub struct PatchbayConfig {
103    /// Automaton update rate in Hz.
104    pub update_rate_hz: f64,
105    /// Command queue capacity.
106    pub command_queue_size: usize,
107    /// Whether to collect runtime statistics.
108    pub collect_stats: bool,
109    /// Whether to emit log events.
110    pub log_events: bool,
111}
112
113impl Default for PatchbayConfig {
114    fn default() -> Self {
115        Self {
116            update_rate_hz: 1000.0,
117            command_queue_size: 1024,
118            collect_stats: true,
119            log_events: false,
120        }
121    }
122}
123
124// =============================================================================
125// Main patchbay manager
126// =============================================================================
127
128/// The main patchbay manager.
129///
130/// Coordinates all control and automation components. Runs in a dedicated
131/// thread at a configurable update rate.
132pub struct PatchbayManager {
133    config: PatchbayConfig,
134    automata: HashMap<String, Box<dyn std::any::Any + Send>>,
135    automaton_states: HashMap<String, Box<dyn std::any::Any + Send>>,
136    servos: HashMap<String, BoxedServo>,
137    mappings: Vec<Mapping>,
138    command_queue: Arc<MpscQueue<SetParameter>>,
139    event_tx: Option<crossbeam_channel::Sender<PatchbayEvent>>,
140    time: f64,
141    stats: PatchbayStats,
142    running: Arc<AtomicBool>,
143    update_thread: Option<std::thread::JoinHandle<()>>,
144}
145
146impl PatchbayManager {
147    /// Create a new patchbay manager.
148    pub fn new(config: PatchbayConfig, command_queue: Arc<MpscQueue<SetParameter>>) -> Self {
149        Self {
150            config,
151            automata: HashMap::new(),
152            automaton_states: HashMap::new(),
153            servos: HashMap::new(),
154            mappings: Vec::new(),
155            command_queue,
156            event_tx: None,
157            time: 0.0,
158            stats: PatchbayStats::default(),
159            running: Arc::new(AtomicBool::new(false)),
160            update_thread: None,
161        }
162    }
163
164    /// Set the event notification channel.
165    pub fn with_event_channel(mut self, tx: crossbeam_channel::Sender<PatchbayEvent>) -> Self {
166        self.event_tx = Some(tx);
167        self
168    }
169
170    // =========================================================================
171    // Automaton management
172    // =========================================================================
173
174    /// Add an automaton.
175    ///
176    /// # Errors
177    ///
178    /// Returns `Err` if an automaton with the same ID already exists.
179    pub fn add_automaton<A: Automaton + 'static>(
180        &mut self,
181        id: impl Into<String>,
182        automaton: A,
183    ) -> Result<(), &'static str>
184    where
185        A::State: 'static,
186    {
187        let id = id.into();
188        if self.automata.contains_key(&id) {
189            return Err("Automaton with this ID already exists");
190        }
191
192        let state = automaton.initial_state();
193        self.automata.insert(
194            id.clone(),
195            Box::new(automaton) as Box<dyn std::any::Any + Send>,
196        );
197        self.automaton_states.insert(id, Box::new(state));
198
199        Ok(())
200    }
201
202    /// Add an LFO as an automaton.
203    ///
204    /// # Errors
205    ///
206    /// Returns `Err` if an automaton with the same ID already exists.
207    pub fn add_lfo(
208        &mut self,
209        id: impl Into<String>,
210        frequency: f64,
211        amplitude: f64,
212        offset: f64,
213        waveform: LfoWaveform,
214    ) -> Result<(), &'static str> {
215        let id_str = id.into();
216        let automaton = LfoAutomaton::new(&id_str, frequency, amplitude, offset, waveform);
217        self.add_automaton(id_str, automaton)
218    }
219
220    /// Add an envelope ADSR as an automaton.
221    ///
222    /// # Errors
223    ///
224    /// Returns `Err` if an automaton with the same ID already exists.
225    pub fn add_envelope(
226        &mut self,
227        id: impl Into<String>,
228        attack: f64,
229        decay: f64,
230        sustain: f64,
231        release: f64,
232    ) -> Result<(), &'static str> {
233        let id_str = id.into();
234        let automaton = EnvelopeAutomaton::adsr(&id_str, attack, decay, sustain, release);
235        self.add_automaton(id_str, automaton)
236    }
237
238    /// Add a sequencer as an automaton.
239    ///
240    /// # Errors
241    ///
242    /// Returns `Err` if an automaton with the same ID already exists.
243    pub fn add_sequencer(
244        &mut self,
245        id: impl Into<String>,
246        steps: Vec<Step>,
247    ) -> Result<(), &'static str> {
248        let id_str = id.into();
249        let automaton = SequencerAutomaton::new(&id_str, steps);
250        self.add_automaton(id_str, automaton)
251    }
252
253    /// Add a function-based automaton.
254    ///
255    /// # Errors
256    ///
257    /// Returns `Err` if an automaton with the same ID already exists.
258    pub fn add_function<F>(
259        &mut self,
260        id: impl Into<String>,
261        generator: F,
262    ) -> Result<(), &'static str>
263    where
264        F: Fn(f64) -> f64 + Send + Sync + 'static,
265    {
266        let id_str = id.into();
267        let automaton = FunctionAutomaton::new(&id_str, generator);
268        self.add_automaton(id_str, automaton)
269    }
270
271    /// Reset an automaton to its initial state.
272    ///
273    /// # Errors
274    ///
275    /// Returns `Err` if the automaton is not found or the type does not match.
276    pub fn reset_automaton<A: Automaton + 'static>(
277        &mut self,
278        id: &str,
279    ) -> Result<(), &'static str> {
280        let automaton = self
281            .automata
282            .get(id)
283            .and_then(|a| a.downcast_ref::<A>())
284            .ok_or("Automaton not found or type mismatch")?;
285        let state = automaton.initial_state();
286        self.automaton_states
287            .insert(id.to_string(), Box::new(state));
288        Ok(())
289    }
290
291    /// Remove an automaton by ID.
292    pub fn remove_automaton(&mut self, id: &str) -> bool {
293        self.automata.remove(id).is_some() && self.automaton_states.remove(id).is_some()
294    }
295
296    // =========================================================================
297    // Servo management
298    // =========================================================================
299
300    /// Add a servo connecting an automaton to a parameter.
301    ///
302    /// # Errors
303    ///
304    /// Returns `Err` if the referenced automaton is not found.
305    pub fn add_servo(
306        &mut self,
307        id: impl Into<String>,
308        automaton_id: impl Into<String>,
309        target_node: NodeId,
310        target_param: impl Into<String>,
311        _mapping: ParameterMapping,
312        _min: f64,
313        _max: f64,
314    ) -> Result<(), &'static str> {
315        let id_str = id.into();
316        let automaton_id_str = automaton_id.into();
317        let target_param_str = target_param.into();
318        let _automaton = self
319            .automata
320            .get(&automaton_id_str)
321            .ok_or("Automaton not found")?;
322
323        let servo = Box::new(TestServo {
324            id: id_str.clone(),
325            target_node,
326            target_param: target_param_str,
327            last_value: 0.0,
328        });
329
330        self.servos.insert(id_str, servo);
331
332        Ok(())
333    }
334
335    /// Add an LFO servo (convenience method).
336    ///
337    /// # Errors
338    ///
339    /// Returns `Err` if an automaton with that ID already exists.
340    pub fn add_lfo_servo(
341        &mut self,
342        id: impl Into<String>,
343        frequency: f64,
344        amplitude: f64,
345        offset: f64,
346        waveform: LfoWaveform,
347        target_node: NodeId,
348        target_param: impl Into<String>,
349        min: f64,
350        max: f64,
351    ) -> Result<(), &'static str> {
352        let id_str = id.into();
353        let automaton_id = format!("{}_auto", &id_str);
354        self.add_lfo(&automaton_id, frequency, amplitude, offset, waveform)?;
355        self.add_servo(
356            id_str,
357            automaton_id,
358            target_node,
359            target_param,
360            ParameterMapping::Linear,
361            min,
362            max,
363        )
364    }
365
366    /// Get a servo by ID.
367    pub fn get_servo(&self, id: &str) -> Option<&dyn AnyServo> {
368        self.servos.get(id).map(|b| b.as_ref())
369    }
370
371    /// Get a mutable servo by ID.
372    pub fn get_servo_mut(&mut self, id: &str) -> Option<&mut BoxedServo> {
373        self.servos.get_mut(id)
374    }
375
376    /// Remove a servo by ID.
377    pub fn remove_servo(&mut self, id: &str) -> bool {
378        self.servos.remove(id).is_some()
379    }
380
381    // =========================================================================
382    // Mapping management
383    // =========================================================================
384
385    /// Add an event mapping.
386    pub fn add_mapping(&mut self, mapping: Mapping) {
387        self.mappings.push(mapping);
388    }
389
390    /// Add a MIDI CC mapping (convenience method).
391    pub fn add_midi_mapping(
392        &mut self,
393        controller: u8,
394        channel: Option<u8>,
395        target_node: NodeId,
396        target_param: impl Into<String>,
397        min: f32,
398        max: f32,
399        transform: Transform,
400    ) {
401        let mapping = midi_cc(
402            controller,
403            channel,
404            target_node,
405            &target_param.into(),
406            min,
407            max,
408            transform,
409        );
410        self.add_mapping(mapping);
411    }
412
413    /// Add an OSC address mapping (convenience method).
414    pub fn add_osc_mapping(
415        &mut self,
416        address: &str,
417        target_node: NodeId,
418        target_param: impl Into<String>,
419        min: f32,
420        max: f32,
421        transform: Transform,
422    ) {
423        let mapping = osc_address(
424            address,
425            target_node,
426            &target_param.into(),
427            min,
428            max,
429            transform,
430        );
431        self.add_mapping(mapping);
432    }
433
434    /// Remove all mappings matching a given pattern.
435    ///
436    /// Returns the number of removed mappings.
437    pub fn remove_mappings(&mut self, pattern: &EventPattern) -> usize {
438        let before = self.mappings.len();
439        self.mappings.retain(|m| &m.pattern != pattern);
440        before - self.mappings.len()
441    }
442
443    /// Clear all mappings.
444    pub fn clear_mappings(&mut self) {
445        self.mappings.clear();
446    }
447
448    // =========================================================================
449    // Event handling
450    // =========================================================================
451
452    /// Handle an external event (MIDI/OSC).
453    pub fn handle_event(&mut self, event: ControlEvent) {
454        let mut commands = Vec::new();
455
456        for mapping in &self.mappings {
457            if let Some(cmd) = mapping.apply(&event) {
458                let value = cmd.value;
459                commands.push(cmd);
460
461                if self.config.log_events {
462                    self.emit_event(PatchbayEvent::MappingTriggered {
463                        pattern: format!("{:?}", mapping.pattern),
464                        target: format!(
465                            "{}:{}",
466                            mapping.target.node_id.0, mapping.target.param_name
467                        ),
468                        value,
469                    });
470                }
471            }
472        }
473
474        for cmd in commands {
475            let _ = self.command_queue.push(cmd.clone());
476            self.stats.commands_sent += 1;
477
478            if self.config.log_events {
479                self.emit_event(PatchbayEvent::CommandSent(cmd));
480            }
481        }
482    }
483
484    /// Handle a MIDI message (convenience method).
485    pub fn handle_midi(&mut self, channel: u8, controller: u8, value: u8) {
486        let event = ControlEvent::MidiControl {
487            channel,
488            controller,
489            value,
490            normalized: value as f32 / 127.0,
491        };
492        self.handle_event(event);
493    }
494
495    /// Handle an OSC message (convenience method).
496    pub fn handle_osc(&mut self, address: &str, args: Vec<f32>) {
497        let event = ControlEvent::Osc {
498            address: address.to_string(),
499            args,
500        };
501        self.handle_event(event);
502    }
503
504    fn emit_event(&self, event: PatchbayEvent) {
505        if let Some(tx) = &self.event_tx {
506            let _ = tx.send(event);
507        }
508    }
509
510    // =========================================================================
511    // Start and stop
512    // =========================================================================
513
514    /// Start the manager in a separate thread.
515    ///
516    /// # Errors
517    ///
518    /// Returns `Err` if the manager is already running.
519    pub fn start(&mut self) -> Result<(), &'static str> {
520        if self.running.load(Ordering::Relaxed) {
521            return Err("Already running");
522        }
523
524        self.running.store(true, Ordering::Relaxed);
525
526        let running = self.running.clone();
527        let update_interval = Duration::from_secs_f64(1.0 / self.config.update_rate_hz);
528        let collect_stats = self.config.collect_stats;
529
530        let automata = std::mem::take(&mut self.automata);
531        let mut automaton_states = std::mem::take(&mut self.automaton_states);
532        let mut servos = std::mem::take(&mut self.servos);
533        let command_queue = self.command_queue.clone();
534        let _event_tx = self.event_tx.clone();
535
536        self.update_thread = Some(std::thread::spawn(move || {
537            let mut last_time = Instant::now();
538            let mut stats = PatchbayStats::default();
539            let mut time = 0.0;
540
541            while running.load(Ordering::Relaxed) {
542                let frame_start = Instant::now();
543
544                let now = Instant::now();
545                let dt = now.duration_since(last_time).as_secs_f64();
546                last_time = now;
547                time += dt;
548
549                let mut commands = Vec::new();
550
551                for id in automata.keys() {
552                    if let Some(_state) = automaton_states.get_mut(id) {
553                        if let Some(servo) = servos.get_mut(id) {
554                            if let Some(cmd) = servo.update(time) {
555                                commands.push(cmd);
556                            }
557                        }
558                    }
559                }
560
561                for cmd in commands {
562                    let _ = command_queue.push(cmd.clone());
563                    stats.commands_sent += 1;
564                }
565
566                if collect_stats {
567                    stats.update(frame_start.elapsed());
568                }
569
570                let elapsed = frame_start.elapsed();
571                if elapsed < update_interval {
572                    std::thread::sleep(update_interval - elapsed);
573                }
574            }
575        }));
576
577        Ok(())
578    }
579
580    /// Stop the manager.
581    pub fn stop(&mut self) {
582        self.running.store(false, Ordering::Relaxed);
583
584        if let Some(thread) = self.update_thread.take() {
585            let _ = thread.join();
586        }
587    }
588
589    /// Return a reference to the runtime statistics.
590    pub fn stats(&self) -> &PatchbayStats {
591        &self.stats
592    }
593
594    /// Reset the runtime statistics.
595    pub fn reset_stats(&mut self) {
596        self.stats = PatchbayStats::default();
597    }
598
599    /// Return the current internal time in seconds.
600    pub fn current_time(&self) -> f64 {
601        self.time
602    }
603
604    /// Check whether the manager is running.
605    pub fn is_running(&self) -> bool {
606        self.running.load(Ordering::Relaxed)
607    }
608}
609
610impl Drop for PatchbayManager {
611    fn drop(&mut self) {
612        self.stop();
613    }
614}
615
616// =============================================================================
617// Helper types for testing
618// =============================================================================
619
620/// Stub servo used for testing.
621struct TestServo {
622    id: String,
623    target_node: NodeId,
624    target_param: String,
625    last_value: f64,
626}
627
628impl AnyServo for TestServo {
629    fn update(&mut self, time: f64) -> Option<SetParameter> {
630        let value = (time * 2.0).sin() * 0.5 + 0.5;
631
632        if (value - self.last_value).abs() > 0.01 {
633            self.last_value = value;
634            Some(SetParameter::new(
635                PortId::param(self.target_node, 0),
636                ParameterId::new(&self.target_param).unwrap(),
637                value as f32,
638                SignalSource::Manual,
639            ))
640        } else {
641            None
642        }
643    }
644
645    fn id(&self) -> &str {
646        &self.id
647    }
648
649    fn set_enabled(&mut self, _enabled: bool) {}
650}
651
652// =============================================================================
653// PatchbayManager builder
654// =============================================================================
655
656/// Builder for creating a [`PatchbayManager`] with a fluent API.
657pub struct PatchbayManagerBuilder {
658    config: PatchbayConfig,
659    command_queue: Option<Arc<MpscQueue<SetParameter>>>,
660    event_channel: Option<crossbeam_channel::Sender<PatchbayEvent>>,
661}
662
663impl PatchbayManagerBuilder {
664    /// Create a new builder with default configuration.
665    pub fn new() -> Self {
666        Self {
667            config: PatchbayConfig::default(),
668            command_queue: None,
669            event_channel: None,
670        }
671    }
672
673    /// Set the patchbay configuration.
674    pub fn with_config(mut self, config: PatchbayConfig) -> Self {
675        self.config = config;
676        self
677    }
678
679    /// Set the update rate in Hz.
680    pub fn with_update_rate(mut self, hz: f64) -> Self {
681        self.config.update_rate_hz = hz;
682        self
683    }
684
685    /// Set the command queue.
686    pub fn with_command_queue(mut self, queue: Arc<MpscQueue<SetParameter>>) -> Self {
687        self.command_queue = Some(queue);
688        self
689    }
690
691    /// Set the event notification channel.
692    pub fn with_event_channel(mut self, tx: crossbeam_channel::Sender<PatchbayEvent>) -> Self {
693        self.event_channel = Some(tx);
694        self.config.log_events = true;
695        self
696    }
697
698    /// Enable or disable statistics collection.
699    pub fn with_stats(mut self, enabled: bool) -> Self {
700        self.config.collect_stats = enabled;
701        self
702    }
703
704    /// Build the [`PatchbayManager`].
705    pub fn build(self) -> PatchbayManager {
706        let queue = self
707            .command_queue
708            .unwrap_or_else(|| Arc::new(MpscQueue::with_capacity(self.config.command_queue_size)));
709
710        let mut manager = PatchbayManager::new(self.config, queue);
711
712        if let Some(tx) = self.event_channel {
713            manager = manager.with_event_channel(tx);
714        }
715
716        manager
717    }
718}
719
720impl Default for PatchbayManagerBuilder {
721    fn default() -> Self {
722        Self::new()
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729    use std::thread;
730    use std::time::Duration;
731
732    #[test]
733    fn test_manager_creation() {
734        let queue = Arc::new(MpscQueue::with_capacity(1024));
735        let manager = PatchbayManager::new(PatchbayConfig::default(), queue);
736
737        assert_eq!(manager.automata.len(), 0);
738        assert_eq!(manager.mappings.len(), 0);
739        assert!(!manager.is_running());
740    }
741
742    #[test]
743    fn test_add_automaton() {
744        let queue = Arc::new(MpscQueue::with_capacity(1024));
745        let mut manager = PatchbayManager::new(PatchbayConfig::default(), queue);
746
747        let result = manager.add_lfo("test_lfo", 1.0, 0.5, 0.0, LfoWaveform::Sine);
748        assert!(result.is_ok());
749        assert_eq!(manager.automata.len(), 1);
750    }
751
752    #[test]
753    fn test_add_mapping() {
754        let queue = Arc::new(MpscQueue::with_capacity(1024));
755        let mut manager = PatchbayManager::new(PatchbayConfig::default(), queue);
756
757        manager.add_midi_mapping(7, None, NodeId(1), "volume", 0.0, 1.0, Transform::Linear);
758        assert_eq!(manager.mappings.len(), 1);
759    }
760
761    #[test]
762    fn test_handle_event() {
763        let queue = Arc::new(MpscQueue::with_capacity(1024));
764        let mut manager = PatchbayManager::new(PatchbayConfig::default(), queue.clone());
765
766        manager.add_midi_mapping(7, None, NodeId(1), "volume", 0.0, 1.0, Transform::Linear);
767
768        let event = ControlEvent::MidiControl {
769            channel: 1,
770            controller: 7,
771            value: 64,
772            normalized: 0.5,
773        };
774
775        manager.handle_event(event);
776    }
777
778    #[test]
779    fn test_start_stop() {
780        let queue = Arc::new(MpscQueue::with_capacity(1024));
781        let mut manager = PatchbayManager::new(PatchbayConfig::default(), queue);
782
783        let result = manager.start();
784        assert!(result.is_ok());
785        assert!(manager.is_running());
786
787        thread::sleep(Duration::from_millis(100));
788
789        manager.stop();
790        assert!(!manager.is_running());
791    }
792
793    #[test]
794    fn test_builder() {
795        let queue = Arc::new(MpscQueue::with_capacity(1024));
796
797        let manager = PatchbayManagerBuilder::new()
798            .with_update_rate(500.0)
799            .with_command_queue(queue)
800            .with_stats(true)
801            .build();
802
803        assert_eq!(manager.config.update_rate_hz, 500.0);
804        assert!(manager.config.collect_stats);
805    }
806}