#![cfg(target_os = "macos")]
use crate::impl_hw_midi_hub_traits;
use crate::message::HwMidiEvent;
use crate::midi::io::MidiEvent;
use coremidi::{Client, Destination, Destinations, OutputPort, PacketBuffer, Source, Sources};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use tracing::{error, info};
pub struct MidiHub {
client: Option<Client>,
input_port: Option<coremidi::InputPort>,
output_port: Option<OutputPort>,
pending: Arc<Mutex<VecDeque<HwMidiEvent>>>,
input_sources: Vec<(String, Source)>,
output_destinations: Vec<(String, Destination)>,
}
impl Default for MidiHub {
fn default() -> Self {
let pending: Arc<Mutex<VecDeque<HwMidiEvent>>> =
Arc::new(Mutex::new(VecDeque::with_capacity(256)));
let client = Client::new("maolan").ok();
let input_port = client.as_ref().and_then(|c| {
let pending_cb = Arc::clone(&pending);
c.input_port("maolan-in", move |packet_list| {
let mut queue = match pending_cb.lock() {
Ok(q) => q,
Err(_) => return,
};
for packet in packet_list.iter() {
let data = packet.data().to_vec();
if !data.is_empty() {
queue.push_back(HwMidiEvent {
device: String::new(),
event: MidiEvent::new(0, data),
});
}
}
})
.ok()
});
let output_port = client
.as_ref()
.and_then(|c| c.output_port("maolan-out").ok());
Self {
client,
input_port,
output_port,
pending,
input_sources: Vec::new(),
output_destinations: Vec::new(),
}
}
}
impl MidiHub {
pub fn list_sources() -> Vec<String> {
let mut result = Vec::new();
for src in Sources {
let display = src.display_name().unwrap_or_default();
result.push(format!("coreaudio:{display}"));
}
result
}
pub fn list_destinations() -> Vec<String> {
let mut result = Vec::new();
for dest in Destinations {
let display = dest.display_name().unwrap_or_default();
result.push(format!("coreaudio:{display}"));
}
result
}
pub fn open_input(&mut self, name: &str) -> Result<(), String> {
let raw_name = name.strip_prefix("coreaudio:").unwrap_or(name);
if self.input_sources.iter().any(|(n, _)| n == raw_name) {
return Ok(());
}
let source = find_source_by_name(raw_name)
.ok_or_else(|| format!("CoreMIDI source not found: {raw_name}"))?;
if let Some(ref client) = self.client {
let pending_cb = Arc::clone(&self.pending);
let device_name = raw_name.to_string();
match client.input_port(&format!("maolan-in-{raw_name}"), move |packet_list| {
let mut queue = match pending_cb.lock() {
Ok(q) => q,
Err(_) => return,
};
for packet in packet_list.iter() {
let data = packet.data().to_vec();
if !data.is_empty() {
queue.push_back(HwMidiEvent {
device: device_name.clone(),
event: MidiEvent::new(0, data),
});
}
}
}) {
Ok(port) => {
if let Err(e) = port.connect_source(&source) {
return Err(format!(
"Failed to connect CoreMIDI source '{raw_name}': {e:?}"
));
}
info!("CoreMIDI input connected: {raw_name}");
}
Err(e) => {
return Err(format!("Failed to create CoreMIDI input port: {e:?}"));
}
}
} else {
return Err("CoreMIDI client not available".to_string());
}
self.input_sources.push((raw_name.to_string(), source));
Ok(())
}
pub fn open_output(&mut self, name: &str) -> Result<(), String> {
let raw_name = name.strip_prefix("coreaudio:").unwrap_or(name);
if self.output_destinations.iter().any(|(n, _)| n == raw_name) {
return Ok(());
}
let dest = find_destination_by_name(raw_name)
.ok_or_else(|| format!("CoreMIDI destination not found: {raw_name}"))?;
info!("CoreMIDI output connected: {raw_name}");
self.output_destinations.push((raw_name.to_string(), dest));
Ok(())
}
pub fn read_events_into(&mut self, out: &mut Vec<HwMidiEvent>) {
out.clear();
let mut queue = match self.pending.lock() {
Ok(q) => q,
Err(_) => return,
};
out.extend(queue.drain(..));
}
pub fn write_events(&mut self, events: &[HwMidiEvent]) {
if events.is_empty() {
return;
}
let output_port = match self.output_port {
Some(ref port) => port,
None => return,
};
for (dest_name, destination) in &self.output_destinations {
for event in events {
if event.device != *dest_name {
continue;
}
if event.event.data.is_empty() {
continue;
}
let packet_buf = PacketBuffer::new(0, &event.event.data);
if let Err(e) = output_port.send(destination, &packet_buf) {
error!("CoreMIDI write error on {dest_name}: {e:?}");
}
}
}
}
pub fn output_devices(&self) -> Vec<String> {
self.output_destinations
.iter()
.map(|(name, _)| name.clone())
.collect()
}
}
fn find_source_by_name(name: &str) -> Option<Source> {
for source in Sources {
if let Some(display_name) = source.display_name() {
if display_name == name {
return Some(source);
}
}
}
None
}
fn find_destination_by_name(name: &str) -> Option<Destination> {
for dest in Destinations {
if let Some(display_name) = dest.display_name() {
if display_name == name {
return Some(dest);
}
}
}
None
}
impl_hw_midi_hub_traits!(MidiHub);