Skip to main content

maolan_engine/
connectable.rs

1use crate::audio::io::AudioIO;
2use crate::midi::io::MIDIIO;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6/// A set of audio input/output ports.
7pub trait AudioPorts {
8    fn audio_inputs(&self) -> Vec<Arc<AudioIO>>;
9    fn audio_outputs(&self) -> Vec<Arc<AudioIO>>;
10}
11
12/// A set of MIDI input/output ports.
13pub trait MidiPorts {
14    fn midi_inputs(&self) -> Vec<Arc<MIDIIO>>;
15    fn midi_outputs(&self) -> Vec<Arc<MIDIIO>>;
16}
17
18/// Anything that can participate in the engine's connection graph:
19/// tracks, plugins, folder children, etc.
20pub trait Connectable: AudioPorts + MidiPorts {}
21impl<T: AudioPorts + MidiPorts> Connectable for T {}
22
23/// Identifies a connectable object inside a track's routing graph.
24#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(tag = "type", content = "value")]
26pub enum ConnectableRef {
27    TrackInput,
28    TrackOutput,
29    ChildTrack(String),
30    ClapPlugin(usize),
31    Vst3Plugin(usize),
32    #[cfg(all(unix, not(target_os = "macos")))]
33    Lv2Plugin(usize),
34}
35
36/// A connection between two `Connectable` objects inside a track.
37#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub struct ConnectableConnection {
39    pub from: ConnectableRef,
40    pub from_port: usize,
41    pub to: ConnectableRef,
42    pub to_port: usize,
43    pub kind: crate::kind::Kind,
44}
45
46fn audio_output_port(connectable: &dyn AudioPorts, port: usize) -> Result<Arc<AudioIO>, String> {
47    connectable
48        .audio_outputs()
49        .get(port)
50        .cloned()
51        .ok_or_else(|| format!("Audio output port {port} not found"))
52}
53
54fn audio_input_port(connectable: &dyn AudioPorts, port: usize) -> Result<Arc<AudioIO>, String> {
55    connectable
56        .audio_inputs()
57        .get(port)
58        .cloned()
59        .ok_or_else(|| format!("Audio input port {port} not found"))
60}
61
62fn midi_output_port(connectable: &dyn MidiPorts, port: usize) -> Result<Arc<MIDIIO>, String> {
63    connectable
64        .midi_outputs()
65        .get(port)
66        .cloned()
67        .ok_or_else(|| format!("MIDI output port {port} not found"))
68}
69
70fn midi_input_port(connectable: &dyn MidiPorts, port: usize) -> Result<Arc<MIDIIO>, String> {
71    connectable
72        .midi_inputs()
73        .get(port)
74        .cloned()
75        .ok_or_else(|| format!("MIDI input port {port} not found"))
76}
77
78/// Connect an audio output port to an audio input port.
79pub fn connect_audio(
80    source: &dyn AudioPorts,
81    source_port: usize,
82    target: &dyn AudioPorts,
83    target_port: usize,
84) -> Result<(), String> {
85    let from = audio_output_port(source, source_port)?;
86    let to = audio_input_port(target, target_port)?;
87    AudioIO::connect(&from, &to);
88    Ok(())
89}
90
91/// Disconnect an audio output port from an audio input port.
92pub fn disconnect_audio(
93    source: &dyn AudioPorts,
94    source_port: usize,
95    target: &dyn AudioPorts,
96    target_port: usize,
97) -> Result<(), String> {
98    let from = audio_output_port(source, source_port)?;
99    let to = audio_input_port(target, target_port)?;
100    AudioIO::disconnect(&from, &to)
101}
102
103/// Connect a MIDI output port to a MIDI input port.
104pub fn connect_midi(
105    source: &dyn MidiPorts,
106    source_port: usize,
107    target: &dyn MidiPorts,
108    target_port: usize,
109) -> Result<(), String> {
110    let from = midi_output_port(source, source_port)?;
111    let to = midi_input_port(target, target_port)?;
112    MIDIIO::connect(&from, &to);
113    Ok(())
114}
115
116/// Disconnect a MIDI output port from a MIDI input port.
117pub fn disconnect_midi(
118    source: &dyn MidiPorts,
119    source_port: usize,
120    target: &dyn MidiPorts,
121    target_port: usize,
122) -> Result<(), String> {
123    let from = midi_output_port(source, source_port)?;
124    let to = midi_input_port(target, target_port)?;
125    MIDIIO::disconnect(&from, &to)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::{
131        AudioPorts, ConnectableConnection, ConnectableRef, MidiPorts, connect_audio, connect_midi,
132        disconnect_audio, disconnect_midi,
133    };
134    use crate::audio::io::AudioIO;
135    use crate::midi::io::MIDIIO;
136    use std::sync::Arc;
137
138    struct TestNode {
139        audio_ins: Vec<Arc<AudioIO>>,
140        audio_outs: Vec<Arc<AudioIO>>,
141        midi_ins: Vec<Arc<MIDIIO>>,
142        midi_outs: Vec<Arc<MIDIIO>>,
143    }
144
145    impl TestNode {
146        fn new(
147            audio_in_count: usize,
148            audio_out_count: usize,
149            midi_in_count: usize,
150            midi_out_count: usize,
151            buffer_size: usize,
152        ) -> Self {
153            Self {
154                audio_ins: (0..audio_in_count)
155                    .map(|_| Arc::new(AudioIO::new(buffer_size)))
156                    .collect(),
157                audio_outs: (0..audio_out_count)
158                    .map(|_| Arc::new(AudioIO::new(buffer_size)))
159                    .collect(),
160                midi_ins: (0..midi_in_count)
161                    .map(|_| Arc::new(MIDIIO::new()))
162                    .collect(),
163                midi_outs: (0..midi_out_count)
164                    .map(|_| Arc::new(MIDIIO::new()))
165                    .collect(),
166            }
167        }
168    }
169
170    impl AudioPorts for TestNode {
171        fn audio_inputs(&self) -> Vec<Arc<AudioIO>> {
172            self.audio_ins.clone()
173        }
174        fn audio_outputs(&self) -> Vec<Arc<AudioIO>> {
175            self.audio_outs.clone()
176        }
177    }
178
179    impl MidiPorts for TestNode {
180        fn midi_inputs(&self) -> Vec<Arc<MIDIIO>> {
181            self.midi_ins.clone()
182        }
183        fn midi_outputs(&self) -> Vec<Arc<MIDIIO>> {
184            self.midi_outs.clone()
185        }
186    }
187
188    #[test]
189    fn connect_audio_links_output_to_input() {
190        let source = TestNode::new(0, 1, 0, 0, 4);
191        let target = TestNode::new(1, 0, 0, 0, 4);
192
193        connect_audio(&source, 0, &target, 0).unwrap();
194
195        assert!(
196            target.audio_ins[0]
197                .connections()
198                .iter()
199                .any(|c| Arc::ptr_eq(c, &source.audio_outs[0]))
200        );
201    }
202
203    #[test]
204    fn disconnect_audio_removes_link() {
205        let source = TestNode::new(0, 1, 0, 0, 4);
206        let target = TestNode::new(1, 0, 0, 0, 4);
207        connect_audio(&source, 0, &target, 0).unwrap();
208
209        disconnect_audio(&source, 0, &target, 0).unwrap();
210
211        assert!(target.audio_ins[0].connections().is_empty());
212        assert!(source.audio_outs[0].connections().is_empty());
213    }
214
215    #[test]
216    fn disconnect_audio_errors_when_missing() {
217        let source = TestNode::new(0, 1, 0, 0, 4);
218        let target = TestNode::new(1, 0, 0, 0, 4);
219
220        let err = disconnect_audio(&source, 0, &target, 0).unwrap_err();
221        assert_eq!(err, "Connection not found");
222    }
223
224    #[test]
225    fn connect_midi_links_output_to_input() {
226        let source = TestNode::new(0, 0, 0, 1, 4);
227        let target = TestNode::new(0, 0, 1, 0, 4);
228
229        connect_midi(&source, 0, &target, 0).unwrap();
230
231        assert!(
232            target.midi_ins[0]
233                .sources()
234                .iter()
235                .any(|s| Arc::ptr_eq(s, &source.midi_outs[0]))
236        );
237    }
238
239    #[test]
240    fn disconnect_midi_removes_link() {
241        let source = TestNode::new(0, 0, 0, 1, 4);
242        let target = TestNode::new(0, 0, 1, 0, 4);
243        connect_midi(&source, 0, &target, 0).unwrap();
244
245        disconnect_midi(&source, 0, &target, 0).unwrap();
246
247        assert!(target.midi_ins[0].sources().is_empty());
248        assert!(source.midi_outs[0].connections().is_empty());
249    }
250
251    #[test]
252    fn connect_invalid_audio_port_errors() {
253        let source = TestNode::new(0, 0, 0, 0, 4);
254        let target = TestNode::new(1, 0, 0, 0, 4);
255
256        assert!(connect_audio(&source, 0, &target, 0).is_err());
257        assert!(connect_audio(&target, 0, &source, 0).is_err());
258    }
259
260    #[test]
261    fn connectable_ref_serde_round_trip() {
262        let refs = vec![
263            ConnectableRef::TrackInput,
264            ConnectableRef::TrackOutput,
265            ConnectableRef::ChildTrack("Child".to_string()),
266            ConnectableRef::ClapPlugin(3),
267            ConnectableRef::Vst3Plugin(7),
268        ];
269        for r in refs {
270            let serialized = serde_json::to_string(&r).unwrap();
271            let deserialized: ConnectableRef = serde_json::from_str(&serialized).unwrap();
272            assert_eq!(r, deserialized);
273        }
274    }
275
276    #[test]
277    fn connectable_connection_serde_round_trip() {
278        let conn = ConnectableConnection {
279            from: ConnectableRef::ChildTrack("Child".to_string()),
280            from_port: 0,
281            to: ConnectableRef::ClapPlugin(2),
282            to_port: 1,
283            kind: crate::kind::Kind::Audio,
284        };
285        let serialized = serde_json::to_string(&conn).unwrap();
286        let deserialized: ConnectableConnection = serde_json::from_str(&serialized).unwrap();
287        assert_eq!(conn, deserialized);
288    }
289}