Skip to main content

maolan_engine/
connectable.rs

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