use super::port::{Api, MidiPort};
use super::{MidiCallback, MidiInputConfig, RtMidiError, RtMidiErrorCallback};
#[cfg(target_os = "macos")]
use super::coremidi_impl::CoreMidiInput;
#[derive(Debug, Clone)]
pub struct TimestampedMessage {
pub timestamp: f64,
pub data: Vec<u8>,
}
pub struct MidiInput {
client_name: String,
api: Api,
config: MidiInputConfig,
port_open: bool,
port_name: Option<String>,
pending_callback: Option<MidiCallback>,
pending_error_callback: Option<RtMidiErrorCallback>,
#[cfg(target_os = "macos")]
platform: Option<PlatformInput>,
#[cfg(target_os = "linux")]
platform: Option<PlatformInput>,
#[cfg(target_os = "windows")]
platform: Option<PlatformInput>,
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
platform: Option<()>,
}
#[cfg(target_os = "macos")]
type PlatformInput = CoreMidiInput;
#[cfg(target_os = "linux")]
type PlatformInput = super::alsa_impl::AlsaMidiInput;
#[cfg(target_os = "windows")]
type PlatformInput = super::winmm_impl::WinMmMidiInput;
impl MidiInput {
pub fn new(client_name: &str) -> Result<Self, RtMidiError> {
Self::with_api(Api::default(), client_name)
}
pub fn with_api(api: Api, client_name: &str) -> Result<Self, RtMidiError> {
Ok(Self {
client_name: client_name.to_string(),
api,
config: MidiInputConfig::default(),
port_open: false,
port_name: None,
pending_callback: None,
pending_error_callback: None,
platform: None,
})
}
pub fn with_queue_size(
api: Api,
client_name: &str,
queue_size_limit: usize,
) -> Result<Self, RtMidiError> {
let mut input = Self::with_api(api, client_name)?;
input.config.queue_size = queue_size_limit;
Ok(input)
}
pub fn set_buffer_size(&mut self, _size: usize, _count: usize) {}
pub fn api(&self) -> Api {
self.api
}
pub fn client_name(&self) -> &str {
&self.client_name
}
pub fn set_client_name(&mut self, name: &str) -> Result<(), RtMidiError> {
self.client_name = name.to_string();
self.platform_set_client_name(name)
}
pub fn set_port_name(&mut self, name: &str) -> Result<(), RtMidiError> {
if !self.port_open {
return Err(RtMidiError::PortNotOpen);
}
self.port_name = Some(name.to_string());
self.platform_set_port_name(name)
}
#[cfg(target_os = "macos")]
fn platform_set_client_name(&mut self, name: &str) -> Result<(), RtMidiError> {
match self.platform {
Some(ref mut p) => p.set_client_name(name),
None => Ok(()),
}
}
#[cfg(target_os = "macos")]
fn platform_set_port_name(&mut self, name: &str) -> Result<(), RtMidiError> {
match self.platform {
Some(ref mut p) => p.set_port_name(name),
None => Err(RtMidiError::PortNotOpen),
}
}
#[cfg(not(target_os = "macos"))]
fn platform_set_client_name(&mut self, _name: &str) -> Result<(), RtMidiError> {
Err(RtMidiError::DriverError(
"set_client_name is not implemented for this platform".to_string(),
))
}
#[cfg(not(target_os = "macos"))]
fn platform_set_port_name(&mut self, _name: &str) -> Result<(), RtMidiError> {
Err(RtMidiError::DriverError(
"set_port_name is not implemented for this platform".to_string(),
))
}
pub fn config(&self) -> &MidiInputConfig {
&self.config
}
pub fn set_config(&mut self, config: MidiInputConfig) {
self.config = config;
}
pub fn ports(&self) -> Vec<MidiPort> {
self.get_ports_impl()
}
pub fn port_count(&self) -> usize {
self.ports().len()
}
pub fn port_name(&self, index: usize) -> Option<String> {
self.ports().get(index).map(|p| p.name().to_string())
}
pub fn open_port(&mut self, port: usize, port_name: &str) -> Result<(), RtMidiError> {
if self.port_open {
return Err(RtMidiError::PortAlreadyOpen);
}
let ports = self.ports();
if port >= ports.len() {
return Err(RtMidiError::InvalidPort(port));
}
self.open_port_impl(port, port_name)?;
self.port_open = true;
self.port_name = Some(port_name.to_string());
Ok(())
}
pub fn open_virtual_port(&mut self, port_name: &str) -> Result<(), RtMidiError> {
if self.port_open {
return Err(RtMidiError::PortAlreadyOpen);
}
self.open_virtual_port_impl(port_name)?;
self.port_open = true;
self.port_name = Some(port_name.to_string());
Ok(())
}
pub fn close_port(&mut self) {
if self.port_open {
self.close_port_impl();
self.port_open = false;
self.port_name = None;
}
}
pub fn is_port_open(&self) -> bool {
self.port_open
}
pub fn set_callback<F>(&mut self, callback: F)
where
F: FnMut(f64, &[u8]) + Send + 'static,
{
let boxed: MidiCallback = Box::new(callback);
if self.has_platform() {
self.platform_set_callback(boxed);
} else {
self.pending_callback = Some(boxed);
}
}
pub fn cancel_callback(&mut self) {
self.pending_callback = None;
if self.has_platform() {
self.platform_cancel_callback();
}
}
pub fn get_message(&mut self) -> Option<TimestampedMessage> {
self.platform_get_message()
.map(|(timestamp, data)| TimestampedMessage { timestamp, data })
}
pub fn ignore_types(&mut self, sysex: bool, timing: bool, active_sensing: bool) {
self.config.ignore_sysex = sysex;
self.config.ignore_timing = timing;
self.config.ignore_active_sensing = active_sensing;
if self.has_platform() {
self.platform_ignore_types(sysex, timing, active_sensing);
}
}
pub fn set_error_callback<F>(&mut self, callback: F)
where
F: FnMut(&RtMidiError) + Send + 'static,
{
let boxed: RtMidiErrorCallback = Box::new(callback);
if self.has_platform() {
self.platform_set_error_callback(boxed);
} else {
self.pending_error_callback = Some(boxed);
}
}
pub fn cancel_error_callback(&mut self) {
self.pending_error_callback = None;
if self.has_platform() {
self.platform_cancel_error_callback();
}
}
fn has_platform(&self) -> bool {
self.platform.is_some()
}
#[cfg(target_os = "macos")]
fn platform_set_callback(&mut self, callback: MidiCallback) {
if let Some(ref mut p) = self.platform {
p.set_callback(callback);
}
}
#[cfg(target_os = "macos")]
fn platform_cancel_callback(&mut self) {
if let Some(ref mut p) = self.platform {
p.cancel_callback();
}
}
#[cfg(target_os = "macos")]
fn platform_get_message(&mut self) -> Option<(f64, Vec<u8>)> {
self.platform.as_mut().and_then(|p| p.get_message())
}
#[cfg(target_os = "macos")]
fn platform_ignore_types(&mut self, sysex: bool, timing: bool, active_sensing: bool) {
if let Some(ref mut p) = self.platform {
p.ignore_types(sysex, timing, active_sensing);
}
}
#[cfg(target_os = "macos")]
fn platform_set_error_callback(&mut self, callback: RtMidiErrorCallback) {
if let Some(ref mut p) = self.platform {
p.set_error_callback(callback);
}
}
#[cfg(target_os = "macos")]
fn platform_cancel_error_callback(&mut self) {
if let Some(ref mut p) = self.platform {
p.cancel_error_callback();
}
}
#[cfg(target_os = "linux")]
fn platform_set_callback(&mut self, callback: MidiCallback) {
if let Some(ref mut p) = self.platform {
p.set_callback(callback);
}
}
#[cfg(target_os = "linux")]
fn platform_cancel_callback(&mut self) {
if let Some(ref mut p) = self.platform {
p.cancel_callback();
}
}
#[cfg(target_os = "linux")]
fn platform_get_message(&mut self) -> Option<(f64, Vec<u8>)> {
self.platform.as_mut().and_then(|p| p.get_message())
}
#[cfg(target_os = "linux")]
fn platform_ignore_types(&mut self, sysex: bool, timing: bool, active_sensing: bool) {
if let Some(ref mut p) = self.platform {
p.ignore_types(sysex, timing, active_sensing);
}
}
#[cfg(target_os = "linux")]
fn platform_set_error_callback(&mut self, callback: RtMidiErrorCallback) {
if let Some(ref mut p) = self.platform {
p.set_error_callback(callback);
}
}
#[cfg(target_os = "linux")]
fn platform_cancel_error_callback(&mut self) {
if let Some(ref mut p) = self.platform {
p.cancel_error_callback();
}
}
#[cfg(target_os = "windows")]
fn platform_set_callback(&mut self, callback: MidiCallback) {
if let Some(ref mut p) = self.platform {
p.set_callback(callback);
}
}
#[cfg(target_os = "windows")]
fn platform_cancel_callback(&mut self) {
if let Some(ref mut p) = self.platform {
p.cancel_callback();
}
}
#[cfg(target_os = "windows")]
fn platform_get_message(&mut self) -> Option<(f64, Vec<u8>)> {
self.platform.as_mut().and_then(|p| p.get_message())
}
#[cfg(target_os = "windows")]
fn platform_ignore_types(&mut self, sysex: bool, timing: bool, active_sensing: bool) {
if let Some(ref mut p) = self.platform {
p.ignore_types(sysex, timing, active_sensing);
}
}
#[cfg(target_os = "windows")]
fn platform_set_error_callback(&mut self, callback: RtMidiErrorCallback) {
if let Some(ref mut p) = self.platform {
p.set_error_callback(callback);
}
}
#[cfg(target_os = "windows")]
fn platform_cancel_error_callback(&mut self) {
if let Some(ref mut p) = self.platform {
p.cancel_error_callback();
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn platform_set_callback(&mut self, _callback: MidiCallback) {}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn platform_cancel_callback(&mut self) {}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn platform_get_message(&mut self) -> Option<(f64, Vec<u8>)> {
None
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn platform_set_error_callback(&mut self, _callback: RtMidiErrorCallback) {}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn platform_cancel_error_callback(&mut self) {}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn platform_ignore_types(&mut self, _sysex: bool, _timing: bool, _active_sensing: bool) {}
fn get_ports_impl(&self) -> Vec<MidiPort> {
match self.api {
Api::Dummy => vec![MidiPort::new(0, "Dummy Input", Api::Dummy)],
#[cfg(target_os = "macos")]
Api::CoreMidi => self.get_ports_coremidi(),
#[cfg(target_os = "linux")]
Api::Alsa => self.get_ports_alsa(),
#[cfg(target_os = "windows")]
Api::WindowsMm => self.get_ports_winmm(),
_ => vec![],
}
}
fn open_port_impl(&mut self, _port: usize, _port_name: &str) -> Result<(), RtMidiError> {
match self.api {
Api::Dummy => Ok(()),
#[cfg(target_os = "macos")]
Api::CoreMidi => self.open_port_coremidi(_port, _port_name),
#[cfg(target_os = "linux")]
Api::Alsa => self.open_port_alsa(_port, _port_name),
#[cfg(target_os = "windows")]
Api::WindowsMm => self.open_port_winmm(_port, _port_name),
_ => Err(RtMidiError::DriverError("API not available".to_string())),
}
}
fn open_virtual_port_impl(&mut self, _port_name: &str) -> Result<(), RtMidiError> {
match self.api {
Api::Dummy => Ok(()),
#[cfg(target_os = "macos")]
Api::CoreMidi => self.open_virtual_port_coremidi(_port_name),
#[cfg(target_os = "linux")]
Api::Alsa => self.open_virtual_port_alsa(_port_name),
_ => Err(RtMidiError::VirtualPortError),
}
}
fn close_port_impl(&mut self) {
match self.api {
Api::Dummy => {}
#[cfg(target_os = "macos")]
Api::CoreMidi => self.close_port_coremidi(),
#[cfg(target_os = "linux")]
Api::Alsa => self.close_port_alsa(),
#[cfg(target_os = "windows")]
Api::WindowsMm => self.close_port_winmm(),
_ => {}
}
}
#[cfg(target_os = "macos")]
fn get_ports_coremidi(&self) -> Vec<MidiPort> {
super::coremidi_impl::get_input_ports()
}
#[cfg(target_os = "macos")]
fn open_port_coremidi(&mut self, port: usize, name: &str) -> Result<(), RtMidiError> {
let mut platform = CoreMidiInput::new(&self.client_name)?;
platform.open_port(port, name)?;
self.apply_pending_state_coremidi(&mut platform);
self.platform = Some(platform);
Ok(())
}
#[cfg(target_os = "macos")]
fn open_virtual_port_coremidi(&mut self, name: &str) -> Result<(), RtMidiError> {
let mut platform = CoreMidiInput::new(&self.client_name)?;
platform.open_virtual_port(name)?;
self.apply_pending_state_coremidi(&mut platform);
self.platform = Some(platform);
Ok(())
}
#[cfg(target_os = "macos")]
fn apply_pending_state_coremidi(&mut self, platform: &mut CoreMidiInput) {
platform.ignore_types(
self.config.ignore_sysex,
self.config.ignore_timing,
self.config.ignore_active_sensing,
);
platform.set_queue_size_limit(self.config.queue_size);
if let Some(callback) = self.pending_callback.take() {
platform.set_callback(callback);
}
if let Some(callback) = self.pending_error_callback.take() {
platform.set_error_callback(callback);
}
}
#[cfg(target_os = "macos")]
fn close_port_coremidi(&mut self) {
if let Some(ref mut p) = self.platform {
p.close_port();
}
self.platform = None;
}
#[cfg(target_os = "linux")]
fn get_ports_alsa(&self) -> Vec<MidiPort> {
super::alsa_impl::get_input_ports()
}
#[cfg(target_os = "linux")]
fn open_port_alsa(&mut self, port: usize, name: &str) -> Result<(), RtMidiError> {
let mut platform = super::alsa_impl::AlsaMidiInput::new(&self.client_name)?;
platform.open_port(port, name)?;
self.apply_pending_state_alsa(&mut platform);
self.platform = Some(platform);
Ok(())
}
#[cfg(target_os = "linux")]
fn open_virtual_port_alsa(&mut self, name: &str) -> Result<(), RtMidiError> {
let mut platform = super::alsa_impl::AlsaMidiInput::new(&self.client_name)?;
platform.open_virtual_port(name)?;
self.apply_pending_state_alsa(&mut platform);
self.platform = Some(platform);
Ok(())
}
#[cfg(target_os = "linux")]
fn apply_pending_state_alsa(&mut self, platform: &mut super::alsa_impl::AlsaMidiInput) {
platform.ignore_types(
self.config.ignore_sysex,
self.config.ignore_timing,
self.config.ignore_active_sensing,
);
platform.set_queue_size_limit(self.config.queue_size);
if let Some(callback) = self.pending_callback.take() {
platform.set_callback(callback);
}
if let Some(callback) = self.pending_error_callback.take() {
platform.set_error_callback(callback);
}
}
#[cfg(target_os = "linux")]
fn close_port_alsa(&mut self) {
if let Some(ref mut p) = self.platform {
p.close_port();
}
self.platform = None;
}
#[cfg(target_os = "windows")]
fn get_ports_winmm(&self) -> Vec<MidiPort> {
super::winmm_impl::get_input_ports()
}
#[cfg(target_os = "windows")]
fn open_port_winmm(&mut self, port: usize, name: &str) -> Result<(), RtMidiError> {
let mut platform = super::winmm_impl::WinMmMidiInput::new(&self.client_name)?;
platform.open_port(port, name)?;
self.apply_pending_state_winmm(&mut platform);
self.platform = Some(platform);
Ok(())
}
#[cfg(target_os = "windows")]
fn apply_pending_state_winmm(&mut self, platform: &mut super::winmm_impl::WinMmMidiInput) {
platform.ignore_types(
self.config.ignore_sysex,
self.config.ignore_timing,
self.config.ignore_active_sensing,
);
platform.set_queue_size_limit(self.config.queue_size);
if let Some(callback) = self.pending_callback.take() {
platform.set_callback(callback);
}
if let Some(callback) = self.pending_error_callback.take() {
platform.set_error_callback(callback);
}
}
#[cfg(target_os = "windows")]
fn close_port_winmm(&mut self) {
if let Some(ref mut p) = self.platform {
p.close_port();
}
self.platform = None;
}
}
impl Drop for MidiInput {
fn drop(&mut self) {
self.close_port();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "macos")]
static COREMIDI_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(target_os = "macos")]
fn lock_coremidi_tests() -> std::sync::MutexGuard<'static, ()> {
COREMIDI_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn test_midi_input_creation() {
let input = MidiInput::new("Test Client");
assert!(input.is_ok());
}
#[test]
fn test_midi_input_config() {
let mut input = MidiInput::new("Test").unwrap();
let config = MidiInputConfig {
queue_size: 200,
ignore_timing: false,
..Default::default()
};
input.set_config(config);
assert_eq!(input.config().queue_size, 200);
}
#[cfg(target_os = "macos")]
#[test]
fn test_set_port_name_renames_live_coremidi_port() {
let _guard = lock_coremidi_tests();
use super::super::output::MidiOutput;
let mut input = MidiInput::new("mkmidilibrary-test-input-rename").unwrap();
let original_name = "mkmidilibrary-test-virtual-port-rename";
if input.open_virtual_port(original_name).is_err() {
return;
}
let renamed = "mkmidilibrary-test-virtual-port-renamed";
if input.set_port_name(renamed).is_err() {
return;
}
assert_eq!(input.port_name.as_deref(), Some(renamed));
let output = MidiOutput::new("mkmidilibrary-test-output-rename").unwrap();
let ports = output.ports();
assert!(
ports.iter().any(|p| p.name() == renamed),
"renamed port not visible under new name"
);
}
#[test]
fn test_with_queue_size_constructor() {
let input = MidiInput::with_queue_size(Api::Dummy, "Test", 42).unwrap();
assert_eq!(input.config().queue_size, 42);
}
#[test]
fn test_ignore_sysex_defaults_true() {
assert!(MidiInputConfig::default().ignore_sysex);
}
#[cfg(target_os = "macos")]
#[test]
fn test_coremidi_queue_size_cap_drops_overflow() {
let _guard = lock_coremidi_tests();
use super::super::output::MidiOutput;
use std::time::{Duration, Instant};
let mut input = MidiInput::new("mkmidilibrary-test-input-queuecap").unwrap();
input.set_config(MidiInputConfig {
queue_size: 3,
..Default::default()
});
let port_name = "mkmidilibrary-test-virtual-port-queuecap";
if input.open_virtual_port(port_name).is_err() {
return;
}
let mut output = MidiOutput::new("mkmidilibrary-test-output-queuecap").unwrap();
let ports = output.ports();
let Some(index) = ports.iter().position(|p| p.name() == port_name) else {
return;
};
if output.open_port(index, "conn").is_err() {
return;
}
for i in 0..10u8 {
output.send_note_on(0, 60 + i, 100).unwrap();
}
std::thread::sleep(Duration::from_millis(200));
let mut count = 0;
let deadline = Instant::now() + Duration::from_secs(1);
while input.get_message().is_some() {
count += 1;
if Instant::now() >= deadline {
break;
}
}
assert!(
count <= 3,
"queue_size cap of 3 should bound delivered messages, got {count}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_error_callback_fires_on_queue_overflow() {
let _guard = lock_coremidi_tests();
use super::super::output::MidiOutput;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
let mut input = MidiInput::new("mkmidilibrary-test-input-errcb").unwrap();
input.set_config(MidiInputConfig {
queue_size: 1,
..Default::default()
});
let warnings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let warnings_clone = Arc::clone(&warnings);
input.set_error_callback(move |err| {
warnings_clone.lock().unwrap().push(err.to_string());
});
let port_name = "mkmidilibrary-test-virtual-port-errcb";
if input.open_virtual_port(port_name).is_err() {
return;
}
let mut output = MidiOutput::new("mkmidilibrary-test-output-errcb").unwrap();
let ports = output.ports();
let Some(index) = ports.iter().position(|p| p.name() == port_name) else {
return;
};
if output.open_port(index, "conn").is_err() {
return;
}
let deadline = Instant::now() + Duration::from_secs(2);
while warnings.lock().unwrap().is_empty() && Instant::now() < deadline {
for i in 0..10u8 {
let _ = output.send_note_on(0, 60 + i, 100);
}
std::thread::sleep(Duration::from_millis(50));
}
assert!(
!warnings.lock().unwrap().is_empty(),
"expected at least one queue-full warning via the error callback"
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_coremidi_loopback_get_message() {
let _guard = lock_coremidi_tests();
use super::super::output::MidiOutput;
use std::time::{Duration, Instant};
let mut input = MidiInput::new("mkmidilibrary-test-input").unwrap();
let port_name = "mkmidilibrary-test-virtual-port-poll";
if input.open_virtual_port(port_name).is_err() {
return;
}
let mut output = MidiOutput::new("mkmidilibrary-test-output").unwrap();
let ports = output.ports();
let Some(index) = ports.iter().position(|p| p.name() == port_name) else {
return;
};
if output.open_port(index, "conn").is_err() {
return;
}
output.send_note_on(0, 60, 100).unwrap();
let deadline = Instant::now() + Duration::from_secs(2);
let mut received = None;
while Instant::now() < deadline {
if let Some(msg) = input.get_message() {
received = Some(msg);
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let msg = received.expect("expected a MIDI message via loopback within 2s");
assert_eq!(msg.data, vec![0x90, 60, 100]);
}
#[cfg(target_os = "macos")]
#[test]
fn test_coremidi_loopback_callback() {
let _guard = lock_coremidi_tests();
use super::super::output::MidiOutput;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
let mut input = MidiInput::new("mkmidilibrary-test-input-cb").unwrap();
let port_name = "mkmidilibrary-test-virtual-port-cb";
if input.open_virtual_port(port_name).is_err() {
return;
}
let received: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
let received_clone = Arc::clone(&received);
input.set_callback(move |_timestamp, data| {
received_clone.lock().unwrap().push(data.to_vec());
});
let mut output = MidiOutput::new("mkmidilibrary-test-output-cb").unwrap();
let ports = output.ports();
let Some(index) = ports.iter().position(|p| p.name() == port_name) else {
return;
};
if output.open_port(index, "conn").is_err() {
return;
}
output.send_note_on(1, 64, 90).unwrap();
let deadline = Instant::now() + Duration::from_secs(2);
loop {
if !received.lock().unwrap().is_empty() {
break;
}
if Instant::now() >= deadline {
panic!("expected a MIDI message via callback loopback within 2s");
}
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(received.lock().unwrap()[0], vec![0x91, 64, 90]);
}
}