pub(crate) struct MidiPorts {
outputs: Vec<midir::MidiOutputConnection>,
port_lost: Vec<bool>,
}
impl std::fmt::Debug for MidiPorts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MidiPorts")
.field("output_count", &self.outputs.len())
.field("ports_lost", &self.port_lost.iter().filter(|&&v| v).count())
.finish()
}
}
impl MidiPorts {
pub(crate) fn open_outputs(configs: &[crate::MidiOutputConfig]) -> Result<Self, crate::Error> {
if configs.is_empty() {
return Ok(Self {
outputs: Vec::new(),
port_lost: Vec::new(),
});
}
let mut outputs = Vec::with_capacity(configs.len());
for config in configs {
let enumerator = midir::MidiOutput::new("oxurack-rt-enum")
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
let ports = enumerator.ports();
let target_lower = config.name.to_lowercase();
let port = ports
.iter()
.find(|p| {
enumerator
.port_name(p)
.map(|name| name.to_lowercase().contains(&target_lower))
.unwrap_or(false)
})
.ok_or_else(|| crate::Error::PortNotFound {
name: config.name.clone(),
})?
.clone();
let conn_out = midir::MidiOutput::new("oxurack-rt")
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
let connection = conn_out
.connect(&port, &config.name)
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
outputs.push(connection);
}
let port_lost = vec![false; outputs.len()];
Ok(Self { outputs, port_lost })
}
pub(crate) fn send(&mut self, port_index: u8, bytes: &[u8]) -> Result<(), crate::Error> {
let idx = port_index as usize;
if idx >= self.outputs.len() {
return Err(crate::Error::PortNotFound {
name: format!("index {port_index}"),
});
}
if self.port_lost[idx] {
return Err(crate::Error::PortNotFound {
name: format!("index {port_index} (lost)"),
});
}
if let Err(e) = self.outputs[idx].send(bytes) {
self.port_lost[idx] = true;
return Err(crate::Error::MidiInit(e.to_string()));
}
Ok(())
}
#[cfg(test)]
pub(crate) fn is_port_lost(&self, port_index: u8) -> bool {
self.port_lost
.get(port_index as usize)
.copied()
.unwrap_or(true)
}
}
pub fn list_midi_output_ports() -> Result<Vec<String>, crate::Error> {
let midi_out = midir::MidiOutput::new("oxurack-rt-enum")
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
let ports = midi_out.ports();
let names: Vec<String> = ports
.iter()
.filter_map(|p| midi_out.port_name(p).ok())
.collect();
Ok(names)
}
pub fn list_midi_input_ports() -> Result<Vec<String>, crate::Error> {
let midi_in = midir::MidiInput::new("oxurack-rt-enum")
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
let ports = midi_in.ports();
let names: Vec<String> = ports
.iter()
.filter_map(|p| midi_in.port_name(p).ok())
.collect();
Ok(names)
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct RawMidiEvent {
pub(crate) port_index: u8,
pub(crate) timestamp_ns: u64,
pub(crate) bytes: [u8; 3],
pub(crate) length: u8,
}
pub(crate) struct MidiInputPorts {
_connections: Vec<midir::MidiInputConnection<()>>,
consumers: Vec<rtrb::Consumer<RawMidiEvent>>,
}
impl std::fmt::Debug for MidiInputPorts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MidiInputPorts")
.field("input_count", &self._connections.len())
.finish()
}
}
impl MidiInputPorts {
pub(crate) fn open(configs: &[crate::MidiInputConfig]) -> Result<Self, crate::Error> {
if configs.is_empty() {
return Ok(Self {
_connections: Vec::new(),
consumers: Vec::new(),
});
}
let mut connections = Vec::with_capacity(configs.len());
let mut consumers = Vec::with_capacity(configs.len());
for (port_index, config) in configs.iter().enumerate() {
let (mut producer, consumer) = rtrb::RingBuffer::new(256);
consumers.push(consumer);
let midi_in = midir::MidiInput::new("oxurack-rt-in-enum")
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
let ports = midi_in.ports();
let target_lower = config.name.to_lowercase();
let port = ports
.iter()
.find(|p| {
midi_in
.port_name(p)
.map(|name| name.to_lowercase().contains(&target_lower))
.unwrap_or(false)
})
.ok_or_else(|| crate::Error::PortNotFound {
name: config.name.clone(),
})?
.clone();
let conn_in = midir::MidiInput::new("oxurack-rt-in")
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
let idx = port_index as u8;
let callback_clock = crate::timing::MonotonicClock::new();
let connection = conn_in
.connect(
&port,
&config.name,
move |_timestamp_us, data, _| {
let mut bytes = [0u8; 3];
let len = data.len().min(3);
bytes[..len].copy_from_slice(&data[..len]);
let event = RawMidiEvent {
port_index: idx,
timestamp_ns: callback_clock.now(),
bytes,
length: len as u8,
};
let _ = producer.push(event);
},
(),
)
.map_err(|e| crate::Error::MidiInit(e.to_string()))?;
connections.push(connection);
}
Ok(Self {
_connections: connections,
consumers,
})
}
pub(crate) fn drain_all(&mut self) -> impl Iterator<Item = RawMidiEvent> + '_ {
self.consumers
.iter_mut()
.flat_map(|consumer| std::iter::from_fn(move || consumer.pop().ok()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_no_ports_succeeds() {
let result = MidiPorts::open_outputs(&[]);
assert!(result.is_ok(), "opening with no configs should succeed");
let ports = result.unwrap();
assert!(ports.outputs.is_empty());
assert!(ports.port_lost.is_empty());
}
#[test]
fn test_open_nonexistent_port_returns_error() {
let configs = vec![crate::MidiOutputConfig {
name: "__nonexistent_test_port__".to_string(),
}];
let result = MidiPorts::open_outputs(&configs);
assert!(result.is_err(), "opening a nonexistent port should fail");
let err = result.unwrap_err();
match &err {
crate::Error::PortNotFound { name } => {
assert_eq!(name, "__nonexistent_test_port__");
}
other => panic!("expected PortNotFound, got: {other}"),
}
}
#[test]
fn test_list_output_ports() {
let result = list_midi_output_ports();
assert!(
result.is_ok(),
"listing output ports should succeed: {result:?}"
);
}
#[test]
fn test_list_input_ports() {
let result = list_midi_input_ports();
assert!(
result.is_ok(),
"listing input ports should succeed: {result:?}"
);
}
#[test]
fn test_open_input_no_ports_succeeds() {
let result = MidiInputPorts::open(&[]);
assert!(
result.is_ok(),
"opening with no input configs should succeed"
);
let mut ports = result.unwrap();
assert_eq!(ports.drain_all().count(), 0);
}
#[test]
fn test_send_out_of_bounds_returns_port_not_found() {
let mut ports = MidiPorts::open_outputs(&[]).unwrap();
let result = ports.send(0, &[0xF8]);
assert!(result.is_err());
match result.unwrap_err() {
crate::Error::PortNotFound { name } => {
assert!(name.contains("index 0"), "expected index in name: {name}");
}
other => panic!("expected PortNotFound, got: {other}"),
}
}
#[test]
fn test_port_lost_tracked_on_empty() {
let ports = MidiPorts::open_outputs(&[]).unwrap();
assert!(ports.is_port_lost(0));
}
#[test]
fn test_open_input_nonexistent_returns_error() {
let configs = vec![crate::MidiInputConfig {
name: "__nonexistent_test_port__".to_string(),
}];
let result = MidiInputPorts::open(&configs);
assert!(
result.is_err(),
"opening a nonexistent input port should fail"
);
let err = result.unwrap_err();
match &err {
crate::Error::PortNotFound { name } => {
assert_eq!(name, "__nonexistent_test_port__");
}
other => panic!("expected PortNotFound, got: {other}"),
}
}
#[test]
fn test_midi_ports_debug() {
let ports = MidiPorts::open_outputs(&[]).unwrap();
let debug = format!("{ports:?}");
assert!(
debug.contains("output_count: 0"),
"expected 'output_count: 0' in debug output, got: {debug}"
);
}
#[test]
fn test_midi_input_ports_debug() {
let ports = MidiInputPorts::open(&[]).unwrap();
let debug = format!("{ports:?}");
assert!(
debug.contains("input_count: 0"),
"expected 'input_count: 0' in debug output, got: {debug}"
);
}
#[test]
fn test_port_lost_out_of_bounds_returns_true() {
let ports = MidiPorts::open_outputs(&[]).unwrap();
assert!(ports.is_port_lost(0));
assert!(ports.is_port_lost(255));
}
}