Skip to main content

rill_patchbay/
engine.rs

1//! Control and automation subsystem.
2//!
3//! Provides event mapping (MIDI/OSC → parameters), automaton-based
4//! modulation (LFO, envelopes), and a two-thread model with lock-free
5//! queues for control → audio communication.
6
7use std::collections::HashMap;
8use std::fmt::Debug;
9use std::sync::Arc;
10use std::time::Duration;
11
12use rill_core::prelude::*;
13use rill_core::queues::telemetry::{Telemetry, CLOCK_TICK};
14use rill_core::queues::{SetParameter, SignalOrigin};
15use rill_core_actor::ActorRef;
16
17use crossbeam_channel::Receiver as CrossbeamReceiver;
18
19pub use crate::automaton::Range;
20use crate::automaton::{EnvelopeAutomaton, LfoAutomaton, LfoWaveform};
21use crate::automaton_task::spawn_automaton_task;
22use crate::port_combiner::{spawn_combiner, PortCombinerHandle};
23use crate::sequencer::{SequencerCommand, SequencerHandle, SnapshotSequencer};
24use crate::strategy::{ConflictStrategy, ControlStrategy, UiCommand};
25
26// =============================================================================
27// 1. Event patterns
28// =============================================================================
29
30/// A pattern for matching controller events.
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub enum EventPattern {
34    /// Any button.
35    AnyButton,
36    /// A button with a specific ID.
37    ButtonId(u32),
38
39    /// Any knob.
40    AnyKnob,
41    /// A knob with a specific ID.
42    KnobId(u32),
43
44    /// Any fader.
45    AnyFader,
46    /// A fader with a specific ID.
47    FaderId(u32),
48
49    /// Any MIDI message.
50    AnyMidi,
51    /// MIDI Control Change.
52    MidiControl {
53        /// MIDI channel (None = any channel).
54        channel: Option<u8>,
55        /// Controller number.
56        controller: u8,
57    },
58    /// MIDI Note.
59    MidiNote {
60        /// MIDI channel (None = any channel).
61        channel: Option<u8>,
62        /// Note number (None = any note).
63        note: Option<u8>,
64    },
65
66    /// Exact OSC address.
67    OscAddress(String),
68
69    /// OSC address pattern (substring match).
70    OscPattern(String),
71}
72
73impl EventPattern {
74    /// Check whether the given event matches this pattern.
75    pub fn matches(&self, event: &ControlEvent) -> bool {
76        match (self, event) {
77            (EventPattern::AnyButton, ControlEvent::Button { .. }) => true,
78            (EventPattern::ButtonId(id), ControlEvent::Button { id: eid, .. }) => *id == *eid,
79
80            (EventPattern::AnyKnob, ControlEvent::Knob { .. }) => true,
81            (EventPattern::KnobId(id), ControlEvent::Knob { id: eid, .. }) => *id == *eid,
82
83            (EventPattern::AnyFader, ControlEvent::Fader { .. }) => true,
84            (EventPattern::FaderId(id), ControlEvent::Fader { id: eid, .. }) => *id == *eid,
85
86            (
87                EventPattern::MidiControl {
88                    channel,
89                    controller,
90                },
91                ControlEvent::MidiControl {
92                    channel: ech,
93                    controller: ectr,
94                    ..
95                },
96            ) => (channel.is_none() || channel.unwrap() == *ech) && *controller == *ectr,
97
98            (EventPattern::OscAddress(addr), ControlEvent::Osc { address, .. }) => addr == address,
99
100            (EventPattern::OscPattern(pat), ControlEvent::Osc { address, .. }) => {
101                address.contains(pat)
102            }
103
104            _ => false,
105        }
106    }
107}
108
109// =============================================================================
110// 2. Event types
111// =============================================================================
112
113/// A controller event from hardware or protocol input.
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115#[derive(Debug, Clone, PartialEq)]
116pub enum ControlEvent {
117    /// Button press/release.
118    Button {
119        /// Button identifier.
120        id: u32,
121        /// Whether the button is pressed.
122        pressed: bool,
123    },
124
125    /// Rotary knob / encoder.
126    Knob {
127        /// Knob identifier.
128        id: u32,
129        /// Raw value.
130        value: f32,
131        /// Normalised value (0.0–1.0).
132        normalized: f32,
133    },
134
135    /// Linear fader.
136    Fader {
137        /// Fader identifier.
138        id: u32,
139        /// Raw value.
140        value: f32,
141        /// Normalised value (0.0–1.0).
142        normalized: f32,
143    },
144
145    /// MIDI Control Change.
146    MidiControl {
147        /// MIDI channel (0–15).
148        channel: u8,
149        /// Controller number (0–127).
150        controller: u8,
151        /// Raw controller value (0–127).
152        value: u8,
153        /// Normalised value (0.0–1.0).
154        normalized: f32,
155    },
156
157    /// MIDI Note.
158    MidiNote {
159        /// MIDI channel (0–15).
160        channel: u8,
161        /// Note number (0–127).
162        note: u8,
163        /// Velocity (0–127).
164        velocity: u8,
165        /// Whether the note is on (true) or off (false).
166        on: bool,
167    },
168
169    /// OSC message.
170    Osc {
171        /// OSC address pattern (e.g. `/filter/cutoff`).
172        address: String,
173        /// Message arguments.
174        args: Vec<f32>,
175    },
176}
177
178impl ControlEvent {
179    /// Return the normalised value (0.0–1.0) if applicable.
180    pub fn normalized_value(&self) -> Option<f32> {
181        match self {
182            ControlEvent::Knob { normalized, .. } => Some(*normalized),
183            ControlEvent::Fader { normalized, .. } => Some(*normalized),
184            ControlEvent::MidiControl { normalized, .. } => Some(*normalized),
185            ControlEvent::Button { pressed, .. } => Some(if *pressed { 1.0 } else { 0.0 }),
186            _ => None,
187        }
188    }
189
190    /// Return the controller element ID, if any.
191    pub fn id(&self) -> Option<u32> {
192        match self {
193            ControlEvent::Button { id, .. } => Some(*id),
194            ControlEvent::Knob { id, .. } => Some(*id),
195            ControlEvent::Fader { id, .. } => Some(*id),
196            _ => None,
197        }
198    }
199}
200
201// =============================================================================
202// 2b. OSC Surface — OSC → EventPattern bridge
203// =============================================================================
204
205/// Maps an OSC address pattern to an internal [`EventPattern`].
206///
207/// One patchbay configuration can have a single canonical surface.
208/// For alternate MIDI layouts, use separate `mappings` slices with
209/// different `EventPattern::MidiControl` entries.
210#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211#[derive(Debug, Clone)]
212pub struct OscSurfaceEntry {
213    /// OSC address pattern, e.g. `"/delay/time"`.
214    pub osc_path: String,
215
216    /// Abstract controller identifier that `mappings` expect.
217    pub event_pattern: EventPattern,
218
219    /// Optional human-readable label (ignored by the engine).
220    #[cfg_attr(
221        feature = "serde",
222        serde(default, skip_serializing_if = "Option::is_none")
223    )]
224    pub label: Option<String>,
225}
226
227/// A list of [`OscSurfaceEntry`] entries.
228pub type OscSurface = Vec<OscSurfaceEntry>;
229
230// =============================================================================
231// 3. Value transforms
232// =============================================================================
233
234/// Type of value transformation.
235#[derive(Clone)]
236pub enum Transform {
237    /// Linear: out = min + value * (max - min).
238    Linear,
239
240    /// Exponential: out = min + value² * (max - min).
241    Exponential,
242
243    /// Logarithmic: out = min + log₁₀(1 + value * 9) / log₁₀(10) * (max - min).
244    Logarithmic,
245
246    /// Inverted: out = max - value * (max - min).
247    Inverted,
248
249    /// Custom user-defined function.
250    Custom(Arc<dyn Fn(f32) -> f32 + Send + Sync>),
251}
252
253impl Debug for Transform {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        match self {
256            Transform::Linear => write!(f, "Linear"),
257            Transform::Exponential => write!(f, "Exponential"),
258            Transform::Logarithmic => write!(f, "Logarithmic"),
259            Transform::Inverted => write!(f, "Inverted"),
260            Transform::Custom(_) => write!(f, "Custom"),
261        }
262    }
263}
264
265impl Transform {
266    /// Apply the transform to a normalised value (0–1).
267    pub fn apply(&self, value: f32, min: f32, max: f32) -> f32 {
268        let range = max - min;
269        let normalized = value.clamp(0.0, 1.0);
270
271        let mapped = match self {
272            Transform::Linear => min + normalized * range,
273            Transform::Exponential => min + normalized * normalized * range,
274            Transform::Logarithmic => min + (1.0 + normalized * 9.0).log10() * range,
275            Transform::Inverted => max - normalized * range,
276            Transform::Custom(f) => min + f(normalized) * range,
277        };
278
279        mapped.clamp(min, max)
280    }
281}
282
283// =============================================================================
284// 4. Event mapping
285// =============================================================================
286
287/// A target parameter on a graph node.
288#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
289#[derive(Debug, Clone)]
290pub struct Target {
291    /// Node ID in the signal graph.
292    pub node_id: NodeId,
293    /// Parameter name.
294    pub param_name: String,
295    /// Minimum value.
296    pub min: f32,
297    /// Maximum value.
298    pub max: f32,
299}
300
301/// A mapping from an event pattern to a parameter target.
302#[derive(Debug, Clone)]
303pub struct Mapping {
304    /// Event pattern to match.
305    pub pattern: EventPattern,
306    /// Target parameter.
307    pub target: Target,
308    /// Value transformation.
309    pub transform: Transform,
310    /// Human-readable name (for debugging).
311    pub name: String,
312    /// Whether this mapping is active.
313    pub enabled: bool,
314}
315
316impl Mapping {
317    /// Create a new mapping.
318    pub fn new(pattern: EventPattern, target: Target, transform: Transform) -> Self {
319        let name = format!("{:?} -> {}", pattern, target.param_name);
320        Self {
321            pattern,
322            target,
323            transform,
324            name,
325            enabled: true,
326        }
327    }
328
329    /// Check whether an event matches this mapping's pattern.
330    pub fn matches(&self, event: &ControlEvent) -> bool {
331        self.enabled && self.pattern.matches(event)
332    }
333
334    /// Apply an event and produce a parameter command, if it matches.
335    pub fn apply(&self, event: &ControlEvent) -> Option<SetParameter> {
336        if !self.matches(event) {
337            return None;
338        }
339
340        event.normalized_value().map(|norm| {
341            let value = self.transform.apply(norm, self.target.min, self.target.max);
342            let pid = ParameterId::new(&self.target.param_name).unwrap();
343            SetParameter::new(
344                PortId::param(self.target.node_id, 0),
345                pid,
346                ParamValue::Float(value),
347                SignalOrigin::External(self.name.clone()),
348            )
349        })
350    }
351}
352
353// =============================================================================
354// 5. Automaton core trait
355// =============================================================================
356
357/// Time type used by automata.
358pub type Time = f64;
359
360/// Marker for automata that need no external action.
361#[derive(Debug, Clone, Default)]
362pub struct NoAction;
363
364/// Core trait for all automata.
365///
366/// An automaton is a stateful function generator. Each call to [`step`](Self::step)
367/// takes the current time, an action, and the current state, and returns a
368/// new state together with an optional output value.  Automata are `Send`
369/// and run on the control thread (soft RT).
370pub trait Automaton: Send + Sync + Debug {
371    /// State type.
372    type State: Clone + Send + Sync + 'static + Debug;
373
374    /// Action type (a pure function applied to the state).
375    type Action: Debug + Clone + Send + Sync + Default + 'static;
376
377    /// Advance the automaton by one time step.
378    ///
379    /// # Arguments
380    /// * `time` — current time
381    /// * `action` — action to apply
382    /// * `state` — current state
383    ///
384    /// Returns `(new_state, optional_output_value)`.
385    fn step(
386        &self,
387        time: Time,
388        action: &Self::Action,
389        state: &Self::State,
390    ) -> (Self::State, Option<f64>);
391
392    /// Return the initial state.
393    fn initial_state(&self) -> Self::State;
394
395    /// Automaton name.
396    fn name(&self) -> &str;
397
398    /// Extract the output value from the state.
399    fn extract_value(&self, state: &Self::State) -> f64;
400
401    /// Reset the automaton to its initial state.
402    fn reset(&self) -> Self::State {
403        self.initial_state()
404    }
405}
406
407// =============================================================================
408// 6. Servo — automaton-to-parameter bridge
409// =============================================================================
410
411/// Mapping type for a servo's output value.
412#[derive(Clone)]
413pub enum ParameterMapping {
414    /// Linear: `min + value * (max - min)`.
415    Linear,
416    /// Exponential: `min + value^exp * (max - min)`.
417    Exponential,
418    /// Logarithmic: `min + log(1 + value * (e - 1)) / log(e) * (max - min)`.
419    Logarithmic,
420    /// Inverted linear: `max - value * (max - min)`.
421    Inverted,
422    /// Custom mapping function.
423    Custom(Arc<dyn Fn(f64) -> f64 + Send + Sync>),
424}
425
426impl std::fmt::Debug for ParameterMapping {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        match self {
429            ParameterMapping::Linear => write!(f, "Linear"),
430            ParameterMapping::Exponential => write!(f, "Exponential"),
431            ParameterMapping::Logarithmic => write!(f, "Logarithmic"),
432            ParameterMapping::Inverted => write!(f, "Inverted"),
433            ParameterMapping::Custom(_) => write!(f, "Custom(<fn>)"),
434        }
435    }
436}
437
438impl ParameterMapping {
439    /// Apply the mapping to a raw automaton value.
440    pub fn apply(&self, raw: f64) -> f64 {
441        match self {
442            ParameterMapping::Linear => raw,
443            ParameterMapping::Exponential => raw * raw,
444            ParameterMapping::Logarithmic => (1.0 + raw * 9.0).log10(),
445            ParameterMapping::Inverted => 1.0 - raw,
446            ParameterMapping::Custom(f) => f(raw),
447        }
448    }
449}
450
451/// A servo bridges an automaton to a graph-node parameter.
452pub struct Servo<A: Automaton> {
453    id: String,
454    automaton: A,
455    state: A::State,
456    target_node: NodeId,
457    target_param: String,
458    mapping: ParameterMapping,
459    min: f64,
460    max: f64,
461    last_value: f64,
462    enabled: bool,
463    last_time: Time,
464}
465
466impl<A: Automaton> Servo<A> {
467    /// Create a new servo.
468    pub fn new(
469        id: impl Into<String>,
470        automaton: A,
471        target_node: NodeId,
472        target_param: impl Into<String>,
473        mapping: ParameterMapping,
474        min: f64,
475        max: f64,
476    ) -> Self {
477        let state = automaton.initial_state();
478        Self {
479            id: id.into(),
480            automaton,
481            state,
482            target_node,
483            target_param: target_param.into(),
484            mapping,
485            min,
486            max,
487            last_value: 0.0,
488            enabled: true,
489            last_time: 0.0,
490        }
491    }
492
493    /// Advance the servo and return a parameter command if the value changed.
494    pub fn update(&mut self, time: Time) -> Option<SetParameter> {
495        if !self.enabled {
496            return None;
497        }
498
499        let (new_state, value_opt) = self
500            .automaton
501            .step(time, &A::Action::default(), &self.state);
502        self.state = new_state;
503
504        if let Some(raw_value) = value_opt {
505            let mapped = self.mapping.apply(raw_value);
506            let clamped = mapped.clamp(self.min, self.max);
507
508            if (clamped - self.last_value).abs() > 1e-6 {
509                self.last_value = clamped;
510                self.last_time = time;
511
512                let pid = ParameterId::new(&self.target_param).unwrap();
513                return Some(SetParameter::new(
514                    PortId::param(self.target_node, 0),
515                    pid,
516                    ParamValue::Float(clamped as f32),
517                    SignalOrigin::Automaton(self.id.clone()),
518                ));
519            }
520        }
521
522        None
523    }
524
525    /// Enable or disable this servo.
526    pub fn set_enabled(&mut self, enabled: bool) {
527        self.enabled = enabled;
528    }
529
530    /// Return the servo's unique identifier.
531    pub fn id(&self) -> &str {
532        &self.id
533    }
534}
535
536/// Type-erased boxed servo.
537pub type BoxedServo = Box<dyn AnyServo>;
538
539/// Trait for type-erased servo operations.
540pub trait AnyServo: Send + Sync {
541    /// Update the servo and return a parameter command if the value changed.
542    fn update(&mut self, time: Time) -> Option<SetParameter>;
543    /// Return the servo's unique identifier.
544    fn id(&self) -> &str;
545    /// Enable or disable the servo.
546    fn set_enabled(&mut self, enabled: bool);
547}
548
549impl<A: Automaton + 'static> AnyServo for Servo<A> {
550    fn update(&mut self, time: Time) -> Option<SetParameter> {
551        Servo::update(self, time)
552    }
553
554    fn id(&self) -> &str {
555        &self.id
556    }
557
558    fn set_enabled(&mut self, enabled: bool) {
559        self.enabled = enabled;
560    }
561}
562
563// =============================================================================
564// 8. Main patchbay controller
565// =============================================================================
566
567/// The central patchbay controller.
568///
569/// Operates on the **control thread** (soft RT) and sends parameter commands
570/// to the audio thread via [`MpscQueue<SetParameter>`](rill_core::queues::MpscQueue).
571///
572/// ## Operation modes
573///
574/// - **Sync** (legacy): [`update(dt)`](Self::update) walks all servos sequentially.
575///   Does not require tokio.
576/// - **Async** (recommended): automata run as tokio tasks through
577///   [`add_automaton_task()`](Self::add_automaton_task). Requires an active
578///   tokio runtime.
579pub struct Patchbay {
580    mappings: Vec<Mapping>,
581    servos: HashMap<String, BoxedServo>,
582    port_combiners: HashMap<String, PortCombinerHandle>,
583    automaton_handles: HashMap<String, tokio::task::JoinHandle<()>>,
584    sequencer_handle: Option<SequencerHandle>,
585    sequencer_task: Option<tokio::task::JoinHandle<()>>,
586    command_queue: ActorRef<SetParameter>,
587    time: Time,
588}
589
590impl Patchbay {
591    /// Create a new patchbay controller.
592    ///
593    /// Async methods (green threads, PortCombiner) require an active
594    /// tokio runtime and will panic otherwise. Synchronous methods
595    /// (servo, mapping, update) work without tokio.
596    pub fn new(command_queue: ActorRef<SetParameter>) -> Self {
597        Self {
598            mappings: Vec::new(),
599            servos: HashMap::new(),
600            port_combiners: HashMap::new(),
601            automaton_handles: HashMap::new(),
602            sequencer_handle: None,
603            sequencer_task: None,
604            command_queue,
605            time: 0.0,
606        }
607    }
608
609    /// Add an event mapping.
610    pub fn add_mapping(&mut self, mapping: Mapping) {
611        self.mappings.push(mapping);
612    }
613
614    /// Add a pre-constructed boxed servo.
615    ///
616    /// Useful for automaton types not covered by `add_lfo` / `add_envelope`
617    /// (e.g. sequencers, named functions).
618    pub fn add_boxed_servo(&mut self, id: String, servo: BoxedServo) {
619        self.servos.insert(id, servo);
620    }
621
622    /// Add a mapping from string descriptions (convenient for scripting).
623    ///
624    /// # Errors
625    ///
626    /// Returns `Err` if the pattern string is malformed.
627    pub fn add_mapping_str(
628        &mut self,
629        pattern: &str,
630        target_node: NodeId,
631        target_param: &str,
632        min: f32,
633        max: f32,
634        transform: Transform,
635    ) -> Result<(), &'static str> {
636        let pattern = match pattern {
637            p if p.starts_with("button:") => {
638                let id = p[7..].parse().map_err(|_| "Invalid button ID")?;
639                EventPattern::ButtonId(id)
640            }
641            p if p.starts_with("knob:") => {
642                let id = p[5..].parse().map_err(|_| "Invalid knob ID")?;
643                EventPattern::KnobId(id)
644            }
645            p if p.starts_with("fader:") => {
646                let id = p[6..].parse().map_err(|_| "Invalid fader ID")?;
647                EventPattern::FaderId(id)
648            }
649            p if p.starts_with("midi:") => {
650                let parts: Vec<&str> = p[5..].split(':').collect();
651                if parts.len() == 2 {
652                    let channel = parts[0].parse().ok();
653                    let controller = parts[1].parse().map_err(|_| "Invalid controller")?;
654                    EventPattern::MidiControl {
655                        channel,
656                        controller,
657                    }
658                } else {
659                    EventPattern::AnyMidi
660                }
661            }
662            p if p.starts_with("osc:") => EventPattern::OscAddress(p[4..].to_string()),
663            _ => return Err("Unknown pattern"),
664        };
665
666        let target = Target {
667            node_id: target_node,
668            param_name: target_param.to_string(),
669            min,
670            max,
671        };
672
673        self.add_mapping(Mapping::new(pattern, target, transform));
674        Ok(())
675    }
676
677    /// Add a servo (automaton → parameter bridge).
678    pub fn add_servo<A: Automaton + 'static>(&mut self, servo: Servo<A>) {
679        self.servos.insert(servo.id().to_string(), Box::new(servo));
680    }
681
682    /// Add an LFO as a servo.
683    pub fn add_lfo(
684        &mut self,
685        id: &str,
686        frequency: f64,
687        amplitude: f64,
688        offset: f64,
689        waveform: LfoWaveform,
690        target_node: NodeId,
691        target_param: &str,
692        min: f64,
693        max: f64,
694    ) {
695        let automaton = LfoAutomaton::new(id, frequency, amplitude, offset, waveform);
696        let servo = Servo::new(
697            id,
698            automaton,
699            target_node,
700            target_param,
701            ParameterMapping::Linear,
702            min,
703            max,
704        );
705        self.add_servo(servo);
706    }
707
708    /// Add an envelope ADSR as a servo.
709    pub fn add_envelope(
710        &mut self,
711        id: &str,
712        attack: f64,
713        decay: f64,
714        sustain: f64,
715        release: f64,
716        target_node: NodeId,
717        target_param: &str,
718        min: f64,
719        max: f64,
720    ) {
721        let automaton = EnvelopeAutomaton::adsr(id, attack, decay, sustain, release);
722        let servo = Servo::new(
723            id,
724            automaton,
725            target_node,
726            target_param,
727            ParameterMapping::Linear,
728            min,
729            max,
730        );
731        self.add_servo(servo);
732    }
733
734    /// Add an automaton as a green thread (tokio task).
735    ///
736    /// Requires an active tokio runtime. Ports with async automata receive
737    /// a `PortCombiner` that resolves UI ↔ automaton conflicts.
738    ///
739    /// # Arguments
740    ///
741    /// * `id` — unique identifier
742    /// * `automaton` — the automaton implementation
743    /// * `interval` — update interval (e.g. 10 ms = 100 Hz)
744    /// * `target` — `(node_id, param_name)`
745    /// * `range` — `(min, max)` parameter range
746    /// * `control` — control strategy
747    /// * `conflict` — conflict resolution strategy
748    pub fn add_automaton_task<A: Automaton + 'static>(
749        &mut self,
750        id: &str,
751        automaton: A,
752        interval: Duration,
753        target: (NodeId, String),
754        range: (f64, f64),
755        control: ControlStrategy,
756        conflict: ConflictStrategy,
757    ) {
758        let key = target_key(target.0, &target.1);
759
760        let combiner = spawn_combiner(target, range, control, conflict, self.command_queue.clone());
761
762        let task = spawn_automaton_task(
763            automaton,
764            interval,
765            combiner.automaton_tx.clone(),
766            combiner.cancel_rx(),
767        );
768
769        self.port_combiners.insert(key, combiner);
770        self.automaton_handles.insert(id.to_string(), task);
771    }
772
773    /// Add an LFO as an async automaton task.
774    pub fn add_lfo_task(
775        &mut self,
776        id: &str,
777        frequency: f64,
778        amplitude: f64,
779        offset: f64,
780        waveform: LfoWaveform,
781        interval: Duration,
782        target: (NodeId, String),
783        range: (f64, f64),
784        control: ControlStrategy,
785        conflict: ConflictStrategy,
786    ) {
787        let automaton = LfoAutomaton::new(id, frequency, amplitude, offset, waveform);
788        self.add_automaton_task(
789            format!("{}_auto", id).as_str(),
790            automaton,
791            interval,
792            target,
793            range,
794            control,
795            conflict,
796        );
797    }
798
799    /// Add an envelope ADSR as an async automaton task.
800    pub fn add_envelope_task(
801        &mut self,
802        id: &str,
803        attack: f64,
804        decay: f64,
805        sustain: f64,
806        release: f64,
807        interval: Duration,
808        target: (NodeId, String),
809        range: (f64, f64),
810        control: ControlStrategy,
811        conflict: ConflictStrategy,
812    ) {
813        let automaton = EnvelopeAutomaton::adsr(id, attack, decay, sustain, release);
814        self.add_automaton_task(
815            format!("{}_auto", id).as_str(),
816            automaton,
817            interval,
818            target,
819            range,
820            control,
821            conflict,
822        );
823    }
824
825    /// Attach a parameter-lock sequencer driven by audio-thread clock ticks.
826    ///
827    /// Spawns a blocking tokio task that reads `CLOCK_TICK` telemetry and
828    /// pushes returned parameter commands to the queue.
829    ///
830    /// Returns a [`SequencerHandle`] for external control.
831    ///
832    /// # Panics
833    ///
834    /// Panics if a sequencer is already attached (call `detach_sequencer()` first).
835    pub fn attach_sequencer(
836        &mut self,
837        tel_rx: CrossbeamReceiver<Telemetry>,
838        sequencer: SnapshotSequencer,
839    ) -> SequencerHandle {
840        assert!(
841            self.sequencer_task.is_none(),
842            "sequencer already attached — detach first"
843        );
844
845        let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<SequencerCommand>();
846        let queue = self.command_queue.clone();
847
848        let task = tokio::task::spawn_blocking(move || {
849            let mut seq = sequencer;
850
851            loop {
852                loop {
853                    match cmd_rx.try_recv() {
854                        Ok(SequencerCommand::Start) => seq.start(),
855                        Ok(SequencerCommand::Stop) => seq.stop(),
856                        Ok(SequencerCommand::Reset { sample_pos }) => seq.reset(sample_pos),
857                        Ok(SequencerCommand::SetPattern(id)) => seq.set_active_pattern(&id),
858                        Err(crossbeam_channel::TryRecvError::Empty) => break,
859                        Err(crossbeam_channel::TryRecvError::Disconnected) => return,
860                    }
861                }
862
863                match tel_rx.recv() {
864                    Ok(Telemetry::Event { kind, data, .. })
865                        if kind == CLOCK_TICK && data.len() >= 3 =>
866                    {
867                        let sample_pos = data[0] as u64;
868                        let sample_rate = data[1];
869                        let tempo = data[2];
870
871                        let beat_pos = data.get(3).copied().unwrap_or(0.0);
872                        let new_beat = data.get(4).copied().unwrap_or(0.0) > 0.5;
873                        let new_bar = data.get(5).copied().unwrap_or(0.0) > 0.5;
874
875                        let cmds = seq.tick_ext(
876                            sample_pos,
877                            sample_rate,
878                            tempo,
879                            beat_pos,
880                            new_beat,
881                            new_bar,
882                        );
883                        for cmd in cmds {
884                            queue.send(cmd);
885                        }
886                    }
887                    Err(_) => return,
888                    _ => {}
889                }
890            }
891        });
892
893        let handle = SequencerHandle::new(cmd_tx);
894        self.sequencer_handle = Some(handle.clone());
895        self.sequencer_task = Some(task);
896
897        handle
898    }
899
900    /// Detach the sequencer: abort its task and drop the handle.
901    pub fn detach_sequencer(&mut self) {
902        if let Some(task) = self.sequencer_task.take() {
903            task.abort();
904        }
905        self.sequencer_handle = None;
906    }
907
908    /// Get a reference to the sequencer handle, if attached.
909    pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
910        self.sequencer_handle.as_ref()
911    }
912
913    /// Stop all async automata and the sequencer.
914    pub fn stop_all(&mut self) {
915        for combiner in self.port_combiners.values() {
916            combiner.stop();
917        }
918        self.port_combiners.clear();
919        self.automaton_handles.clear();
920        self.detach_sequencer();
921    }
922
923    // ── Convenience aliases (forwarded from removed PatchbayEngine) ────
924
925    /// Add an automaton as a green thread with PortCombiner.
926    pub fn add_automaton<A: Automaton + 'static>(
927        &mut self,
928        id: &str,
929        automaton: A,
930        interval: Duration,
931        target: (NodeId, String),
932        range: (f64, f64),
933        control: ControlStrategy,
934        conflict: ConflictStrategy,
935    ) {
936        self.add_automaton_task(id, automaton, interval, target, range, control, conflict);
937    }
938
939    /// Add an LFO as a green thread (async automaton + PortCombiner).
940    pub fn add_lfo_async(
941        &mut self,
942        id: &str,
943        frequency: f64,
944        amplitude: f64,
945        offset: f64,
946        waveform: LfoWaveform,
947        interval: Duration,
948        target: (NodeId, String),
949        range: (f64, f64),
950        control: ControlStrategy,
951        conflict: ConflictStrategy,
952    ) {
953        self.add_lfo_task(
954            id, frequency, amplitude, offset, waveform, interval, target, range, control, conflict,
955        );
956    }
957
958    /// Add an envelope as a green thread (async automaton + PortCombiner).
959    pub fn add_envelope_async(
960        &mut self,
961        id: &str,
962        attack: f64,
963        decay: f64,
964        sustain: f64,
965        release: f64,
966        interval: Duration,
967        target: (NodeId, String),
968        range: (f64, f64),
969        control: ControlStrategy,
970        conflict: ConflictStrategy,
971    ) {
972        self.add_envelope_task(
973            id, attack, decay, sustain, release, interval, target, range, control, conflict,
974        );
975    }
976
977    /// Handle an external event (MIDI/OSC).
978    ///
979    /// If a `PortCombiner` exists for the target port the event is routed
980    /// there for conflict resolution; otherwise it is pushed directly to
981    /// the command queue.
982    pub fn handle_event(&mut self, event: ControlEvent) {
983        for mapping in &self.mappings {
984            if let Some(cmd) = mapping.apply(&event) {
985                let key = target_key(cmd.port.node_id(), cmd.parameter.as_ref());
986                if let Some(combiner) = self.port_combiners.get(&key) {
987                    let _ = combiner
988                        .ui_tx
989                        .send(UiCommand::SetValue(cmd.value.as_f32().unwrap_or(0.0) as f64));
990                } else {
991                    self.command_queue.send(cmd);
992                }
993            }
994        }
995    }
996
997    /// Update synchronous servos.
998    ///
999    /// This method is deprecated. For new projects use `add_automaton_task()`
1000    /// with green threads.
1001    pub fn update(&mut self, dt: f32) {
1002        self.time += dt as f64;
1003
1004        for servo in self.servos.values_mut() {
1005            if let Some(cmd) = servo.update(self.time) {
1006                self.command_queue.send(cmd);
1007            }
1008        }
1009    }
1010
1011    /// Get a combiner by key (format: `"node_id:param_name"`).
1012    pub fn get_combiner(&self, key: &str) -> Option<&PortCombinerHandle> {
1013        self.port_combiners.get(key)
1014    }
1015
1016    /// Return all mappings.
1017    pub fn mappings(&self) -> &[Mapping] {
1018        &self.mappings
1019    }
1020
1021    /// Get a servo by ID.
1022    pub fn get_servo(&self, id: &str) -> Option<&dyn AnyServo> {
1023        self.servos.get(id).map(|b| b.as_ref())
1024    }
1025
1026    /// Get a mutable servo by ID.
1027    pub fn get_servo_mut(&mut self, id: &str) -> Option<&mut BoxedServo> {
1028        self.servos.get_mut(id)
1029    }
1030
1031    /// Remove a servo by ID.
1032    pub fn remove_servo(&mut self, id: &str) -> bool {
1033        self.servos.remove(id).is_some()
1034    }
1035
1036    /// Clear all mappings, servos, and async automata.
1037    pub fn clear(&mut self) {
1038        self.mappings.clear();
1039        self.servos.clear();
1040        self.stop_all();
1041    }
1042
1043    /// Reset the internal clock to zero.
1044    pub fn reset_time(&mut self) {
1045        self.time = 0.0;
1046    }
1047
1048    /// Current internal time in seconds.
1049    pub fn current_time(&self) -> Time {
1050        self.time
1051    }
1052}
1053
1054impl Drop for Patchbay {
1055    fn drop(&mut self) {
1056        self.stop_all();
1057    }
1058}
1059
1060// =============================================================================
1061// 9. Helper functions for creating mappings
1062// =============================================================================
1063
1064/// Create a MIDI CC → parameter mapping.
1065pub fn midi_cc(
1066    controller: u8,
1067    channel: Option<u8>,
1068    target_node: NodeId,
1069    target_param: &str,
1070    min: f32,
1071    max: f32,
1072    transform: Transform,
1073) -> Mapping {
1074    let pattern = EventPattern::MidiControl {
1075        channel,
1076        controller,
1077    };
1078    let target = Target {
1079        node_id: target_node,
1080        param_name: target_param.to_string(),
1081        min,
1082        max,
1083    };
1084    Mapping::new(pattern, target, transform)
1085}
1086
1087/// Create an OSC address → parameter mapping.
1088pub fn osc_address(
1089    address: &str,
1090    target_node: NodeId,
1091    target_param: &str,
1092    min: f32,
1093    max: f32,
1094    transform: Transform,
1095) -> Mapping {
1096    let pattern = EventPattern::OscAddress(address.to_string());
1097    let target = Target {
1098        node_id: target_node,
1099        param_name: target_param.to_string(),
1100        min,
1101        max,
1102    };
1103    Mapping::new(pattern, target, transform)
1104}
1105
1106// =============================================================================
1107// 9b. PortCombiner key helper
1108// =============================================================================
1109
1110fn target_key(node_id: NodeId, param_name: &str) -> String {
1111    format!("{}:{}", node_id.inner(), param_name)
1112}
1113
1114// =============================================================================
1115// 10. Tests
1116// =============================================================================
1117
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121
1122    #[test]
1123    fn test_midi_mapping() {
1124        let node = NodeId(1);
1125        let mapping = midi_cc(7, Some(1), node, "volume", 0.0, 1.0, Transform::Linear);
1126
1127        let event = ControlEvent::MidiControl {
1128            channel: 1,
1129            controller: 7,
1130            value: 64,
1131            normalized: 0.5,
1132        };
1133
1134        assert!(mapping.matches(&event));
1135
1136        let cmd = mapping.apply(&event).unwrap();
1137        assert_eq!(cmd.port.node_id(), node);
1138        assert_eq!(cmd.parameter.as_ref(), "volume");
1139        assert!((cmd.value.as_f32().unwrap() - 0.5).abs() < 1e-6);
1140    }
1141
1142    #[test]
1143    fn test_lfo_servo() {
1144        let node = NodeId(1);
1145        let (actor_ref, _mailbox) = ActorRef::new_pair();
1146        let mut control = Patchbay::new(actor_ref);
1147
1148        control.add_lfo(
1149            "test_lfo",
1150            1.0,
1151            0.5,
1152            0.0,
1153            LfoWaveform::Sine,
1154            node,
1155            "cutoff",
1156            100.0,
1157            1000.0,
1158        );
1159
1160        assert!(control.get_servo("test_lfo").is_some());
1161
1162        for _i in 0..10 {
1163            control.update(0.1);
1164        }
1165    }
1166
1167    #[test]
1168    fn test_envelope_servo() {
1169        let node = NodeId(1);
1170        let (actor_ref, _mailbox) = ActorRef::new_pair();
1171        let mut control = Patchbay::new(actor_ref);
1172
1173        control.add_envelope("test_env", 0.1, 0.2, 0.7, 0.3, node, "gain", 0.0, 1.0);
1174
1175        if let Some(_servo) = control.get_servo_mut("test_env") {}
1176
1177        control.update(0.05);
1178        control.update(0.05);
1179    }
1180}