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::{SetParameter, SignalOrigin};
15use rill_core_actor::ActorRef;
16
17use crossbeam_channel::Receiver as CrossbeamReceiver;
18
19pub use crate::automaton::Range;
20use crate::automaton::{EnvelopeAutomaton, LfoAutomaton, LfoWaveform};
21use crate::automaton_task::spawn_automaton_task;
22use crate::port_combiner::{spawn_combiner, PortCombinerHandle};
23use crate::sequencer::{SequencerCommand, SequencerHandle, SnapshotSequencer};
24use crate::strategy::{ConflictStrategy, ControlStrategy, UiCommand};
25
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub enum EventPattern {
34 AnyButton,
36 ButtonId(u32),
38
39 AnyKnob,
41 KnobId(u32),
43
44 AnyFader,
46 FaderId(u32),
48
49 AnyMidi,
51 MidiControl {
53 channel: Option<u8>,
55 controller: u8,
57 },
58 MidiNote {
60 channel: Option<u8>,
62 note: Option<u8>,
64 },
65
66 OscAddress(String),
68
69 OscPattern(String),
71}
72
73impl EventPattern {
74 pub fn matches(&self, event: &ControlEvent) -> bool {
76 match (self, event) {
77 (EventPattern::AnyButton, ControlEvent::Button { .. }) => true,
78 (EventPattern::ButtonId(id), ControlEvent::Button { id: eid, .. }) => *id == *eid,
79
80 (EventPattern::AnyKnob, ControlEvent::Knob { .. }) => true,
81 (EventPattern::KnobId(id), ControlEvent::Knob { id: eid, .. }) => *id == *eid,
82
83 (EventPattern::AnyFader, ControlEvent::Fader { .. }) => true,
84 (EventPattern::FaderId(id), ControlEvent::Fader { id: eid, .. }) => *id == *eid,
85
86 (
87 EventPattern::MidiControl {
88 channel,
89 controller,
90 },
91 ControlEvent::MidiControl {
92 channel: ech,
93 controller: ectr,
94 ..
95 },
96 ) => (channel.is_none() || channel.unwrap() == *ech) && *controller == *ectr,
97
98 (EventPattern::OscAddress(addr), ControlEvent::Osc { address, .. }) => addr == address,
99
100 (EventPattern::OscPattern(pat), ControlEvent::Osc { address, .. }) => {
101 address.contains(pat)
102 }
103
104 _ => false,
105 }
106 }
107}
108
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115#[derive(Debug, Clone, PartialEq)]
116pub enum ControlEvent {
117 Button {
119 id: u32,
121 pressed: bool,
123 },
124
125 Knob {
127 id: u32,
129 value: f32,
131 normalized: f32,
133 },
134
135 Fader {
137 id: u32,
139 value: f32,
141 normalized: f32,
143 },
144
145 MidiControl {
147 channel: u8,
149 controller: u8,
151 value: u8,
153 normalized: f32,
155 },
156
157 MidiNote {
159 channel: u8,
161 note: u8,
163 velocity: u8,
165 on: bool,
167 },
168
169 Osc {
171 address: String,
173 args: Vec<f32>,
175 },
176}
177
178impl ControlEvent {
179 pub fn normalized_value(&self) -> Option<f32> {
181 match self {
182 ControlEvent::Knob { normalized, .. } => Some(*normalized),
183 ControlEvent::Fader { normalized, .. } => Some(*normalized),
184 ControlEvent::MidiControl { normalized, .. } => Some(*normalized),
185 ControlEvent::Button { pressed, .. } => Some(if *pressed { 1.0 } else { 0.0 }),
186 _ => None,
187 }
188 }
189
190 pub fn id(&self) -> Option<u32> {
192 match self {
193 ControlEvent::Button { id, .. } => Some(*id),
194 ControlEvent::Knob { id, .. } => Some(*id),
195 ControlEvent::Fader { id, .. } => Some(*id),
196 _ => None,
197 }
198 }
199}
200
201#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211#[derive(Debug, Clone)]
212pub struct OscSurfaceEntry {
213 pub osc_path: String,
215
216 pub event_pattern: EventPattern,
218
219 #[cfg_attr(
221 feature = "serde",
222 serde(default, skip_serializing_if = "Option::is_none")
223 )]
224 pub label: Option<String>,
225}
226
227pub type OscSurface = Vec<OscSurfaceEntry>;
229
230#[derive(Clone)]
236pub enum Transform {
237 Linear,
239
240 Exponential,
242
243 Logarithmic,
245
246 Inverted,
248
249 Custom(Arc<dyn Fn(f32) -> f32 + Send + Sync>),
251}
252
253impl Debug for Transform {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 match self {
256 Transform::Linear => write!(f, "Linear"),
257 Transform::Exponential => write!(f, "Exponential"),
258 Transform::Logarithmic => write!(f, "Logarithmic"),
259 Transform::Inverted => write!(f, "Inverted"),
260 Transform::Custom(_) => write!(f, "Custom"),
261 }
262 }
263}
264
265impl Transform {
266 pub fn apply(&self, value: f32, min: f32, max: f32) -> f32 {
268 let range = max - min;
269 let normalized = value.clamp(0.0, 1.0);
270
271 let mapped = match self {
272 Transform::Linear => min + normalized * range,
273 Transform::Exponential => min + normalized * normalized * range,
274 Transform::Logarithmic => min + (1.0 + normalized * 9.0).log10() * range,
275 Transform::Inverted => max - normalized * range,
276 Transform::Custom(f) => min + f(normalized) * range,
277 };
278
279 mapped.clamp(min, max)
280 }
281}
282
283#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
289#[derive(Debug, Clone)]
290pub struct Target {
291 pub node_id: NodeId,
293 pub param_name: String,
295 pub min: f32,
297 pub max: f32,
299}
300
301#[derive(Debug, Clone)]
303pub struct Mapping {
304 pub pattern: EventPattern,
306 pub target: Target,
308 pub transform: Transform,
310 pub name: String,
312 pub enabled: bool,
314}
315
316impl Mapping {
317 pub fn new(pattern: EventPattern, target: Target, transform: Transform) -> Self {
319 let name = format!("{:?} -> {}", pattern, target.param_name);
320 Self {
321 pattern,
322 target,
323 transform,
324 name,
325 enabled: true,
326 }
327 }
328
329 pub fn matches(&self, event: &ControlEvent) -> bool {
331 self.enabled && self.pattern.matches(event)
332 }
333
334 pub fn apply(&self, event: &ControlEvent) -> Option<SetParameter> {
336 if !self.matches(event) {
337 return None;
338 }
339
340 event.normalized_value().map(|norm| {
341 let value = self.transform.apply(norm, self.target.min, self.target.max);
342 let pid = ParameterId::new(&self.target.param_name).unwrap();
343 SetParameter::new(
344 PortId::param(self.target.node_id, 0),
345 pid,
346 ParamValue::Float(value),
347 SignalOrigin::External(self.name.clone()),
348 )
349 })
350 }
351}
352
353pub type Time = f64;
359
360#[derive(Debug, Clone, Default)]
362pub struct NoAction;
363
364pub trait Automaton: Send + Sync + Debug {
371 type State: Clone + Send + Sync + 'static + Debug;
373
374 type Action: Debug + Clone + Send + Sync + Default + 'static;
376
377 fn step(
386 &self,
387 time: Time,
388 action: &Self::Action,
389 state: &Self::State,
390 ) -> (Self::State, Option<f64>);
391
392 fn initial_state(&self) -> Self::State;
394
395 fn name(&self) -> &str;
397
398 fn extract_value(&self, state: &Self::State) -> f64;
400
401 fn reset(&self) -> Self::State {
403 self.initial_state()
404 }
405}
406
407#[derive(Clone)]
413pub enum ParameterMapping {
414 Linear,
416 Exponential,
418 Logarithmic,
420 Inverted,
422 Custom(Arc<dyn Fn(f64) -> f64 + Send + Sync>),
424}
425
426impl std::fmt::Debug for ParameterMapping {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 match self {
429 ParameterMapping::Linear => write!(f, "Linear"),
430 ParameterMapping::Exponential => write!(f, "Exponential"),
431 ParameterMapping::Logarithmic => write!(f, "Logarithmic"),
432 ParameterMapping::Inverted => write!(f, "Inverted"),
433 ParameterMapping::Custom(_) => write!(f, "Custom(<fn>)"),
434 }
435 }
436}
437
438impl ParameterMapping {
439 pub fn apply(&self, raw: f64) -> f64 {
441 match self {
442 ParameterMapping::Linear => raw,
443 ParameterMapping::Exponential => raw * raw,
444 ParameterMapping::Logarithmic => (1.0 + raw * 9.0).log10(),
445 ParameterMapping::Inverted => 1.0 - raw,
446 ParameterMapping::Custom(f) => f(raw),
447 }
448 }
449}
450
451pub struct Servo<A: Automaton> {
453 id: String,
454 automaton: A,
455 state: A::State,
456 target_node: NodeId,
457 target_param: String,
458 mapping: ParameterMapping,
459 min: f64,
460 max: f64,
461 last_value: f64,
462 enabled: bool,
463 last_time: Time,
464}
465
466impl<A: Automaton> Servo<A> {
467 pub fn new(
469 id: impl Into<String>,
470 automaton: A,
471 target_node: NodeId,
472 target_param: impl Into<String>,
473 mapping: ParameterMapping,
474 min: f64,
475 max: f64,
476 ) -> Self {
477 let state = automaton.initial_state();
478 Self {
479 id: id.into(),
480 automaton,
481 state,
482 target_node,
483 target_param: target_param.into(),
484 mapping,
485 min,
486 max,
487 last_value: 0.0,
488 enabled: true,
489 last_time: 0.0,
490 }
491 }
492
493 pub fn update(&mut self, time: Time) -> Option<SetParameter> {
495 if !self.enabled {
496 return None;
497 }
498
499 let (new_state, value_opt) = self
500 .automaton
501 .step(time, &A::Action::default(), &self.state);
502 self.state = new_state;
503
504 if let Some(raw_value) = value_opt {
505 let mapped = self.mapping.apply(raw_value);
506 let clamped = mapped.clamp(self.min, self.max);
507
508 if (clamped - self.last_value).abs() > 1e-6 {
509 self.last_value = clamped;
510 self.last_time = time;
511
512 let pid = ParameterId::new(&self.target_param).unwrap();
513 return Some(SetParameter::new(
514 PortId::param(self.target_node, 0),
515 pid,
516 ParamValue::Float(clamped as f32),
517 SignalOrigin::Automaton(self.id.clone()),
518 ));
519 }
520 }
521
522 None
523 }
524
525 pub fn set_enabled(&mut self, enabled: bool) {
527 self.enabled = enabled;
528 }
529
530 pub fn id(&self) -> &str {
532 &self.id
533 }
534}
535
536pub type BoxedServo = Box<dyn AnyServo>;
538
539pub trait AnyServo: Send + Sync {
541 fn update(&mut self, time: Time) -> Option<SetParameter>;
543 fn id(&self) -> &str;
545 fn set_enabled(&mut self, enabled: bool);
547}
548
549impl<A: Automaton + 'static> AnyServo for Servo<A> {
550 fn update(&mut self, time: Time) -> Option<SetParameter> {
551 Servo::update(self, time)
552 }
553
554 fn id(&self) -> &str {
555 &self.id
556 }
557
558 fn set_enabled(&mut self, enabled: bool) {
559 self.enabled = enabled;
560 }
561}
562
563pub struct Patchbay {
580 mappings: Vec<Mapping>,
581 servos: HashMap<String, BoxedServo>,
582 port_combiners: HashMap<String, PortCombinerHandle>,
583 automaton_handles: HashMap<String, tokio::task::JoinHandle<()>>,
584 sequencer_handle: Option<SequencerHandle>,
585 sequencer_task: Option<tokio::task::JoinHandle<()>>,
586 command_queue: ActorRef<SetParameter>,
587 time: Time,
588}
589
590impl Patchbay {
591 pub fn new(command_queue: ActorRef<SetParameter>) -> Self {
597 Self {
598 mappings: Vec::new(),
599 servos: HashMap::new(),
600 port_combiners: HashMap::new(),
601 automaton_handles: HashMap::new(),
602 sequencer_handle: None,
603 sequencer_task: None,
604 command_queue,
605 time: 0.0,
606 }
607 }
608
609 pub fn add_mapping(&mut self, mapping: Mapping) {
611 self.mappings.push(mapping);
612 }
613
614 pub fn add_boxed_servo(&mut self, id: String, servo: BoxedServo) {
619 self.servos.insert(id, servo);
620 }
621
622 pub fn add_mapping_str(
628 &mut self,
629 pattern: &str,
630 target_node: NodeId,
631 target_param: &str,
632 min: f32,
633 max: f32,
634 transform: Transform,
635 ) -> Result<(), &'static str> {
636 let pattern = match pattern {
637 p if p.starts_with("button:") => {
638 let id = p[7..].parse().map_err(|_| "Invalid button ID")?;
639 EventPattern::ButtonId(id)
640 }
641 p if p.starts_with("knob:") => {
642 let id = p[5..].parse().map_err(|_| "Invalid knob ID")?;
643 EventPattern::KnobId(id)
644 }
645 p if p.starts_with("fader:") => {
646 let id = p[6..].parse().map_err(|_| "Invalid fader ID")?;
647 EventPattern::FaderId(id)
648 }
649 p if p.starts_with("midi:") => {
650 let parts: Vec<&str> = p[5..].split(':').collect();
651 if parts.len() == 2 {
652 let channel = parts[0].parse().ok();
653 let controller = parts[1].parse().map_err(|_| "Invalid controller")?;
654 EventPattern::MidiControl {
655 channel,
656 controller,
657 }
658 } else {
659 EventPattern::AnyMidi
660 }
661 }
662 p if p.starts_with("osc:") => EventPattern::OscAddress(p[4..].to_string()),
663 _ => return Err("Unknown pattern"),
664 };
665
666 let target = Target {
667 node_id: target_node,
668 param_name: target_param.to_string(),
669 min,
670 max,
671 };
672
673 self.add_mapping(Mapping::new(pattern, target, transform));
674 Ok(())
675 }
676
677 pub fn add_servo<A: Automaton + 'static>(&mut self, servo: Servo<A>) {
679 self.servos.insert(servo.id().to_string(), Box::new(servo));
680 }
681
682 pub fn add_lfo(
684 &mut self,
685 id: &str,
686 frequency: f64,
687 amplitude: f64,
688 offset: f64,
689 waveform: LfoWaveform,
690 target_node: NodeId,
691 target_param: &str,
692 min: f64,
693 max: f64,
694 ) {
695 let automaton = LfoAutomaton::new(id, frequency, amplitude, offset, waveform);
696 let servo = Servo::new(
697 id,
698 automaton,
699 target_node,
700 target_param,
701 ParameterMapping::Linear,
702 min,
703 max,
704 );
705 self.add_servo(servo);
706 }
707
708 pub fn add_envelope(
710 &mut self,
711 id: &str,
712 attack: f64,
713 decay: f64,
714 sustain: f64,
715 release: f64,
716 target_node: NodeId,
717 target_param: &str,
718 min: f64,
719 max: f64,
720 ) {
721 let automaton = EnvelopeAutomaton::adsr(id, attack, decay, sustain, release);
722 let servo = Servo::new(
723 id,
724 automaton,
725 target_node,
726 target_param,
727 ParameterMapping::Linear,
728 min,
729 max,
730 );
731 self.add_servo(servo);
732 }
733
734 pub fn add_automaton_task<A: Automaton + 'static>(
749 &mut self,
750 id: &str,
751 automaton: A,
752 interval: Duration,
753 target: (NodeId, String),
754 range: (f64, f64),
755 control: ControlStrategy,
756 conflict: ConflictStrategy,
757 ) {
758 let key = target_key(target.0, &target.1);
759
760 let combiner = spawn_combiner(target, range, control, conflict, self.command_queue.clone());
761
762 let task = spawn_automaton_task(
763 automaton,
764 interval,
765 combiner.automaton_tx.clone(),
766 combiner.cancel_rx(),
767 );
768
769 self.port_combiners.insert(key, combiner);
770 self.automaton_handles.insert(id.to_string(), task);
771 }
772
773 pub fn add_lfo_task(
775 &mut self,
776 id: &str,
777 frequency: f64,
778 amplitude: f64,
779 offset: f64,
780 waveform: LfoWaveform,
781 interval: Duration,
782 target: (NodeId, String),
783 range: (f64, f64),
784 control: ControlStrategy,
785 conflict: ConflictStrategy,
786 ) {
787 let automaton = LfoAutomaton::new(id, frequency, amplitude, offset, waveform);
788 self.add_automaton_task(
789 format!("{}_auto", id).as_str(),
790 automaton,
791 interval,
792 target,
793 range,
794 control,
795 conflict,
796 );
797 }
798
799 pub fn add_envelope_task(
801 &mut self,
802 id: &str,
803 attack: f64,
804 decay: f64,
805 sustain: f64,
806 release: f64,
807 interval: Duration,
808 target: (NodeId, String),
809 range: (f64, f64),
810 control: ControlStrategy,
811 conflict: ConflictStrategy,
812 ) {
813 let automaton = EnvelopeAutomaton::adsr(id, attack, decay, sustain, release);
814 self.add_automaton_task(
815 format!("{}_auto", id).as_str(),
816 automaton,
817 interval,
818 target,
819 range,
820 control,
821 conflict,
822 );
823 }
824
825 pub fn attach_sequencer(
836 &mut self,
837 tel_rx: CrossbeamReceiver<Telemetry>,
838 sequencer: SnapshotSequencer,
839 ) -> SequencerHandle {
840 assert!(
841 self.sequencer_task.is_none(),
842 "sequencer already attached — detach first"
843 );
844
845 let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<SequencerCommand>();
846 let queue = self.command_queue.clone();
847
848 let task = tokio::task::spawn_blocking(move || {
849 let mut seq = sequencer;
850
851 loop {
852 loop {
853 match cmd_rx.try_recv() {
854 Ok(SequencerCommand::Start) => seq.start(),
855 Ok(SequencerCommand::Stop) => seq.stop(),
856 Ok(SequencerCommand::Reset { sample_pos }) => seq.reset(sample_pos),
857 Ok(SequencerCommand::SetPattern(id)) => seq.set_active_pattern(&id),
858 Err(crossbeam_channel::TryRecvError::Empty) => break,
859 Err(crossbeam_channel::TryRecvError::Disconnected) => return,
860 }
861 }
862
863 match tel_rx.recv() {
864 Ok(Telemetry::Event { kind, data, .. })
865 if kind == CLOCK_TICK && data.len() >= 3 =>
866 {
867 let sample_pos = data[0] as u64;
868 let sample_rate = data[1];
869 let tempo = data[2];
870
871 let beat_pos = data.get(3).copied().unwrap_or(0.0);
872 let new_beat = data.get(4).copied().unwrap_or(0.0) > 0.5;
873 let new_bar = data.get(5).copied().unwrap_or(0.0) > 0.5;
874
875 let cmds = seq.tick_ext(
876 sample_pos,
877 sample_rate,
878 tempo,
879 beat_pos,
880 new_beat,
881 new_bar,
882 );
883 for cmd in cmds {
884 queue.send(cmd);
885 }
886 }
887 Err(_) => return,
888 _ => {}
889 }
890 }
891 });
892
893 let handle = SequencerHandle::new(cmd_tx);
894 self.sequencer_handle = Some(handle.clone());
895 self.sequencer_task = Some(task);
896
897 handle
898 }
899
900 pub fn detach_sequencer(&mut self) {
902 if let Some(task) = self.sequencer_task.take() {
903 task.abort();
904 }
905 self.sequencer_handle = None;
906 }
907
908 pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
910 self.sequencer_handle.as_ref()
911 }
912
913 pub fn stop_all(&mut self) {
915 for combiner in self.port_combiners.values() {
916 combiner.stop();
917 }
918 self.port_combiners.clear();
919 self.automaton_handles.clear();
920 self.detach_sequencer();
921 }
922
923 pub fn add_automaton<A: Automaton + 'static>(
927 &mut self,
928 id: &str,
929 automaton: A,
930 interval: Duration,
931 target: (NodeId, String),
932 range: (f64, f64),
933 control: ControlStrategy,
934 conflict: ConflictStrategy,
935 ) {
936 self.add_automaton_task(id, automaton, interval, target, range, control, conflict);
937 }
938
939 pub fn add_lfo_async(
941 &mut self,
942 id: &str,
943 frequency: f64,
944 amplitude: f64,
945 offset: f64,
946 waveform: LfoWaveform,
947 interval: Duration,
948 target: (NodeId, String),
949 range: (f64, f64),
950 control: ControlStrategy,
951 conflict: ConflictStrategy,
952 ) {
953 self.add_lfo_task(
954 id, frequency, amplitude, offset, waveform, interval, target, range, control, conflict,
955 );
956 }
957
958 pub fn add_envelope_async(
960 &mut self,
961 id: &str,
962 attack: f64,
963 decay: f64,
964 sustain: f64,
965 release: f64,
966 interval: Duration,
967 target: (NodeId, String),
968 range: (f64, f64),
969 control: ControlStrategy,
970 conflict: ConflictStrategy,
971 ) {
972 self.add_envelope_task(
973 id, attack, decay, sustain, release, interval, target, range, control, conflict,
974 );
975 }
976
977 pub fn handle_event(&mut self, event: ControlEvent) {
983 for mapping in &self.mappings {
984 if let Some(cmd) = mapping.apply(&event) {
985 let key = target_key(cmd.port.node_id(), cmd.parameter.as_ref());
986 if let Some(combiner) = self.port_combiners.get(&key) {
987 let _ = combiner
988 .ui_tx
989 .send(UiCommand::SetValue(cmd.value.as_f32().unwrap_or(0.0) as f64));
990 } else {
991 self.command_queue.send(cmd);
992 }
993 }
994 }
995 }
996
997 pub fn update(&mut self, dt: f32) {
1002 self.time += dt as f64;
1003
1004 for servo in self.servos.values_mut() {
1005 if let Some(cmd) = servo.update(self.time) {
1006 self.command_queue.send(cmd);
1007 }
1008 }
1009 }
1010
1011 pub fn get_combiner(&self, key: &str) -> Option<&PortCombinerHandle> {
1013 self.port_combiners.get(key)
1014 }
1015
1016 pub fn mappings(&self) -> &[Mapping] {
1018 &self.mappings
1019 }
1020
1021 pub fn get_servo(&self, id: &str) -> Option<&dyn AnyServo> {
1023 self.servos.get(id).map(|b| b.as_ref())
1024 }
1025
1026 pub fn get_servo_mut(&mut self, id: &str) -> Option<&mut BoxedServo> {
1028 self.servos.get_mut(id)
1029 }
1030
1031 pub fn remove_servo(&mut self, id: &str) -> bool {
1033 self.servos.remove(id).is_some()
1034 }
1035
1036 pub fn clear(&mut self) {
1038 self.mappings.clear();
1039 self.servos.clear();
1040 self.stop_all();
1041 }
1042
1043 pub fn reset_time(&mut self) {
1045 self.time = 0.0;
1046 }
1047
1048 pub fn current_time(&self) -> Time {
1050 self.time
1051 }
1052}
1053
1054impl Drop for Patchbay {
1055 fn drop(&mut self) {
1056 self.stop_all();
1057 }
1058}
1059
1060pub fn midi_cc(
1066 controller: u8,
1067 channel: Option<u8>,
1068 target_node: NodeId,
1069 target_param: &str,
1070 min: f32,
1071 max: f32,
1072 transform: Transform,
1073) -> Mapping {
1074 let pattern = EventPattern::MidiControl {
1075 channel,
1076 controller,
1077 };
1078 let target = Target {
1079 node_id: target_node,
1080 param_name: target_param.to_string(),
1081 min,
1082 max,
1083 };
1084 Mapping::new(pattern, target, transform)
1085}
1086
1087pub fn osc_address(
1089 address: &str,
1090 target_node: NodeId,
1091 target_param: &str,
1092 min: f32,
1093 max: f32,
1094 transform: Transform,
1095) -> Mapping {
1096 let pattern = EventPattern::OscAddress(address.to_string());
1097 let target = Target {
1098 node_id: target_node,
1099 param_name: target_param.to_string(),
1100 min,
1101 max,
1102 };
1103 Mapping::new(pattern, target, transform)
1104}
1105
1106fn target_key(node_id: NodeId, param_name: &str) -> String {
1111 format!("{}:{}", node_id.inner(), param_name)
1112}
1113
1114#[cfg(test)]
1119mod tests {
1120 use super::*;
1121
1122 #[test]
1123 fn test_midi_mapping() {
1124 let node = NodeId(1);
1125 let mapping = midi_cc(7, Some(1), node, "volume", 0.0, 1.0, Transform::Linear);
1126
1127 let event = ControlEvent::MidiControl {
1128 channel: 1,
1129 controller: 7,
1130 value: 64,
1131 normalized: 0.5,
1132 };
1133
1134 assert!(mapping.matches(&event));
1135
1136 let cmd = mapping.apply(&event).unwrap();
1137 assert_eq!(cmd.port.node_id(), node);
1138 assert_eq!(cmd.parameter.as_ref(), "volume");
1139 assert!((cmd.value.as_f32().unwrap() - 0.5).abs() < 1e-6);
1140 }
1141
1142 #[test]
1143 fn test_lfo_servo() {
1144 let node = NodeId(1);
1145 let (actor_ref, _mailbox) = ActorRef::new_pair();
1146 let mut control = Patchbay::new(actor_ref);
1147
1148 control.add_lfo(
1149 "test_lfo",
1150 1.0,
1151 0.5,
1152 0.0,
1153 LfoWaveform::Sine,
1154 node,
1155 "cutoff",
1156 100.0,
1157 1000.0,
1158 );
1159
1160 assert!(control.get_servo("test_lfo").is_some());
1161
1162 for _i in 0..10 {
1163 control.update(0.1);
1164 }
1165 }
1166
1167 #[test]
1168 fn test_envelope_servo() {
1169 let node = NodeId(1);
1170 let (actor_ref, _mailbox) = ActorRef::new_pair();
1171 let mut control = Patchbay::new(actor_ref);
1172
1173 control.add_envelope("test_env", 0.1, 0.2, 0.7, 0.3, node, "gain", 0.0, 1.0);
1174
1175 if let Some(_servo) = control.get_servo_mut("test_env") {}
1176
1177 control.update(0.05);
1178 control.update(0.05);
1179 }
1180}