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)]
321pub struct PortValues {
322 pub values: StdMap<PortId, f64>,
323}
324
325impl PortValues {
326 pub fn new() -> Self {
327 Self::default()
328 }
329
330 pub fn get(&self, id: PortId) -> Option<f64> {
331 self.values.get(&id).copied()
332 }
333
334 pub fn get_or(&self, id: PortId, default: f64) -> f64 {
335 self.values.get(&id).copied().unwrap_or(default)
336 }
337
338 pub fn set(&mut self, id: PortId, value: f64) {
339 self.values.insert(id, value);
340 }
341
342 pub fn accumulate(&mut self, id: PortId, value: f64) {
344 *self.values.entry(id).or_insert(0.0) += value;
345 }
346
347 pub fn has(&self, id: PortId) -> bool {
348 self.values.contains_key(&id)
349 }
350
351 pub fn clear(&mut self) {
352 self.values.clear();
353 }
354}
355
356pub struct BlockPortValues {
358 buffers: StdMap<PortId, Vec<f64>>,
359 block_size: usize,
360}
361
362impl BlockPortValues {
363 pub fn new(block_size: usize) -> Self {
364 Self {
365 buffers: StdMap::new(),
366 block_size,
367 }
368 }
369
370 pub fn block_size(&self) -> usize {
371 self.block_size
372 }
373
374 pub fn get_buffer(&self, port: PortId) -> Option<&[f64]> {
375 self.buffers.get(&port).map(|v| v.as_slice())
376 }
377
378 pub fn get_buffer_mut(&mut self, port: PortId) -> &mut Vec<f64> {
379 self.buffers
380 .entry(port)
381 .or_insert_with(|| vec![0.0; self.block_size])
382 }
383
384 pub fn frame(&self, index: usize) -> PortValues {
385 let mut values = PortValues::new();
386 self.frame_into(index, &mut values);
387 values
388 }
389
390 pub fn frame_into(&self, index: usize, dst: &mut PortValues) {
397 dst.clear();
398 for (&port, buffer) in &self.buffers {
399 if index < buffer.len() {
400 dst.set(port, buffer[index]);
401 }
402 }
403 }
404
405 pub fn set_frame(&mut self, index: usize, values: PortValues) {
406 self.set_frame_ref(index, &values);
407 }
408
409 pub fn set_frame_ref(&mut self, index: usize, values: &PortValues) {
415 for (&port, &value) in &values.values {
416 let buffer = self.get_buffer_mut(port);
417 if index < buffer.len() {
418 buffer[index] = value;
419 }
420 }
421 }
422
423 pub fn clear(&mut self) {
424 for buffer in self.buffers.values_mut() {
425 buffer.fill(0.0);
426 }
427 }
428}
429
430#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
432pub enum ParamRange {
433 Linear { min: f64, max: f64 },
435
436 Exponential { min: f64, max: f64 },
438
439 VoltPerOctave { base_freq: f64 },
441}
442
443impl ParamRange {
444 pub fn apply(&self, normalized: f64) -> f64 {
445 match self {
446 ParamRange::Linear { min, max } => min + normalized.clamp(0.0, 1.0) * (max - min),
447 ParamRange::Exponential { min, max } => {
448 let clamped = normalized.clamp(0.0, 1.0);
449 if *min > 0.0 && *max > 0.0 {
456 min * Libm::<f64>::pow(max / min, clamped)
457 } else {
458 min + clamped * (max - min)
459 }
460 }
461 ParamRange::VoltPerOctave { base_freq } => {
462 base_freq * Libm::<f64>::pow(2.0, normalized)
463 }
464 }
465 }
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct ModulatedParam {
471 pub base: f64,
473
474 pub cv: f64,
480
481 pub attenuverter: f64,
485
486 pub range: ParamRange,
488}
489
490impl ModulatedParam {
491 pub const CV_FULL_SCALE_VOLTS: f64 = 5.0;
496
497 pub fn new(range: ParamRange) -> Self {
498 Self {
499 base: 0.5,
500 cv: 0.0,
501 attenuverter: 1.0,
502 range,
503 }
504 }
505
506 pub fn with_base(mut self, base: f64) -> Self {
507 self.base = base;
508 self
509 }
510
511 pub fn value(&self) -> f64 {
519 let modulated = self.base + (self.cv / Self::CV_FULL_SCALE_VOLTS) * self.attenuverter;
520 self.range.apply(modulated)
521 }
522
523 pub fn set_cv(&mut self, cv: f64) {
525 self.cv = cv;
526 }
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct ParamDef {
532 pub id: ParamId,
533 pub name: String,
534 pub default: f64,
535 pub range: ParamRange,
536}
537
538pub trait GraphModule: Send + Sync {
540 fn port_spec(&self) -> &PortSpec;
542
543 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues);
545
546 fn process_block(
556 &mut self,
557 inputs: &BlockPortValues,
558 outputs: &mut BlockPortValues,
559 frames: usize,
560 ) {
561 let mut in_frame = PortValues::new();
562 let mut out_frame = PortValues::new();
563 for i in 0..frames {
564 inputs.frame_into(i, &mut in_frame);
565 out_frame.clear();
566 self.tick(&in_frame, &mut out_frame);
567 outputs.set_frame_ref(i, &out_frame);
568 }
569 }
570
571 fn reset(&mut self);
573
574 fn set_sample_rate(&mut self, sample_rate: f64);
576
577 fn breaks_feedback_cycle(&self) -> bool {
590 false
591 }
592
593 fn params(&self) -> &[ParamDef] {
604 &[]
605 }
606
607 fn get_param(&self, _id: ParamId) -> Option<f64> {
612 None
613 }
614
615 fn set_param(&mut self, _id: ParamId, _value: f64) {}
620
621 fn type_id(&self) -> &'static str {
623 "unknown"
624 }
625
626 #[cfg(feature = "alloc")]
628 fn serialize_state(&self) -> Option<serde_json::Value> {
629 None
630 }
631
632 #[cfg(feature = "alloc")]
634 fn deserialize_state(
635 &mut self,
636 _state: &serde_json::Value,
637 ) -> Result<(), alloc::string::String> {
638 Ok(())
639 }
640
641 #[cfg(feature = "alloc")]
654 fn introspect(&self) -> Option<&dyn crate::introspection::ModuleIntrospection> {
655 None
656 }
657
658 #[cfg(feature = "alloc")]
660 fn introspect_mut(&mut self) -> Option<&mut dyn crate::introspection::ModuleIntrospection> {
661 None
662 }
663}
664
665#[macro_export]
672macro_rules! impl_introspect {
673 () => {
674 #[cfg(feature = "alloc")]
675 fn introspect(&self) -> Option<&dyn $crate::introspection::ModuleIntrospection> {
676 Some(self)
677 }
678 #[cfg(feature = "alloc")]
679 fn introspect_mut(
680 &mut self,
681 ) -> Option<&mut dyn $crate::introspection::ModuleIntrospection> {
682 Some(self)
683 }
684 };
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690
691 #[test]
692 fn test_signal_kind_ranges() {
693 assert_eq!(SignalKind::Audio.voltage_range(), (-5.0, 5.0));
694 assert_eq!(SignalKind::Gate.voltage_range(), (0.0, 5.0));
695 assert_eq!(SignalKind::CvUnipolar.voltage_range(), (0.0, 10.0));
696 }
697
698 #[test]
699 fn test_signal_kind_summable() {
700 assert!(SignalKind::Audio.is_summable());
701 assert!(SignalKind::CvBipolar.is_summable());
702 assert!(!SignalKind::Gate.is_summable());
703 assert!(!SignalKind::Trigger.is_summable());
704 }
705
706 #[test]
707 fn test_port_values() {
708 let mut pv = PortValues::new();
709 pv.set(0, 1.0);
710 pv.set(1, 2.0);
711 assert_eq!(pv.get(0), Some(1.0));
712 assert_eq!(pv.get(1), Some(2.0));
713 assert_eq!(pv.get(2), None);
714 assert_eq!(pv.get_or(2, 5.0), 5.0);
715
716 pv.accumulate(0, 0.5);
717 assert_eq!(pv.get(0), Some(1.5));
718 }
719
720 #[test]
721 fn test_param_range_linear() {
722 let range = ParamRange::Linear {
723 min: 0.0,
724 max: 100.0,
725 };
726 assert!((range.apply(0.0) - 0.0).abs() < 1e-10);
727 assert!((range.apply(0.5) - 50.0).abs() < 1e-10);
728 assert!((range.apply(1.0) - 100.0).abs() < 1e-10);
729 }
730
731 #[test]
732 fn test_param_range_exponential() {
733 let range = ParamRange::Exponential {
734 min: 20.0,
735 max: 20000.0,
736 };
737 assert!((range.apply(0.0) - 20.0).abs() < 1e-10);
738 assert!((range.apply(1.0) - 20000.0).abs() < 1e-10);
739 }
740
741 #[test]
742 fn test_param_range_voct() {
743 let range = ParamRange::VoltPerOctave { base_freq: 261.63 };
744 assert!((range.apply(0.0) - 261.63).abs() < 0.01);
746 assert!((range.apply(1.0) - 523.26).abs() < 0.01);
748 }
749
750 #[test]
751 fn test_modulated_param() {
752 let mut param = ModulatedParam::new(ParamRange::Linear {
753 min: 0.0,
754 max: 100.0,
755 })
756 .with_base(0.5);
757
758 assert!((param.value() - 50.0).abs() < 1e-10);
760
761 param.set_cv(1.0);
764 assert!((param.value() - 70.0).abs() < 1e-10);
765
766 param.attenuverter = -1.0;
768 assert!((param.value() - 30.0).abs() < 1e-10);
769 }
770
771 #[test]
772 fn test_modulated_param_full_scale_cv_is_proportional() {
773 let mut param = ModulatedParam::new(ParamRange::Linear {
776 min: 0.0,
777 max: 100.0,
778 })
779 .with_base(0.5);
780
781 param.set_cv(1.0);
785 assert!(
786 (param.value() - 70.0).abs() < 1e-10,
787 "1 V CV should be proportional, got {}",
788 param.value()
789 );
790
791 param.set_cv(5.0);
793 assert!((param.value() - 100.0).abs() < 1e-10);
794
795 param.set_cv(-5.0);
797 assert!((param.value() - 0.0).abs() < 1e-10);
798 }
799
800 #[test]
801 fn test_signal_kind_gate_threshold() {
802 assert!(SignalKind::Gate.gate_threshold().is_some());
803 assert!(SignalKind::Trigger.gate_threshold().is_some());
804 assert!(SignalKind::Audio.gate_threshold().is_none());
805 }
806
807 #[test]
808 fn test_port_def_with_default_and_attenuverter() {
809 let port = PortDef::new(0, "test", SignalKind::CvUnipolar)
810 .with_default(5.0)
811 .with_attenuverter();
812
813 assert!((port.default - 5.0).abs() < 0.001);
814 assert!(port.has_attenuverter);
815 }
816
817 #[test]
818 fn test_port_def_normalled_to() {
819 let port = PortDef::new(0, "test", SignalKind::CvUnipolar).normalled_to(1);
820 assert_eq!(port.normalled_to, Some(1));
821 }
822
823 #[test]
824 fn test_port_spec_lookup() {
825 let spec = PortSpec {
826 inputs: vec![
827 PortDef::new(0, "in1", SignalKind::Audio),
828 PortDef::new(1, "in2", SignalKind::CvBipolar),
829 ],
830 outputs: vec![
831 PortDef::new(10, "out1", SignalKind::Audio),
832 PortDef::new(11, "out2", SignalKind::Gate),
833 ],
834 };
835
836 assert!(spec.input_by_name("in1").is_some());
837 assert!(spec.input_by_name("nonexistent").is_none());
838 assert!(spec.output_by_name("out1").is_some());
839 assert!(spec.output_by_name("nonexistent").is_none());
840
841 assert!(spec.input_by_id(0).is_some());
842 assert!(spec.input_by_id(99).is_none());
843 assert!(spec.output_by_id(10).is_some());
844 assert!(spec.output_by_id(99).is_none());
845 }
846
847 #[test]
848 fn test_port_values_has() {
849 let mut pv = PortValues::new();
850 assert!(!pv.has(0));
851 pv.set(0, 1.0);
852 assert!(pv.has(0));
853 }
854
855 #[test]
856 fn test_port_values_clear() {
857 let mut pv = PortValues::new();
858 pv.set(0, 1.0);
859 pv.set(1, 2.0);
860 pv.clear();
861 assert!(!pv.has(0));
862 assert!(!pv.has(1));
863 }
864
865 #[test]
866 fn test_block_port_values() {
867 let mut bpv = BlockPortValues::new(64);
868 assert_eq!(bpv.block_size(), 64);
869
870 let buf_mut = bpv.get_buffer_mut(0);
872 assert_eq!(buf_mut.len(), 64);
873 buf_mut[0] = 1.0;
874
875 assert_eq!(bpv.get_buffer(0).unwrap()[0], 1.0);
877
878 let mut frame_vals = PortValues::new();
880 frame_vals.set(0, 99.0);
881 bpv.set_frame(1, frame_vals);
882
883 bpv.clear();
885 }
886
887 #[test]
888 fn test_signal_kind_clock() {
889 let range = SignalKind::Clock.voltage_range();
890 assert_eq!(range, (0.0, 5.0));
891 assert!(!SignalKind::Clock.is_summable());
892 }
893
894 #[test]
895 fn test_param_range_exponential_clamped() {
896 let range = ParamRange::Exponential {
897 min: 20.0,
898 max: 20000.0,
899 };
900 let below = range.apply(-0.5);
902 assert!((below - 20.0).abs() < 1e-10);
903
904 let above = range.apply(1.5);
905 assert!((above - 20000.0).abs() < 1e-10);
906 }
907
908 #[test]
909 fn test_param_range_exponential_invalid_domain_no_nan() {
910 let range = ParamRange::Exponential {
913 min: 20.0,
914 max: -1.0,
915 };
916 for &t in &[0.0, 0.25, 0.5, 0.75, 1.0] {
917 let v = range.apply(t);
918 assert!(v.is_finite(), "apply({}) produced non-finite {}", t, v);
919 }
920 assert!((range.apply(0.0) - 20.0).abs() < 1e-10);
922 assert!((range.apply(1.0) - (-1.0)).abs() < 1e-10);
923
924 let zero_max = ParamRange::Exponential {
926 min: 10.0,
927 max: 0.0,
928 };
929 assert!(zero_max.apply(0.5).is_finite());
930 }
931
932 #[test]
937 fn test_signal_colors_default() {
938 let colors = SignalColors::default();
939 assert_eq!(colors.audio, "#e94560");
940 assert_eq!(colors.cv_bipolar, "#0f3460");
941 assert_eq!(colors.cv_unipolar, "#00b4d8");
942 assert_eq!(colors.volt_per_octave, "#90be6d");
943 assert_eq!(colors.gate, "#f9c74f");
944 assert_eq!(colors.trigger, "#f8961e");
945 assert_eq!(colors.clock, "#9d4edd");
946 }
947
948 #[test]
949 fn test_signal_colors_get() {
950 let colors = SignalColors::default();
951 assert_eq!(colors.get(SignalKind::Audio), "#e94560");
952 assert_eq!(colors.get(SignalKind::Gate), "#f9c74f");
953 assert_eq!(colors.get(SignalKind::VoltPerOctave), "#90be6d");
954 }
955
956 #[test]
957 fn test_port_info_creation() {
958 let info = PortInfo::new(0, "test", SignalKind::Audio)
959 .with_description("A test port")
960 .with_normalled_to("other");
961
962 assert_eq!(info.id, 0);
963 assert_eq!(info.name, "test");
964 assert_eq!(info.kind, SignalKind::Audio);
965 assert_eq!(info.description, Some("A test port".to_string()));
966 assert_eq!(info.normalled_to, Some("other".to_string()));
967 }
968
969 #[test]
970 fn test_port_info_from_port_def() {
971 let def = PortDef::new(5, "cutoff", SignalKind::CvUnipolar);
972 let info = PortInfo::from(&def);
973
974 assert_eq!(info.id, 5);
975 assert_eq!(info.name, "cutoff");
976 assert_eq!(info.kind, SignalKind::CvUnipolar);
977 assert!(info.normalled_to.is_none());
978 assert!(info.description.is_none());
979 }
980
981 #[test]
982 fn test_ports_compatible_exact() {
983 assert_eq!(
984 ports_compatible(SignalKind::Audio, SignalKind::Audio),
985 Compatibility::Exact
986 );
987 assert_eq!(
988 ports_compatible(SignalKind::Gate, SignalKind::Gate),
989 Compatibility::Exact
990 );
991 assert_eq!(
992 ports_compatible(SignalKind::VoltPerOctave, SignalKind::VoltPerOctave),
993 Compatibility::Exact
994 );
995 }
996
997 #[test]
998 fn test_ports_compatible_audio_to_anything() {
999 assert!(matches!(
1002 ports_compatible(SignalKind::Audio, SignalKind::CvBipolar),
1003 Compatibility::Warning { .. }
1004 ));
1005 assert!(matches!(
1006 ports_compatible(SignalKind::Audio, SignalKind::Gate),
1007 Compatibility::Warning { .. }
1008 ));
1009 }
1010
1011 #[test]
1012 fn test_ports_compatible_cv_interop() {
1013 assert!(matches!(
1015 ports_compatible(SignalKind::CvBipolar, SignalKind::CvUnipolar),
1016 Compatibility::Warning { .. }
1017 ));
1018 assert!(matches!(
1019 ports_compatible(SignalKind::CvUnipolar, SignalKind::CvBipolar),
1020 Compatibility::Warning { .. }
1021 ));
1022 assert_eq!(
1024 ports_compatible(SignalKind::VoltPerOctave, SignalKind::CvBipolar),
1025 Compatibility::Allowed
1026 );
1027 }
1028
1029 #[test]
1030 fn test_ports_compatible_gate_trigger_interop() {
1031 assert!(matches!(
1033 ports_compatible(SignalKind::Gate, SignalKind::Trigger),
1034 Compatibility::Warning { .. }
1035 ));
1036 assert!(matches!(
1037 ports_compatible(SignalKind::Trigger, SignalKind::Gate),
1038 Compatibility::Warning { .. }
1039 ));
1040 assert_eq!(
1042 ports_compatible(SignalKind::Clock, SignalKind::Trigger),
1043 Compatibility::Allowed
1044 );
1045 assert!(matches!(
1046 ports_compatible(SignalKind::Clock, SignalKind::Gate),
1047 Compatibility::Warning { .. }
1048 ));
1049 }
1050
1051 #[test]
1052 fn test_ports_compatible_warnings() {
1053 let compat = ports_compatible(SignalKind::Gate, SignalKind::Audio);
1055 assert!(matches!(compat, Compatibility::Warning { .. }));
1056
1057 assert_eq!(
1059 ports_compatible(SignalKind::CvBipolar, SignalKind::VoltPerOctave),
1060 Compatibility::Allowed
1061 );
1062 }
1063
1064 #[test]
1065 fn test_ports_compatible_agrees_with_is_compatible_with() {
1066 let audio_cv = SignalKind::Audio.is_compatible_with(&SignalKind::CvBipolar);
1070 assert!(
1071 audio_cv.warning.is_some(),
1072 "is_compatible_with should warn on Audio->CvBipolar"
1073 );
1074 assert!(
1075 matches!(
1076 ports_compatible(SignalKind::Audio, SignalKind::CvBipolar),
1077 Compatibility::Warning { .. }
1078 ),
1079 "ports_compatible should agree and warn on Audio->CvBipolar"
1080 );
1081
1082 let all = [
1083 SignalKind::Audio,
1084 SignalKind::CvBipolar,
1085 SignalKind::CvUnipolar,
1086 SignalKind::VoltPerOctave,
1087 SignalKind::Gate,
1088 SignalKind::Trigger,
1089 SignalKind::Clock,
1090 ];
1091 for &a in &all {
1092 for &b in &all {
1093 let low = ports_compatible(a, b);
1094 let high = a.is_compatible_with(&b);
1095 let low_warns = matches!(low, Compatibility::Warning { .. });
1097 assert_eq!(
1098 low_warns,
1099 high.warning.is_some(),
1100 "compatibility APIs disagree for {:?} -> {:?}",
1101 a,
1102 b
1103 );
1104 }
1105 }
1106 }
1107
1108 #[test]
1109 fn test_signal_kind_serializes_snake_case() {
1110 assert_eq!(
1112 serde_json::to_string(&SignalKind::CvBipolar).unwrap(),
1113 "\"cv_bipolar\""
1114 );
1115 assert_eq!(
1116 serde_json::to_string(&SignalKind::VoltPerOctave).unwrap(),
1117 "\"volt_per_octave\""
1118 );
1119 assert_eq!(
1120 serde_json::to_string(&SignalKind::Audio).unwrap(),
1121 "\"audio\""
1122 );
1123 let k: SignalKind = serde_json::from_str("\"cv_unipolar\"").unwrap();
1125 assert_eq!(k, SignalKind::CvUnipolar);
1126 }
1127
1128 #[test]
1129 fn test_compatibility_serialization() {
1130 let exact = Compatibility::Exact;
1131 let json = serde_json::to_string(&exact).unwrap();
1132 assert!(json.contains("exact"));
1133
1134 let warning = Compatibility::Warning {
1135 message: "test".to_string(),
1136 };
1137 let json = serde_json::to_string(&warning).unwrap();
1138 assert!(json.contains("warning"));
1139 assert!(json.contains("test"));
1140 }
1141}