use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use rill_core::queues::{CommandEnum, SensorCommand};
use rill_core_actor::{ActorRef, ActorSystem};
use rill_io::midi_input::MidiInput;
use rill_io::midi_message::MidiMessage;
use crate::engine::{ControlEvent, MidiTransportKind, Module};
use crate::midi_clock::MidiClockTracker;
use crate::sensor::Sensor;
pub struct MidiHub {
id: String,
thread: Option<JoinHandle<()>>,
running: Arc<AtomicBool>,
events: Option<ActorRef<ControlEvent>>,
backend: Option<Box<dyn MidiInput>>,
tracker: Option<MidiClockTracker>,
}
impl MidiHub {
pub fn new(id: impl Into<String>, backend: Box<dyn MidiInput>) -> Self {
Self {
id: id.into(),
thread: None,
running: Arc::new(AtomicBool::new(true)),
events: None,
backend: Some(backend),
tracker: None,
}
}
pub fn with_clock_tracker(
id: impl Into<String>,
backend: Box<dyn MidiInput>,
tracker: MidiClockTracker,
) -> Self {
Self {
id: id.into(),
thread: None,
running: Arc::new(AtomicBool::new(true)),
events: None,
backend: Some(backend),
tracker: Some(tracker),
}
}
pub fn start(
id: impl Into<String>,
backend: Box<dyn MidiInput>,
events: ActorRef<ControlEvent>,
) -> Self {
let mut hub = Self::new(id, backend);
hub.attach(events);
hub.start();
hub
}
pub fn shared_clock(&self) -> Option<Arc<rill_core::time::SystemClock>> {
self.tracker.as_ref().map(|t| t.shared_clock())
}
}
impl Module for MidiHub {
fn id(&self) -> &str {
&self.id
}
fn stop(&mut self) {
self.running.store(false, Ordering::Release);
if let Some(handle) = self.thread.take() {
let _ = handle.join();
}
}
}
impl Sensor for MidiHub {
fn attach(&mut self, events: ActorRef<ControlEvent>) {
self.events = Some(events);
}
fn start(&mut self) {
let events = self
.events
.take()
.expect("MidiHub: attach() must be called before start()");
let backend = self
.backend
.take()
.expect("MidiHub: already started or no backend");
let mut tracker = self.tracker.take();
let r = self.running.clone();
self.thread = Some(thread::spawn(move || {
let mut backend = backend;
while r.load(Ordering::Acquire) {
match backend.poll() {
Ok(msgs) => {
for msg in msgs {
if let Some(ref mut t) = tracker {
t.process_status(msg.status());
}
if let Some(event) = parse_midi(&msg) {
events.send(event);
}
}
}
Err(e) => {
log::warn!("midi backend poll error: {e}");
thread::sleep(Duration::from_millis(10));
}
}
thread::sleep(Duration::from_millis(1));
}
}));
}
}
impl Drop for MidiHub {
fn drop(&mut self) {
self.stop();
}
}
pub fn spawn_midi_sensor(
id: &str,
backend: Box<dyn MidiInput>,
system: &ActorSystem,
servo_ref: ActorRef<CommandEnum>,
) -> ActorRef<CommandEnum> {
let enabled = Arc::new(AtomicBool::new(true));
let sr = servo_ref.clone();
let mid = id.to_string();
let actor_ref = system.spawn_detached(
&format!("midi_{id}"),
{
let e2 = enabled.clone();
move || {
Box::new(move |msg: CommandEnum| {
if let CommandEnum::Sensor(SensorCommand::SetEnabled { enabled: en, .. }) = msg
{
e2.store(en, Ordering::Release);
}
})
}
},
10,
);
thread::spawn(move || {
let mut backend = backend;
loop {
thread::sleep(Duration::from_millis(5));
if !enabled.load(Ordering::Acquire) {
continue;
}
match backend.poll() {
Ok(msgs) => {
for msg in &msgs {
if let Some(event) = parse_midi(msg) {
sr.send(CommandEnum::Control(event));
}
}
}
Err(e) => {
log::warn!("midi sensor '{mid}' poll error: {e}");
thread::sleep(Duration::from_millis(50));
}
}
}
});
actor_ref
}
pub fn parse_midi(msg: &MidiMessage) -> Option<ControlEvent> {
let status = msg.status();
match msg.message_type() {
0x80 => Some(ControlEvent::MidiNote {
channel: msg.channel(),
note: msg.data1(),
velocity: 0,
on: false,
}),
0x90 => {
let velocity = msg.data2();
if velocity == 0 {
Some(ControlEvent::MidiNote {
channel: msg.channel(),
note: msg.data1(),
velocity: 0,
on: false,
})
} else {
Some(ControlEvent::MidiNote {
channel: msg.channel(),
note: msg.data1(),
velocity,
on: true,
})
}
}
0xA0 => Some(ControlEvent::MidiNote {
channel: msg.channel(),
note: msg.data1(),
velocity: msg.data2(),
on: true,
}),
0xB0 => Some(ControlEvent::MidiControl {
channel: msg.channel(),
controller: msg.data1(),
value: msg.data2(),
normalized: msg.data2() as f32 / 127.0,
}),
0xC0 => unsupported(msg),
0xD0 => unsupported(msg),
0xE0 => {
let lsb = msg.data1() as i32;
let msb = msg.data2() as i32;
let val = (msb << 7) | lsb;
let normalized = val as f32 / 8191.0; Some(ControlEvent::MidiControl {
channel: msg.channel(),
controller: 128, value: ((normalized * 127.0) as u8).min(127),
normalized,
})
}
0xF0 => match status {
0xF2 => unsupported(msg), 0xF8 => Some(ControlEvent::MidiClock),
0xFA => Some(ControlEvent::MidiTransport {
kind: MidiTransportKind::Start,
}),
0xFB => Some(ControlEvent::MidiTransport {
kind: MidiTransportKind::Continue,
}),
0xFC => Some(ControlEvent::MidiTransport {
kind: MidiTransportKind::Stop,
}),
_ => unsupported(msg),
},
_ => unsupported(msg),
}
}
#[allow(clippy::unnecessary_wraps)]
fn unsupported(_msg: &MidiMessage) -> Option<ControlEvent> {
None
}
pub fn serialize_to_midi(event: &ControlEvent) -> Option<MidiMessage> {
match event {
ControlEvent::MidiClock => Some(MidiMessage::new(0xF8, 0, 0)),
ControlEvent::MidiTransport { kind } => {
let status = match kind {
MidiTransportKind::Start => 0xFA,
MidiTransportKind::Stop => 0xFC,
MidiTransportKind::Continue => 0xFB,
};
Some(MidiMessage::new(status, 0, 0))
}
ControlEvent::MidiNote {
note, velocity, on, ..
} => {
let status = if *on { 0x90 } else { 0x80 };
Some(MidiMessage::new(
status,
*note,
if *on { *velocity } else { 0 },
))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rill_core::queues::control_event::MidiTransportKind;
#[test]
fn test_serialize_midi_clock_roundtrip() {
let event = ControlEvent::MidiClock;
let msg = serialize_to_midi(&event).unwrap();
let back = parse_midi(&msg).unwrap();
assert_eq!(back, event);
}
#[test]
fn test_serialize_midi_transport_roundtrip() {
for kind in [
MidiTransportKind::Start,
MidiTransportKind::Stop,
MidiTransportKind::Continue,
] {
let event = ControlEvent::MidiTransport { kind };
let msg = serialize_to_midi(&event).unwrap();
let back = parse_midi(&msg).unwrap();
assert_eq!(back, event);
}
}
#[test]
fn test_serialize_midi_note_roundtrip() {
let event = ControlEvent::MidiNote {
channel: 0,
note: 64,
velocity: 100,
on: true,
};
let msg = serialize_to_midi(&event).unwrap();
assert_eq!(msg, MidiMessage::new(0x90, 64, 100));
let event_off = ControlEvent::MidiNote {
channel: 0,
note: 64,
velocity: 0,
on: false,
};
let msg_off = serialize_to_midi(&event_off).unwrap();
assert_eq!(msg_off, MidiMessage::new(0x80, 64, 0));
}
#[test]
fn test_serialize_unsupported_returns_none() {
let event = ControlEvent::MidiControl {
channel: 0,
controller: 7,
value: 100,
normalized: 0.8,
};
assert!(serialize_to_midi(&event).is_none());
}
}