1use 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum EventPattern {
33 AnyButton,
35 ButtonId(u32),
37
38 AnyKnob,
40 KnobId(u32),
42
43 AnyFader,
45 FaderId(u32),
47
48 AnyMidi,
50 MidiControl {
52 channel: Option<u8>,
54 controller: u8,
56 },
57 MidiNote {
59 channel: Option<u8>,
61 note: Option<u8>,
63 },
64
65 OscAddress(String),
67
68 OscPattern(String),
70}
71
72impl EventPattern {
73 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114#[derive(Debug, Clone, PartialEq)]
115pub enum ControlEvent {
116 Button {
118 id: u32,
120 pressed: bool,
122 },
123
124 Knob {
126 id: u32,
128 value: f32,
130 normalized: f32,
132 },
133
134 Fader {
136 id: u32,
138 value: f32,
140 normalized: f32,
142 },
143
144 MidiControl {
146 channel: u8,
148 controller: u8,
150 value: u8,
152 normalized: f32,
154 },
155
156 MidiNote {
158 channel: u8,
160 note: u8,
162 velocity: u8,
164 on: bool,
166 },
167
168 Osc {
170 address: String,
172 args: Vec<f32>,
174 },
175}
176
177impl ControlEvent {
178 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 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210#[derive(Debug, Clone)]
211pub struct OscSurfaceEntry {
212 pub osc_path: String,
214
215 pub event_pattern: EventPattern,
217
218 #[cfg_attr(
220 feature = "serde",
221 serde(default, skip_serializing_if = "Option::is_none")
222 )]
223 pub label: Option<String>,
224}
225
226pub type OscSurface = Vec<OscSurfaceEntry>;
228
229#[derive(Clone)]
235pub enum Transform {
236 Linear,
238
239 Exponential,
241
242 Logarithmic,
244
245 Inverted,
247
248 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 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
288#[derive(Debug, Clone)]
289pub struct Target {
290 pub node_id: NodeId,
292 pub param_name: String,
294 pub min: f32,
296 pub max: f32,
298}
299
300#[derive(Debug, Clone)]
302pub struct Mapping {
303 pub pattern: EventPattern,
305 pub target: Target,
307 pub transform: Transform,
309 pub name: String,
311 pub enabled: bool,
313}
314
315impl Mapping {
316 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 pub fn matches(&self, event: &ControlEvent) -> bool {
330 self.enabled && self.pattern.matches(event)
331 }
332
333 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
352pub type Time = f64;
358
359#[derive(Debug, Clone, Default)]
361pub struct NoAction;
362
363pub trait Automaton: Send + Sync + Debug {
370 type State: Clone + Send + Sync + 'static + Debug;
372
373 type Action: Debug + Clone + Send + Sync + Default + 'static;
375
376 fn step(
385 &self,
386 time: Time,
387 action: &Self::Action,
388 state: &Self::State,
389 ) -> (Self::State, Option<f64>);
390
391 fn initial_state(&self) -> Self::State;
393
394 fn name(&self) -> &str;
396
397 fn extract_value(&self, state: &Self::State) -> f64;
399
400 fn reset(&self) -> Self::State {
402 self.initial_state()
403 }
404}
405
406#[derive(Clone)]
412pub enum ParameterMapping {
413 Linear,
415 Exponential,
417 Logarithmic,
419 Inverted,
421 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 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
450pub 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 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 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 pub fn set_enabled(&mut self, enabled: bool) {
526 self.enabled = enabled;
527 }
528
529 pub fn id(&self) -> &str {
531 &self.id
532 }
533}
534
535pub type BoxedServo = Box<dyn AnyServo>;
537
538pub trait AnyServo: Send + Sync {
540 fn update(&mut self, time: Time) -> Option<SetParameter>;
542 fn id(&self) -> &str;
544 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
562pub 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 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 pub fn add_mapping(&mut self, mapping: Mapping) {
606 self.mappings.push(mapping);
607 }
608
609 pub fn add_boxed_servo(&mut self, id: String, servo: BoxedServo) {
614 self.servos.insert(id, servo);
615 }
616
617 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 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 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 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 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 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 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 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 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 pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
905 self.sequencer_handle.as_ref()
906 }
907
908 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 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 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 pub fn get_combiner(&self, key: &str) -> Option<&PortCombinerHandle> {
952 self.port_combiners.get(key)
953 }
954
955 pub fn mappings(&self) -> &[Mapping] {
957 &self.mappings
958 }
959
960 pub fn get_servo(&self, id: &str) -> Option<&dyn AnyServo> {
962 self.servos.get(id).map(|b| b.as_ref())
963 }
964
965 pub fn get_servo_mut(&mut self, id: &str) -> Option<&mut BoxedServo> {
967 self.servos.get_mut(id)
968 }
969
970 pub fn remove_servo(&mut self, id: &str) -> bool {
972 self.servos.remove(id).is_some()
973 }
974
975 pub fn clear(&mut self) {
977 self.mappings.clear();
978 self.servos.clear();
979 self.stop_all();
980 }
981
982 pub fn reset_time(&mut self) {
984 self.time = 0.0;
985 }
986
987 pub fn current_time(&self) -> Time {
989 self.time
990 }
991}
992
993pub 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
1020pub 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
1039fn target_key(node_id: NodeId, param_name: &str) -> String {
1044 format!("{}:{}", node_id.inner(), param_name)
1045}
1046
1047#[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}