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;
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(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
220 pub label: Option<String>,
221}
222
223pub type OscSurface = Vec<OscSurfaceEntry>;
225
226#[derive(Clone)]
232pub enum Transform {
233 Linear,
235
236 Exponential,
238
239 Logarithmic,
241
242 Inverted,
244
245 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 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
285#[derive(Debug, Clone)]
286pub struct Target {
287 pub node_id: NodeId,
289 pub param_name: String,
291 pub min: f32,
293 pub max: f32,
295}
296
297#[derive(Debug, Clone)]
299pub struct Mapping {
300 pub pattern: EventPattern,
302 pub target: Target,
304 pub transform: Transform,
306 pub name: String,
308 pub enabled: bool,
310}
311
312impl Mapping {
313 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 pub fn matches(&self, event: &ControlEvent) -> bool {
327 self.enabled && self.pattern.matches(event)
328 }
329
330 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
347pub type Time = f64;
353
354#[derive(Debug, Clone, Default)]
356pub struct NoAction;
357
358pub trait Automaton: Send + Sync + Debug {
365 type State: Clone + Send + Sync + 'static + Debug;
367
368 type Action: Debug + Clone + Send + Sync + Default + 'static;
370
371 fn step(
380 &self,
381 time: Time,
382 action: &Self::Action,
383 state: &Self::State,
384 ) -> (Self::State, Option<f64>);
385
386 fn initial_state(&self) -> Self::State;
388
389 fn name(&self) -> &str;
391
392 fn extract_value(&self, state: &Self::State) -> f64;
394
395 fn reset(&self) -> Self::State {
397 self.initial_state()
398 }
399}
400
401#[derive(Clone)]
407pub enum ParameterMapping {
408 Linear,
410 Exponential,
412 Logarithmic,
414 Inverted,
416 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 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
445pub 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 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 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 pub fn set_enabled(&mut self, enabled: bool) {
519 self.enabled = enabled;
520 }
521
522 pub fn id(&self) -> &str {
524 &self.id
525 }
526}
527
528pub type BoxedServo = Box<dyn AnyServo>;
530
531pub trait AnyServo: Send + Sync {
533 fn update(&mut self, time: Time) -> Option<ParameterCommand>;
535 fn id(&self) -> &str;
537 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
561#[derive(Debug, Clone)]
562pub struct ParameterCommand {
563 pub node_id: NodeId,
565 pub param: String,
567 pub value: f32,
569}
570
571impl ParameterCommand {
572 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
582pub 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 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 pub fn add_mapping(&mut self, mapping: Mapping) {
626 self.mappings.push(mapping);
627 }
628
629 pub fn add_boxed_servo(&mut self, id: String, servo: BoxedServo) {
634 self.servos.insert(id, servo);
635 }
636
637 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 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 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 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 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 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 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 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 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 pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
927 self.sequencer_handle.as_ref()
928 }
929
930 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 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 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 pub fn get_combiner(&self, key: &str) -> Option<&PortCombinerHandle> {
974 self.port_combiners.get(key)
975 }
976
977 pub fn mappings(&self) -> &[Mapping] {
979 &self.mappings
980 }
981
982 pub fn get_servo(&self, id: &str) -> Option<&dyn AnyServo> {
984 self.servos.get(id).map(|b| b.as_ref())
985 }
986
987 pub fn get_servo_mut(&mut self, id: &str) -> Option<&mut BoxedServo> {
989 self.servos.get_mut(id)
990 }
991
992 pub fn remove_servo(&mut self, id: &str) -> bool {
994 self.servos.remove(id).is_some()
995 }
996
997 pub fn clear(&mut self) {
999 self.mappings.clear();
1000 self.servos.clear();
1001 self.stop_all();
1002 }
1003
1004 pub fn reset_time(&mut self) {
1006 self.time = 0.0;
1007 }
1008
1009 pub fn current_time(&self) -> Time {
1011 self.time
1012 }
1013}
1014
1015pub 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
1042pub 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
1061fn target_key(node_id: NodeId, param_name: &str) -> String {
1066 format!("{}:{}", node_id.0, param_name)
1067}
1068
1069#[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}