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