pub mod clock;
mod error;
mod messages;
mod midi_io;
mod priority;
mod queues;
mod thread;
mod timing;
pub use error::Error;
pub use messages::{EcsCommand, MidiMessage, RtErrorCode, RtEvent, TransportEvent};
pub use midi_io::{list_midi_input_ports, list_midi_output_ports};
pub use queues::RtHandles;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
#[derive(Debug, Clone)]
pub struct MidiOutputConfig {
pub name: String,
}
#[derive(Debug, Clone)]
pub struct MidiInputConfig {
pub name: String,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum ClockMode {
Master {
tempo_bpm: f64,
send_transport: bool,
},
Slave {
clock_input_port: String,
timeout_ns: u64,
},
Passthrough {
clock_input_port: String,
timeout_ns: u64,
multiply: u8,
divide: u8,
},
}
#[derive(Debug, Clone)]
pub struct RuntimeConfig {
pub clock_mode: ClockMode,
pub outputs: Vec<MidiOutputConfig>,
pub inputs: Vec<MidiInputConfig>,
pub event_queue_capacity: usize,
pub command_queue_capacity: usize,
pub allow_normal_priority: bool,
}
pub struct Runtime {
pub(crate) thread: Option<JoinHandle<()>>,
shutdown: Arc<AtomicBool>,
}
impl std::fmt::Debug for Runtime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runtime")
.field("running", &self.thread.is_some())
.finish()
}
}
impl Runtime {
pub fn start(config: RuntimeConfig) -> Result<(Self, RtHandles), Error> {
let (rt_queues, ecs_handles) = crate::queues::create_queues(
config.event_queue_capacity,
config.command_queue_capacity,
);
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = Arc::clone(&shutdown);
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
let thread = std::thread::Builder::new()
.name("oxurack-rt".into())
.spawn(move || {
crate::thread::rt_thread_main(rt_queues, config, ready_tx, shutdown_clone);
})
.map_err(|e| Error::MidiInit(format!("failed to spawn RT thread: {e}")))?;
let ready_result = ready_rx.recv().map_err(|_| Error::ThreadPanicked)?;
ready_result?;
Ok((
Self {
thread: Some(thread),
shutdown,
},
ecs_handles,
))
}
pub fn stop(&mut self) -> Result<(), Error> {
self.shutdown.store(true, Ordering::Relaxed);
if let Some(thread) = self.thread.take() {
thread.join().map_err(|_| Error::ThreadPanicked)?;
} else {
return Err(Error::AlreadyStopped);
}
Ok(())
}
}
impl Drop for Runtime {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_runtime_debug() {
let config = RuntimeConfig {
clock_mode: ClockMode::Master {
tempo_bpm: 120.0,
send_transport: false,
},
outputs: Vec::new(),
inputs: Vec::new(),
event_queue_capacity: 1024,
command_queue_capacity: 1024,
allow_normal_priority: true,
};
let (mut runtime, _handles) = Runtime::start(config).unwrap();
let debug_running = format!("{runtime:?}");
assert!(
debug_running.contains("running: true"),
"expected 'running: true' in debug output, got: {debug_running}"
);
runtime.stop().unwrap();
let debug_stopped = format!("{runtime:?}");
assert!(
debug_stopped.contains("running: false"),
"expected 'running: false' in debug output, got: {debug_stopped}"
);
}
#[test]
fn test_rt_handles_debug() {
let config = RuntimeConfig {
clock_mode: ClockMode::Master {
tempo_bpm: 120.0,
send_transport: false,
},
outputs: Vec::new(),
inputs: Vec::new(),
event_queue_capacity: 1024,
command_queue_capacity: 1024,
allow_normal_priority: true,
};
let (mut runtime, handles) = Runtime::start(config).unwrap();
let debug = format!("{handles:?}");
assert!(
debug.contains("RtHandles"),
"expected 'RtHandles' in debug output, got: {debug}"
);
runtime.stop().unwrap();
}
#[test]
fn test_runtime_config_debug() {
let config = RuntimeConfig {
clock_mode: ClockMode::Master {
tempo_bpm: 120.0,
send_transport: false,
},
outputs: Vec::new(),
inputs: Vec::new(),
event_queue_capacity: 1024,
command_queue_capacity: 1024,
allow_normal_priority: true,
};
let debug = format!("{config:?}");
assert!(
debug.contains("RuntimeConfig"),
"expected 'RuntimeConfig' in debug output, got: {debug}"
);
}
#[test]
fn test_clock_mode_debug() {
let master = ClockMode::Master {
tempo_bpm: 120.0,
send_transport: true,
};
let debug = format!("{master:?}");
assert!(
debug.contains("Master"),
"expected 'Master' in debug output, got: {debug}"
);
let slave = ClockMode::Slave {
clock_input_port: "test".to_string(),
timeout_ns: 1_000_000_000,
};
let debug = format!("{slave:?}");
assert!(
debug.contains("Slave"),
"expected 'Slave' in debug output, got: {debug}"
);
let passthrough = ClockMode::Passthrough {
clock_input_port: "test".to_string(),
timeout_ns: 1_000_000_000,
multiply: 2,
divide: 1,
};
let debug = format!("{passthrough:?}");
assert!(
debug.contains("Passthrough"),
"expected 'Passthrough' in debug output, got: {debug}"
);
}
#[test]
fn test_midi_output_config_debug() {
let config = MidiOutputConfig {
name: "test-port".to_string(),
};
let debug = format!("{config:?}");
assert!(
debug.contains("test-port"),
"expected port name in debug output, got: {debug}"
);
}
#[test]
fn test_midi_input_config_debug() {
let config = MidiInputConfig {
name: "test-input".to_string(),
};
let debug = format!("{config:?}");
assert!(
debug.contains("test-input"),
"expected port name in debug output, got: {debug}"
);
}
}