Skip to main content

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