1use crate::StdMap;
7use alloc::string::String;
8#[cfg(feature = "wasm")]
9use alloc::string::ToString;
10use alloc::vec;
11use alloc::vec::Vec;
12use libm::Libm;
13use serde::{Deserialize, Serialize};
14
15pub type PortId = u32;
17
18pub type ParamId = u32;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
27#[serde(rename_all = "snake_case")]
28pub enum SignalKind {
29 Audio,
31
32 CvBipolar,
34
35 CvUnipolar,
37
38 VoltPerOctave,
41
42 Gate,
45
46 Trigger,
49
50 Clock,
52}
53
54impl SignalKind {
55 pub fn voltage_range(&self) -> (f64, f64) {
57 match self {
58 SignalKind::Audio => (-5.0, 5.0),
59 SignalKind::CvBipolar => (-5.0, 5.0),
60 SignalKind::CvUnipolar => (0.0, 10.0),
61 SignalKind::VoltPerOctave => (-5.0, 5.0), SignalKind::Gate => (0.0, 5.0),
63 SignalKind::Trigger => (0.0, 5.0),
64 SignalKind::Clock => (0.0, 5.0),
65 }
66 }
67
68 pub fn is_summable(&self) -> bool {
70 matches!(
71 self,
72 SignalKind::Audio
73 | SignalKind::CvBipolar
74 | SignalKind::CvUnipolar
75 | SignalKind::VoltPerOctave
76 )
77 }
78
79 pub fn gate_threshold(&self) -> Option<f64> {
81 match self {
82 SignalKind::Gate | SignalKind::Trigger | SignalKind::Clock => Some(2.5),
83 _ => None,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
94#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
95#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
96pub struct SignalColors {
97 pub audio: String,
99 pub cv_bipolar: String,
101 pub cv_unipolar: String,
103 pub volt_per_octave: String,
105 pub gate: String,
107 pub trigger: String,
109 pub clock: String,
111}
112
113impl Default for SignalColors {
114 fn default() -> Self {
115 Self {
116 audio: "#e94560".into(),
117 cv_bipolar: "#0f3460".into(),
118 cv_unipolar: "#00b4d8".into(),
119 volt_per_octave: "#90be6d".into(),
120 gate: "#f9c74f".into(),
121 trigger: "#f8961e".into(),
122 clock: "#9d4edd".into(),
123 }
124 }
125}
126
127impl SignalColors {
128 pub fn get(&self, kind: SignalKind) -> &str {
130 match kind {
131 SignalKind::Audio => &self.audio,
132 SignalKind::CvBipolar => &self.cv_bipolar,
133 SignalKind::CvUnipolar => &self.cv_unipolar,
134 SignalKind::VoltPerOctave => &self.volt_per_octave,
135 SignalKind::Gate => &self.gate,
136 SignalKind::Trigger => &self.trigger,
137 SignalKind::Clock => &self.clock,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
145#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
146pub struct PortInfo {
147 pub id: u32,
149 pub name: String,
151 pub kind: SignalKind,
153 pub normalled_to: Option<String>,
155 pub description: Option<String>,
157}
158
159impl PortInfo {
160 pub fn new(id: u32, name: impl Into<String>, kind: SignalKind) -> Self {
162 Self {
163 id,
164 name: name.into(),
165 kind,
166 normalled_to: None,
167 description: None,
168 }
169 }
170
171 pub fn with_normalled_to(mut self, port_name: impl Into<String>) -> Self {
173 self.normalled_to = Some(port_name.into());
174 self
175 }
176
177 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
179 self.description = Some(desc.into());
180 self
181 }
182}
183
184impl From<&PortDef> for PortInfo {
185 fn from(def: &PortDef) -> Self {
186 Self {
187 id: def.id,
188 name: def.name.clone(),
189 kind: def.kind,
190 normalled_to: None, description: None,
192 }
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
199#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
200#[serde(rename_all = "snake_case", tag = "status")]
201pub enum Compatibility {
202 Exact,
204 Allowed,
206 Warning { message: String },
208}
209
210pub fn ports_compatible(from: SignalKind, to: SignalKind) -> Compatibility {
226 if from == to {
227 return Compatibility::Exact;
228 }
229
230 match from.is_compatible_with(&to).warning {
232 None => Compatibility::Allowed,
233 Some(message) => Compatibility::Warning { message },
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
240pub struct PortDef {
241 pub id: PortId,
243
244 pub name: String,
246
247 pub kind: SignalKind,
249
250 pub default: f64,
252
253 pub normalled_to: Option<PortId>,
255
256 pub has_attenuverter: bool,
258}
259
260impl PortDef {
261 pub fn new(id: PortId, name: impl Into<String>, kind: SignalKind) -> Self {
262 Self {
263 id,
264 name: name.into(),
265 kind,
266 default: 0.0,
267 normalled_to: None,
268 has_attenuverter: false,
269 }
270 }
271
272 pub fn with_default(mut self, default: f64) -> Self {
273 self.default = default;
274 self
275 }
276
277 pub fn with_attenuverter(mut self) -> Self {
278 self.has_attenuverter = true;
279 self
280 }
281
282 pub fn normalled_to(mut self, port: PortId) -> Self {
283 self.normalled_to = Some(port);
284 self
285 }
286}
287
288#[derive(Debug, Clone, Default, Serialize, Deserialize)]
290#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
291#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
292pub struct PortSpec {
293 pub inputs: Vec<PortDef>,
294 pub outputs: Vec<PortDef>,
295}
296
297impl PortSpec {
298 pub fn new() -> Self {
299 Self::default()
300 }
301
302 pub fn input_by_name(&self, name: &str) -> Option<&PortDef> {
303 self.inputs.iter().find(|p| p.name == name)
304 }
305
306 pub fn output_by_name(&self, name: &str) -> Option<&PortDef> {
307 self.outputs.iter().find(|p| p.name == name)
308 }
309
310 pub fn input_by_id(&self, id: PortId) -> Option<&PortDef> {
311 self.inputs.iter().find(|p| p.id == id)
312 }
313
314 pub fn output_by_id(&self, id: PortId) -> Option<&PortDef> {
315 self.outputs.iter().find(|p| p.id == id)
316 }
317}
318
319#[derive(Debug, Clone, Default)]
337pub struct PortValues {
338 ids: Vec<PortId>,
340 values: Vec<Option<f64>>,
342}
343
344impl PortValues {
345 pub fn new() -> Self {
346 Self::default()
347 }
348
349 #[inline]
351 fn slot_of(&self, id: PortId) -> Option<usize> {
352 self.ids.iter().position(|&candidate| candidate == id)
353 }
354
355 #[inline]
356 pub fn get(&self, id: PortId) -> Option<f64> {
357 self.slot_of(id).and_then(|k| self.values[k])
358 }
359
360 #[inline]
361 pub fn get_or(&self, id: PortId, default: f64) -> f64 {
362 self.get(id).unwrap_or(default)
363 }
364
365 #[inline]
366 pub fn set(&mut self, id: PortId, value: f64) {
367 match self.slot_of(id) {
368 Some(k) => self.values[k] = Some(value),
369 None => {
370 self.ids.push(id);
371 self.values.push(Some(value));
372 }
373 }
374 }
375
376 #[inline]
378 pub fn accumulate(&mut self, id: PortId, value: f64) {
379 match self.slot_of(id) {
382 Some(k) => self.values[k] = Some(self.values[k].unwrap_or(0.0) + value),
383 None => {
384 self.ids.push(id);
385 self.values.push(Some(0.0 + value));
386 }
387 }
388 }
389
390 #[inline]
391 pub fn has(&self, id: PortId) -> bool {
392 self.get(id).is_some()
393 }
394
395 #[inline]
397 pub fn clear(&mut self) {
398 self.values.fill(None);
399 }
400
401 #[inline]
408 pub(crate) fn get_at(&self, slot: usize, id: PortId) -> Option<f64> {
409 match self.ids.get(slot) {
410 Some(&found) if found == id => self.values[slot],
411 _ => self.get(id),
412 }
413 }
414
415 #[inline]
431 pub fn iter(&self) -> impl Iterator<Item = (PortId, f64)> + '_ {
432 self.ids
433 .iter()
434 .zip(self.values.iter())
435 .filter_map(|(&id, value)| value.map(|v| (id, v)))
436 }
437}
438
439pub struct BlockPortValues {
441 buffers: StdMap<PortId, Vec<f64>>,
442 block_size: usize,
443}
444
445impl BlockPortValues {
446 pub fn new(block_size: usize) -> Self {
447 Self {
448 buffers: StdMap::new(),
449 block_size,
450 }
451 }
452
453 pub fn block_size(&self) -> usize {
454 self.block_size
455 }
456
457 pub fn get_buffer(&self, port: PortId) -> Option<&[f64]> {
458 self.buffers.get(&port).map(|v| v.as_slice())
459 }
460
461 pub fn get_buffer_mut(&mut self, port: PortId) -> &mut Vec<f64> {
462 self.buffers
463 .entry(port)
464 .or_insert_with(|| vec![0.0; self.block_size])
465 }
466
467 pub fn frame(&self, index: usize) -> PortValues {
468 let mut values = PortValues::new();
469 self.frame_into(index, &mut values);
470 values
471 }
472
473 pub fn frame_into(&self, index: usize, dst: &mut PortValues) {
480 dst.clear();
481 for (&port, buffer) in &self.buffers {
482 if index < buffer.len() {
483 dst.set(port, buffer[index]);
484 }
485 }
486 }
487
488 pub fn set_frame(&mut self, index: usize, values: PortValues) {
489 self.set_frame_ref(index, &values);
490 }
491
492 pub fn set_frame_ref(&mut self, index: usize, values: &PortValues) {
498 for (port, value) in values.iter() {
499 let buffer = self.get_buffer_mut(port);
500 if index < buffer.len() {
501 buffer[index] = value;
502 }
503 }
504 }
505
506 pub fn clear(&mut self) {
507 for buffer in self.buffers.values_mut() {
508 buffer.fill(0.0);
509 }
510 }
511}
512
513#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
515pub enum ParamRange {
516 Linear { min: f64, max: f64 },
518
519 Exponential { min: f64, max: f64 },
521
522 VoltPerOctave { base_freq: f64 },
524}
525
526impl ParamRange {
527 pub fn apply(&self, normalized: f64) -> f64 {
528 match self {
529 ParamRange::Linear { min, max } => min + normalized.clamp(0.0, 1.0) * (max - min),
530 ParamRange::Exponential { min, max } => {
531 let clamped = normalized.clamp(0.0, 1.0);
532 if *min > 0.0 && *max > 0.0 {
539 min * Libm::<f64>::pow(max / min, clamped)
540 } else {
541 min + clamped * (max - min)
542 }
543 }
544 ParamRange::VoltPerOctave { base_freq } => {
545 base_freq * Libm::<f64>::pow(2.0, normalized)
546 }
547 }
548 }
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct ModulatedParam {
554 pub base: f64,
556
557 pub cv: f64,
563
564 pub attenuverter: f64,
568
569 pub range: ParamRange,
571}
572
573impl ModulatedParam {
574 pub const CV_FULL_SCALE_VOLTS: f64 = 5.0;
579
580 pub fn new(range: ParamRange) -> Self {
581 Self {
582 base: 0.5,
583 cv: 0.0,
584 attenuverter: 1.0,
585 range,
586 }
587 }
588
589 pub fn with_base(mut self, base: f64) -> Self {
590 self.base = base;
591 self
592 }
593
594 pub fn value(&self) -> f64 {
602 let modulated = self.base + (self.cv / Self::CV_FULL_SCALE_VOLTS) * self.attenuverter;
603 self.range.apply(modulated)
604 }
605
606 pub fn set_cv(&mut self, cv: f64) {
608 self.cv = cv;
609 }
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct ParamDef {
615 pub id: ParamId,
616 pub name: String,
617 pub default: f64,
618 pub range: ParamRange,
619}
620
621pub trait GraphModule: Send + Sync {
623 fn port_spec(&self) -> &PortSpec;
625
626 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues);
628
629 fn tick_masked(&mut self, inputs: &PortValues, outputs: &mut PortValues, wanted: u32) {
650 let _ = wanted;
651 self.tick(inputs, outputs);
652 }
653
654 fn process_block(
664 &mut self,
665 inputs: &BlockPortValues,
666 outputs: &mut BlockPortValues,
667 frames: usize,
668 ) {
669 let mut in_frame = PortValues::new();
670 let mut out_frame = PortValues::new();
671 for i in 0..frames {
672 inputs.frame_into(i, &mut in_frame);
673 out_frame.clear();
674 self.tick(&in_frame, &mut out_frame);
675 outputs.set_frame_ref(i, &out_frame);
676 }
677 }
678
679 fn reset(&mut self);
681
682 fn set_sample_rate(&mut self, sample_rate: f64);
684
685 fn breaks_feedback_cycle(&self) -> bool {
698 false
699 }
700
701 fn params(&self) -> &[ParamDef] {
712 &[]
713 }
714
715 fn get_param(&self, _id: ParamId) -> Option<f64> {
720 None
721 }
722
723 fn set_param(&mut self, _id: ParamId, _value: f64) {}
728
729 fn type_id(&self) -> &'static str {
731 "unknown"
732 }
733
734 #[cfg(feature = "alloc")]
736 fn serialize_state(&self) -> Option<serde_json::Value> {
737 None
738 }
739
740 #[cfg(feature = "alloc")]
742 fn deserialize_state(
743 &mut self,
744 _state: &serde_json::Value,
745 ) -> Result<(), alloc::string::String> {
746 Ok(())
747 }
748
749 #[cfg(feature = "alloc")]
762 fn introspect(&self) -> Option<&dyn crate::introspection::ModuleIntrospection> {
763 None
764 }
765
766 #[cfg(feature = "alloc")]
768 fn introspect_mut(&mut self) -> Option<&mut dyn crate::introspection::ModuleIntrospection> {
769 None
770 }
771}
772
773#[macro_export]
780macro_rules! impl_introspect {
781 () => {
782 #[cfg(feature = "alloc")]
783 fn introspect(&self) -> Option<&dyn $crate::introspection::ModuleIntrospection> {
784 Some(self)
785 }
786 #[cfg(feature = "alloc")]
787 fn introspect_mut(
788 &mut self,
789 ) -> Option<&mut dyn $crate::introspection::ModuleIntrospection> {
790 Some(self)
791 }
792 };
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 #[test]
800 fn test_signal_kind_ranges() {
801 assert_eq!(SignalKind::Audio.voltage_range(), (-5.0, 5.0));
802 assert_eq!(SignalKind::Gate.voltage_range(), (0.0, 5.0));
803 assert_eq!(SignalKind::CvUnipolar.voltage_range(), (0.0, 10.0));
804 }
805
806 #[test]
807 fn test_signal_kind_summable() {
808 assert!(SignalKind::Audio.is_summable());
809 assert!(SignalKind::CvBipolar.is_summable());
810 assert!(!SignalKind::Gate.is_summable());
811 assert!(!SignalKind::Trigger.is_summable());
812 }
813
814 #[test]
815 fn test_port_values() {
816 let mut pv = PortValues::new();
817 pv.set(0, 1.0);
818 pv.set(1, 2.0);
819 assert_eq!(pv.get(0), Some(1.0));
820 assert_eq!(pv.get(1), Some(2.0));
821 assert_eq!(pv.get(2), None);
822 assert_eq!(pv.get_or(2, 5.0), 5.0);
823
824 pv.accumulate(0, 0.5);
825 assert_eq!(pv.get(0), Some(1.5));
826 }
827
828 #[test]
832 fn test_port_values_clear_keeps_layout_and_absence() {
833 let mut pv = PortValues::new();
834 pv.set(7, 1.0);
835 pv.set(3, 2.0);
836
837 pv.clear();
838 assert!(!pv.has(7));
839 assert!(!pv.has(3));
840 assert_eq!(pv.get(7), None);
841 assert_eq!(pv.get_or(3, -1.0), -1.0);
842 assert_eq!(pv.iter().count(), 0);
843
844 pv.set(3, 4.0);
846 assert_eq!(pv.get_at(1, 3), Some(4.0));
847 assert_eq!(pv.get(7), None);
848 assert_eq!(pv.iter().collect::<Vec<_>>(), vec![(3, 4.0)]);
849 }
850
851 #[test]
854 fn test_port_values_get_at_falls_back_to_lookup() {
855 let mut pv = PortValues::new();
856 pv.set(10, 1.0);
857 pv.set(11, 2.0);
858
859 assert_eq!(pv.get_at(0, 10), Some(1.0));
860 assert_eq!(pv.get_at(1, 10), Some(1.0));
862 assert_eq!(pv.get_at(99, 11), Some(2.0));
863 assert_eq!(pv.get_at(0, 12), None);
864 }
865
866 #[test]
869 fn test_port_values_accumulate_from_absent_normalizes_signed_zero() {
870 let mut pv = PortValues::new();
871 pv.accumulate(0, -0.0);
872 assert_eq!(pv.get(0).map(f64::to_bits), Some(0.0f64.to_bits()));
873 }
874
875 #[test]
876 fn test_param_range_linear() {
877 let range = ParamRange::Linear {
878 min: 0.0,
879 max: 100.0,
880 };
881 assert!((range.apply(0.0) - 0.0).abs() < 1e-10);
882 assert!((range.apply(0.5) - 50.0).abs() < 1e-10);
883 assert!((range.apply(1.0) - 100.0).abs() < 1e-10);
884 }
885
886 #[test]
887 fn test_param_range_exponential() {
888 let range = ParamRange::Exponential {
889 min: 20.0,
890 max: 20000.0,
891 };
892 assert!((range.apply(0.0) - 20.0).abs() < 1e-10);
893 assert!((range.apply(1.0) - 20000.0).abs() < 1e-10);
894 }
895
896 #[test]
897 fn test_param_range_voct() {
898 let range = ParamRange::VoltPerOctave { base_freq: 261.63 };
899 assert!((range.apply(0.0) - 261.63).abs() < 0.01);
901 assert!((range.apply(1.0) - 523.26).abs() < 0.01);
903 }
904
905 #[test]
906 fn test_modulated_param() {
907 let mut param = ModulatedParam::new(ParamRange::Linear {
908 min: 0.0,
909 max: 100.0,
910 })
911 .with_base(0.5);
912
913 assert!((param.value() - 50.0).abs() < 1e-10);
915
916 param.set_cv(1.0);
919 assert!((param.value() - 70.0).abs() < 1e-10);
920
921 param.attenuverter = -1.0;
923 assert!((param.value() - 30.0).abs() < 1e-10);
924 }
925
926 #[test]
927 fn test_modulated_param_full_scale_cv_is_proportional() {
928 let mut param = ModulatedParam::new(ParamRange::Linear {
931 min: 0.0,
932 max: 100.0,
933 })
934 .with_base(0.5);
935
936 param.set_cv(1.0);
940 assert!(
941 (param.value() - 70.0).abs() < 1e-10,
942 "1 V CV should be proportional, got {}",
943 param.value()
944 );
945
946 param.set_cv(5.0);
948 assert!((param.value() - 100.0).abs() < 1e-10);
949
950 param.set_cv(-5.0);
952 assert!((param.value() - 0.0).abs() < 1e-10);
953 }
954
955 #[test]
956 fn test_signal_kind_gate_threshold() {
957 assert!(SignalKind::Gate.gate_threshold().is_some());
958 assert!(SignalKind::Trigger.gate_threshold().is_some());
959 assert!(SignalKind::Audio.gate_threshold().is_none());
960 }
961
962 #[test]
963 fn test_port_def_with_default_and_attenuverter() {
964 let port = PortDef::new(0, "test", SignalKind::CvUnipolar)
965 .with_default(5.0)
966 .with_attenuverter();
967
968 assert!((port.default - 5.0).abs() < 0.001);
969 assert!(port.has_attenuverter);
970 }
971
972 #[test]
973 fn test_port_def_normalled_to() {
974 let port = PortDef::new(0, "test", SignalKind::CvUnipolar).normalled_to(1);
975 assert_eq!(port.normalled_to, Some(1));
976 }
977
978 #[test]
979 fn test_port_spec_lookup() {
980 let spec = PortSpec {
981 inputs: vec![
982 PortDef::new(0, "in1", SignalKind::Audio),
983 PortDef::new(1, "in2", SignalKind::CvBipolar),
984 ],
985 outputs: vec![
986 PortDef::new(10, "out1", SignalKind::Audio),
987 PortDef::new(11, "out2", SignalKind::Gate),
988 ],
989 };
990
991 assert!(spec.input_by_name("in1").is_some());
992 assert!(spec.input_by_name("nonexistent").is_none());
993 assert!(spec.output_by_name("out1").is_some());
994 assert!(spec.output_by_name("nonexistent").is_none());
995
996 assert!(spec.input_by_id(0).is_some());
997 assert!(spec.input_by_id(99).is_none());
998 assert!(spec.output_by_id(10).is_some());
999 assert!(spec.output_by_id(99).is_none());
1000 }
1001
1002 #[test]
1003 fn test_port_values_has() {
1004 let mut pv = PortValues::new();
1005 assert!(!pv.has(0));
1006 pv.set(0, 1.0);
1007 assert!(pv.has(0));
1008 }
1009
1010 #[test]
1011 fn test_port_values_clear() {
1012 let mut pv = PortValues::new();
1013 pv.set(0, 1.0);
1014 pv.set(1, 2.0);
1015 pv.clear();
1016 assert!(!pv.has(0));
1017 assert!(!pv.has(1));
1018 }
1019
1020 #[test]
1021 fn test_block_port_values() {
1022 let mut bpv = BlockPortValues::new(64);
1023 assert_eq!(bpv.block_size(), 64);
1024
1025 let buf_mut = bpv.get_buffer_mut(0);
1027 assert_eq!(buf_mut.len(), 64);
1028 buf_mut[0] = 1.0;
1029
1030 assert_eq!(bpv.get_buffer(0).unwrap()[0], 1.0);
1032
1033 let mut frame_vals = PortValues::new();
1035 frame_vals.set(0, 99.0);
1036 bpv.set_frame(1, frame_vals);
1037
1038 bpv.clear();
1040 }
1041
1042 #[test]
1043 fn test_signal_kind_clock() {
1044 let range = SignalKind::Clock.voltage_range();
1045 assert_eq!(range, (0.0, 5.0));
1046 assert!(!SignalKind::Clock.is_summable());
1047 }
1048
1049 #[test]
1050 fn test_param_range_exponential_clamped() {
1051 let range = ParamRange::Exponential {
1052 min: 20.0,
1053 max: 20000.0,
1054 };
1055 let below = range.apply(-0.5);
1057 assert!((below - 20.0).abs() < 1e-10);
1058
1059 let above = range.apply(1.5);
1060 assert!((above - 20000.0).abs() < 1e-10);
1061 }
1062
1063 #[test]
1064 fn test_param_range_exponential_invalid_domain_no_nan() {
1065 let range = ParamRange::Exponential {
1068 min: 20.0,
1069 max: -1.0,
1070 };
1071 for &t in &[0.0, 0.25, 0.5, 0.75, 1.0] {
1072 let v = range.apply(t);
1073 assert!(v.is_finite(), "apply({}) produced non-finite {}", t, v);
1074 }
1075 assert!((range.apply(0.0) - 20.0).abs() < 1e-10);
1077 assert!((range.apply(1.0) - (-1.0)).abs() < 1e-10);
1078
1079 let zero_max = ParamRange::Exponential {
1081 min: 10.0,
1082 max: 0.0,
1083 };
1084 assert!(zero_max.apply(0.5).is_finite());
1085 }
1086
1087 #[test]
1092 fn test_signal_colors_default() {
1093 let colors = SignalColors::default();
1094 assert_eq!(colors.audio, "#e94560");
1095 assert_eq!(colors.cv_bipolar, "#0f3460");
1096 assert_eq!(colors.cv_unipolar, "#00b4d8");
1097 assert_eq!(colors.volt_per_octave, "#90be6d");
1098 assert_eq!(colors.gate, "#f9c74f");
1099 assert_eq!(colors.trigger, "#f8961e");
1100 assert_eq!(colors.clock, "#9d4edd");
1101 }
1102
1103 #[test]
1104 fn test_signal_colors_get() {
1105 let colors = SignalColors::default();
1106 assert_eq!(colors.get(SignalKind::Audio), "#e94560");
1107 assert_eq!(colors.get(SignalKind::Gate), "#f9c74f");
1108 assert_eq!(colors.get(SignalKind::VoltPerOctave), "#90be6d");
1109 }
1110
1111 #[test]
1112 fn test_port_info_creation() {
1113 let info = PortInfo::new(0, "test", SignalKind::Audio)
1114 .with_description("A test port")
1115 .with_normalled_to("other");
1116
1117 assert_eq!(info.id, 0);
1118 assert_eq!(info.name, "test");
1119 assert_eq!(info.kind, SignalKind::Audio);
1120 assert_eq!(info.description, Some("A test port".to_string()));
1121 assert_eq!(info.normalled_to, Some("other".to_string()));
1122 }
1123
1124 #[test]
1125 fn test_port_info_from_port_def() {
1126 let def = PortDef::new(5, "cutoff", SignalKind::CvUnipolar);
1127 let info = PortInfo::from(&def);
1128
1129 assert_eq!(info.id, 5);
1130 assert_eq!(info.name, "cutoff");
1131 assert_eq!(info.kind, SignalKind::CvUnipolar);
1132 assert!(info.normalled_to.is_none());
1133 assert!(info.description.is_none());
1134 }
1135
1136 #[test]
1137 fn test_ports_compatible_exact() {
1138 assert_eq!(
1139 ports_compatible(SignalKind::Audio, SignalKind::Audio),
1140 Compatibility::Exact
1141 );
1142 assert_eq!(
1143 ports_compatible(SignalKind::Gate, SignalKind::Gate),
1144 Compatibility::Exact
1145 );
1146 assert_eq!(
1147 ports_compatible(SignalKind::VoltPerOctave, SignalKind::VoltPerOctave),
1148 Compatibility::Exact
1149 );
1150 }
1151
1152 #[test]
1153 fn test_ports_compatible_audio_to_anything() {
1154 assert!(matches!(
1157 ports_compatible(SignalKind::Audio, SignalKind::CvBipolar),
1158 Compatibility::Warning { .. }
1159 ));
1160 assert!(matches!(
1161 ports_compatible(SignalKind::Audio, SignalKind::Gate),
1162 Compatibility::Warning { .. }
1163 ));
1164 }
1165
1166 #[test]
1167 fn test_ports_compatible_cv_interop() {
1168 assert!(matches!(
1170 ports_compatible(SignalKind::CvBipolar, SignalKind::CvUnipolar),
1171 Compatibility::Warning { .. }
1172 ));
1173 assert!(matches!(
1174 ports_compatible(SignalKind::CvUnipolar, SignalKind::CvBipolar),
1175 Compatibility::Warning { .. }
1176 ));
1177 assert_eq!(
1179 ports_compatible(SignalKind::VoltPerOctave, SignalKind::CvBipolar),
1180 Compatibility::Allowed
1181 );
1182 }
1183
1184 #[test]
1185 fn test_ports_compatible_gate_trigger_interop() {
1186 assert!(matches!(
1188 ports_compatible(SignalKind::Gate, SignalKind::Trigger),
1189 Compatibility::Warning { .. }
1190 ));
1191 assert!(matches!(
1192 ports_compatible(SignalKind::Trigger, SignalKind::Gate),
1193 Compatibility::Warning { .. }
1194 ));
1195 assert_eq!(
1197 ports_compatible(SignalKind::Clock, SignalKind::Trigger),
1198 Compatibility::Allowed
1199 );
1200 assert!(matches!(
1201 ports_compatible(SignalKind::Clock, SignalKind::Gate),
1202 Compatibility::Warning { .. }
1203 ));
1204 }
1205
1206 #[test]
1207 fn test_ports_compatible_warnings() {
1208 let compat = ports_compatible(SignalKind::Gate, SignalKind::Audio);
1210 assert!(matches!(compat, Compatibility::Warning { .. }));
1211
1212 assert_eq!(
1214 ports_compatible(SignalKind::CvBipolar, SignalKind::VoltPerOctave),
1215 Compatibility::Allowed
1216 );
1217 }
1218
1219 #[test]
1220 fn test_ports_compatible_agrees_with_is_compatible_with() {
1221 let audio_cv = SignalKind::Audio.is_compatible_with(&SignalKind::CvBipolar);
1225 assert!(
1226 audio_cv.warning.is_some(),
1227 "is_compatible_with should warn on Audio->CvBipolar"
1228 );
1229 assert!(
1230 matches!(
1231 ports_compatible(SignalKind::Audio, SignalKind::CvBipolar),
1232 Compatibility::Warning { .. }
1233 ),
1234 "ports_compatible should agree and warn on Audio->CvBipolar"
1235 );
1236
1237 let all = [
1238 SignalKind::Audio,
1239 SignalKind::CvBipolar,
1240 SignalKind::CvUnipolar,
1241 SignalKind::VoltPerOctave,
1242 SignalKind::Gate,
1243 SignalKind::Trigger,
1244 SignalKind::Clock,
1245 ];
1246 for &a in &all {
1247 for &b in &all {
1248 let low = ports_compatible(a, b);
1249 let high = a.is_compatible_with(&b);
1250 let low_warns = matches!(low, Compatibility::Warning { .. });
1252 assert_eq!(
1253 low_warns,
1254 high.warning.is_some(),
1255 "compatibility APIs disagree for {:?} -> {:?}",
1256 a,
1257 b
1258 );
1259 }
1260 }
1261 }
1262
1263 #[test]
1264 fn test_signal_kind_serializes_snake_case() {
1265 assert_eq!(
1267 serde_json::to_string(&SignalKind::CvBipolar).unwrap(),
1268 "\"cv_bipolar\""
1269 );
1270 assert_eq!(
1271 serde_json::to_string(&SignalKind::VoltPerOctave).unwrap(),
1272 "\"volt_per_octave\""
1273 );
1274 assert_eq!(
1275 serde_json::to_string(&SignalKind::Audio).unwrap(),
1276 "\"audio\""
1277 );
1278 let k: SignalKind = serde_json::from_str("\"cv_unipolar\"").unwrap();
1280 assert_eq!(k, SignalKind::CvUnipolar);
1281 }
1282
1283 #[test]
1284 fn test_compatibility_serialization() {
1285 let exact = Compatibility::Exact;
1286 let json = serde_json::to_string(&exact).unwrap();
1287 assert!(json.contains("exact"));
1288
1289 let warning = Compatibility::Warning {
1290 message: "test".to_string(),
1291 };
1292 let json = serde_json::to_string(&warning).unwrap();
1293 assert!(json.contains("warning"));
1294 assert!(json.contains("test"));
1295 }
1296}