Skip to main content

phosphor_midi/
ports.rs

1//! MIDI port enumeration and monitoring.
2//!
3//! Wraps midir to provide a list of available MIDI input/output ports
4//! and detect when controllers are connected/disconnected.
5
6use anyhow::Result;
7use tracing;
8
9/// Information about an available MIDI port.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct MidiPortInfo {
12    /// Human-readable port name (e.g., "Akai MPK Mini MIDI 1").
13    pub name: String,
14    /// Index in the midir port list (changes on rescan).
15    pub index: usize,
16    /// Whether this is an input or output port.
17    pub direction: PortDirection,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PortDirection {
22    Input,
23    Output,
24}
25
26/// Scan for available MIDI input ports.
27pub fn list_input_ports() -> Result<Vec<MidiPortInfo>> {
28    let midi_in = midir::MidiInput::new("phosphor-scan")?;
29    let ports = midi_in.ports();
30
31    let mut result = Vec::with_capacity(ports.len());
32    for (i, port) in ports.iter().enumerate() {
33        let name = midi_in
34            .port_name(port)
35            .unwrap_or_else(|_| format!("MIDI Input {i}"));
36        result.push(MidiPortInfo {
37            name,
38            index: i,
39            direction: PortDirection::Input,
40        });
41    }
42
43    tracing::debug!("Found {} MIDI input ports", result.len());
44    Ok(result)
45}
46
47/// Scan for available MIDI output ports.
48pub fn list_output_ports() -> Result<Vec<MidiPortInfo>> {
49    let midi_out = midir::MidiOutput::new("phosphor-scan")?;
50    let ports = midi_out.ports();
51
52    let mut result = Vec::with_capacity(ports.len());
53    for (i, port) in ports.iter().enumerate() {
54        let name = midi_out
55            .port_name(port)
56            .unwrap_or_else(|_| format!("MIDI Output {i}"));
57        result.push(MidiPortInfo {
58            name,
59            index: i,
60            direction: PortDirection::Output,
61        });
62    }
63
64    tracing::debug!("Found {} MIDI output ports", result.len());
65    Ok(result)
66}
67
68/// Detect changes between two port scans.
69pub fn diff_ports(old: &[MidiPortInfo], new: &[MidiPortInfo]) -> PortDiff {
70    let added: Vec<MidiPortInfo> = new
71        .iter()
72        .filter(|p| !old.iter().any(|o| o.name == p.name))
73        .cloned()
74        .collect();
75
76    let removed: Vec<MidiPortInfo> = old
77        .iter()
78        .filter(|p| !new.iter().any(|n| n.name == p.name))
79        .cloned()
80        .collect();
81
82    PortDiff { added, removed }
83}
84
85/// Result of comparing two port scans.
86#[derive(Debug, Clone)]
87pub struct PortDiff {
88    pub added: Vec<MidiPortInfo>,
89    pub removed: Vec<MidiPortInfo>,
90}
91
92impl PortDiff {
93    pub fn has_changes(&self) -> bool {
94        !self.added.is_empty() || !self.removed.is_empty()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn diff_detects_added_port() {
104        let old = vec![];
105        let new = vec![MidiPortInfo {
106            name: "Controller A".into(),
107            index: 0,
108            direction: PortDirection::Input,
109        }];
110        let diff = diff_ports(&old, &new);
111        assert_eq!(diff.added.len(), 1);
112        assert_eq!(diff.added[0].name, "Controller A");
113        assert!(diff.removed.is_empty());
114        assert!(diff.has_changes());
115    }
116
117    #[test]
118    fn diff_detects_removed_port() {
119        let old = vec![MidiPortInfo {
120            name: "Controller A".into(),
121            index: 0,
122            direction: PortDirection::Input,
123        }];
124        let new = vec![];
125        let diff = diff_ports(&old, &new);
126        assert!(diff.added.is_empty());
127        assert_eq!(diff.removed.len(), 1);
128        assert_eq!(diff.removed[0].name, "Controller A");
129    }
130
131    #[test]
132    fn diff_no_changes() {
133        let ports = vec![MidiPortInfo {
134            name: "Controller A".into(),
135            index: 0,
136            direction: PortDirection::Input,
137        }];
138        let diff = diff_ports(&ports, &ports);
139        assert!(!diff.has_changes());
140    }
141
142    #[test]
143    fn diff_simultaneous_add_and_remove() {
144        let old = vec![MidiPortInfo {
145            name: "Old Controller".into(),
146            index: 0,
147            direction: PortDirection::Input,
148        }];
149        let new = vec![MidiPortInfo {
150            name: "New Controller".into(),
151            index: 0,
152            direction: PortDirection::Input,
153        }];
154        let diff = diff_ports(&old, &new);
155        assert_eq!(diff.added.len(), 1);
156        assert_eq!(diff.removed.len(), 1);
157        assert_eq!(diff.added[0].name, "New Controller");
158        assert_eq!(diff.removed[0].name, "Old Controller");
159    }
160
161    // Note: list_input_ports() and list_output_ports() are integration tests
162    // that require actual MIDI hardware or virtual ports. They're tested
163    // in CI only on machines with MIDI support, and manually during development.
164    // We test the diff logic (pure functions) exhaustively here.
165}