use crate::core::io::clock::MidiClock;
use crate::core::io::osc::Osc;
use crate::core::io::udp::Udp;
use midir::{MidiInput, MidiInputConnection, MidiOutput};
use std::{
net::UdpSocket,
sync::{
Arc,
atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
mpsc::{SyncSender, sync_channel},
},
thread::JoinHandle,
};
pub(crate) const MIDI_NOTE_OFF: u8 = 0x80;
const MIDI_NOTE_ON: u8 = 0x90;
const MIDI_CC: u8 = 0xB0;
const MIDI_PITCH_BEND: u8 = 0xE0;
const MIDI_CHANNELS: usize = 16;
pub(crate) const DEFAULT_OSC_PORT: u16 = 49162;
pub(crate) const DEFAULT_UDP_PORT: u16 = 49161;
const DEFAULT_CC_OFFSET: u8 = 64;
pub(crate) fn pack(bpm: u16, paused: bool, bclock: bool, stop: bool) -> u64 {
(bpm as u64) | ((paused as u64) << 16) | ((bclock as u64) << 17) | ((stop as u64) << 18)
}
pub(crate) fn unpack(val: u64) -> (u16, bool, bool, bool) {
(
(val & 0xFFFF) as u16,
(val >> 16) & 1 == 1,
(val >> 17) & 1 == 1,
(val >> 18) & 1 == 1,
)
}
pub(crate) struct MidiFrame {
pub(crate) bytes: Vec<Vec<u8>>,
pub(crate) osc: Vec<(String, String)>,
pub(crate) udp: Vec<String>,
pub(crate) osc_port: u16,
pub(crate) udp_port: u16,
pub(crate) ip: String,
pub(crate) osc_midi_bidule: Option<String>,
}
pub(crate) enum MidiCommand {
Silence,
ClockStart,
ClockStop,
SelectOutput(i32),
SendPg {
channel: u8,
bank: Option<u8>,
sub: Option<u8>,
pgm: Option<u8>,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MidiNote {
pub channel: u8,
pub octave: u8,
pub note: char,
pub note_id: u8,
pub velocity: u8,
pub length: usize,
pub is_played: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MidiCc {
pub channel: u8,
pub knob: u8,
pub value: u8,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MidiPb {
pub channel: u8,
pub lsb: u8,
pub msb: u8,
}
#[derive(Debug, Clone)]
pub enum MidiMessage {
Cc(MidiCc),
Pb(MidiPb),
}
pub struct MidiState {
pub stack: Vec<MidiNote>,
pub mono_stack: [Option<MidiNote>; MIDI_CHANNELS],
pub cc_stack: Vec<MidiMessage>,
pub last_io_count: usize,
pub osc: Osc,
pub udp: Udp,
pub cc_offset: u8,
pub device_name: String,
pub input_device_name: String,
pub output_index: i32,
pub input_index: i32,
pub ip: String,
pub osc_midi_bidule: Option<String>,
pending: Vec<Vec<u8>>,
frame_tx: SyncSender<MidiFrame>,
cmd_tx: SyncSender<MidiCommand>,
in_tx: SyncSender<u8>,
_input_conn: Option<MidiInputConnection<()>>,
shared: Arc<AtomicU64>,
puppet_tick: Arc<AtomicBool>,
transport_event: Arc<AtomicU8>,
is_puppet: Arc<AtomicBool>,
_thread_handle: Option<JoinHandle<()>>,
}
impl std::fmt::Debug for MidiState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MidiState")
.field("device_name", &self.device_name)
.field("input_device_name", &self.input_device_name)
.field("output_index", &self.output_index)
.field("input_index", &self.input_index)
.field("cc_offset", &self.cc_offset)
.field("ip", &self.ip)
.field("osc_midi_bidule", &self.osc_midi_bidule)
.field("stack_len", &self.stack.len())
.field("cc_stack_len", &self.cc_stack.len())
.finish_non_exhaustive()
}
}
impl MidiState {
pub fn new() -> Self {
let udp_socket = UdpSocket::bind("0.0.0.0:0").ok();
let shared = Arc::new(AtomicU64::new(pack(120, true, false, false)));
let (frame_tx, frame_rx) = sync_channel::<MidiFrame>(2);
let (cmd_tx, cmd_rx) = sync_channel::<MidiCommand>(16);
let (in_tx, in_rx) = sync_channel::<u8>(64);
let puppet_tick = Arc::new(AtomicBool::new(false));
let transport_event = Arc::new(AtomicU8::new(0));
let is_puppet = Arc::new(AtomicBool::new(false));
let clock = MidiClock::new(
udp_socket,
DEFAULT_OSC_PORT,
DEFAULT_UDP_PORT,
in_rx,
Arc::clone(&puppet_tick),
Arc::clone(&transport_event),
Arc::clone(&is_puppet),
);
let shared_clone = Arc::clone(&shared);
let handle = std::thread::Builder::new()
.name("midi-clock".into())
.spawn(move || clock.run(shared_clone, frame_rx, cmd_rx))
.ok();
let mut state = Self {
stack: Vec::new(),
mono_stack: std::array::from_fn(|_| None),
cc_stack: Vec::new(),
osc: Osc::new(DEFAULT_OSC_PORT),
udp: Udp::new(DEFAULT_UDP_PORT),
cc_offset: DEFAULT_CC_OFFSET,
device_name: String::from("No Midi Device"),
input_device_name: String::from("No Input Device"),
output_index: -1,
input_index: -1,
ip: String::from("127.0.0.1"),
osc_midi_bidule: None,
last_io_count: 0,
pending: Vec::new(),
frame_tx,
cmd_tx,
in_tx,
_input_conn: None,
shared,
puppet_tick,
transport_event,
is_puppet,
_thread_handle: handle,
};
state.select_next_output();
state
}
pub fn set_shared(&self, bpm: usize, paused: bool, bclock: bool) {
self.shared
.store(pack(bpm as u16, paused, bclock, false), Ordering::Relaxed);
}
pub fn select_next_output(&mut self) {
if let Ok(midi) = MidiOutput::new("o2") {
let ports = midi.ports();
if ports.is_empty() {
self.output_index = -1;
self.device_name = String::from("No Output Device");
} else {
self.output_index = (self.output_index + 1) % ports.len() as i32;
let port = &ports[self.output_index as usize];
self.device_name = midi
.port_name(port)
.unwrap_or_else(|_| String::from("Unknown Device"));
}
}
let _ = self
.cmd_tx
.try_send(MidiCommand::SelectOutput(self.output_index));
}
pub fn select_next_input(&mut self) {
self._input_conn = None;
let Ok(midi) = MidiInput::new("o2") else {
return;
};
let ports = midi.ports();
if ports.is_empty() {
self.input_index = -1;
self.input_device_name = String::from("No Input Device");
return;
}
let next = if self.input_index >= ports.len() as i32 - 1 {
-1
} else {
self.input_index + 1
};
if next < 0 {
self.input_index = -1;
self.input_device_name = String::from("No Input Device");
return;
}
let port = &ports[next as usize];
let name = midi
.port_name(port)
.unwrap_or_else(|_| String::from("Unknown Device"));
let tx = self.in_tx.clone();
match midi.connect(
port,
"o2-input",
move |_, data, _| {
if let Some(&byte) = data.first() {
let _ = tx.try_send(byte);
}
},
(),
) {
Ok(conn) => {
self.input_index = next;
self.input_device_name = name;
self._input_conn = Some(conn);
}
Err(_) => {
self.input_index = -1;
self.input_device_name = String::from("No Input Device");
}
}
}
pub fn is_puppet(&self) -> bool {
self.is_puppet.load(Ordering::Relaxed)
}
pub fn poll_puppet_tick(&self) -> bool {
self.puppet_tick.swap(false, Ordering::Relaxed)
}
pub fn poll_transport_event(&self) -> u8 {
self.transport_event.swap(0, Ordering::Relaxed)
}
pub fn send_midi_msg(&mut self, msg: &[u8]) {
self.pending.push(msg.to_vec());
}
pub fn run(&mut self) {
self.stack.retain_mut(|note| {
if !note.is_played {
self.pending.push(vec![
MIDI_NOTE_ON + note.channel,
note.note_id,
note.velocity,
]);
note.is_played = true;
}
if note.length < 1 {
self.pending
.push(vec![MIDI_NOTE_OFF + note.channel, note.note_id, 0]);
false
} else {
note.length = note.length.saturating_sub(1);
true
}
});
for slot in self.mono_stack.iter_mut() {
if let Some(note) = slot {
if note.length < 1 {
if note.is_played {
self.pending
.push(vec![MIDI_NOTE_OFF + note.channel, note.note_id, 0]);
}
*slot = None;
continue;
}
if !note.is_played {
self.pending.push(vec![
MIDI_NOTE_ON + note.channel,
note.note_id,
note.velocity,
]);
note.is_played = true;
}
note.length = note.length.saturating_sub(1);
}
}
for msg in self.cc_stack.drain(..) {
match msg {
MidiMessage::Cc(cc) => {
let knob_val = self.cc_offset.saturating_add(cc.knob).min(127);
self.pending
.push(vec![MIDI_CC + cc.channel, knob_val, cc.value]);
}
MidiMessage::Pb(pb) => {
self.pending
.push(vec![MIDI_PITCH_BEND + pb.channel, pb.lsb, pb.msb]);
}
}
}
}
pub fn flush(&mut self) {
self.last_io_count = self.stack.len()
+ self.mono_stack.iter().flatten().count()
+ self.cc_stack.len()
+ self.osc.stack.len()
+ self.udp.stack.len();
self.run();
let frame = MidiFrame {
bytes: std::mem::take(&mut self.pending),
osc: std::mem::take(&mut self.osc.stack),
udp: std::mem::take(&mut self.udp.stack),
osc_port: self.osc.port,
udp_port: self.udp.port,
ip: self.ip.clone(),
osc_midi_bidule: self.osc_midi_bidule.clone(),
};
let _ = self.frame_tx.try_send(frame);
}
pub fn silence(&mut self) {
self.stack.clear();
self.mono_stack = std::array::from_fn(|_| None);
self.cc_stack.clear();
self.osc.stack.clear();
self.udp.stack.clear();
self.pending.clear();
let _ = self.cmd_tx.try_send(MidiCommand::Silence);
}
pub fn send_clock_start(&self) {
let _ = self.cmd_tx.try_send(MidiCommand::ClockStart);
}
pub fn send_clock_stop(&self) {
let _ = self.cmd_tx.try_send(MidiCommand::ClockStop);
}
pub fn send_pg(&self, channel: u8, bank: Option<u8>, sub: Option<u8>, pgm: Option<u8>) {
let _ = self.cmd_tx.try_send(MidiCommand::SendPg {
channel,
bank,
sub,
pgm,
});
}
pub fn select_output_by_index(&mut self, index: i32) {
if index < 0 {
self.output_index = -1;
self.device_name = String::from("No Output Device");
} else if let Ok(midi) = MidiOutput::new("o2") {
let ports = midi.ports();
if let Some(port) = ports.get(index as usize) {
self.output_index = index;
self.device_name = midi
.port_name(port)
.unwrap_or_else(|_| String::from("Unknown Device"));
}
}
let _ = self
.cmd_tx
.try_send(MidiCommand::SelectOutput(self.output_index));
}
}
impl Default for MidiState {
fn default() -> Self {
Self::new()
}
}
impl Drop for MidiState {
fn drop(&mut self) {
let val = self.shared.load(Ordering::Relaxed) | (1u64 << 18);
self.shared.store(val, Ordering::Release);
if let Some(handle) = self._thread_handle.take() {
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_midi_state_run_lifecycle() {
let mut state = MidiState::new();
state.stack.push(MidiNote {
channel: 0,
octave: 4,
note: 'C',
note_id: 60,
velocity: 100,
length: 2,
is_played: false,
});
state.run();
assert_eq!(state.stack.len(), 1);
assert!(state.stack[0].is_played);
assert_eq!(state.stack[0].length, 1);
state.run();
assert_eq!(state.stack.len(), 1);
assert_eq!(state.stack[0].length, 0);
state.run();
assert_eq!(state.stack.len(), 0);
}
#[test]
fn test_midi_state_silence_clears_all() {
let mut state = MidiState::new();
state.stack.push(MidiNote {
channel: 15,
octave: 2,
note: 'A',
note_id: 45,
velocity: 127,
length: 5,
is_played: true,
});
state.mono_stack[5] = Some(MidiNote {
channel: 5,
octave: 3,
note: 'B',
note_id: 59,
velocity: 64,
length: 1,
is_played: false,
});
state.cc_stack.push(MidiMessage::Cc(MidiCc {
channel: 0,
knob: 10,
value: 127,
}));
state
.osc
.stack
.push(("/test".to_string(), "data".to_string()));
state.udp.stack.push("udp_data".to_string());
state.silence();
assert!(state.stack.is_empty());
assert!(state.mono_stack.iter().all(|s| s.is_none()));
assert!(state.cc_stack.is_empty());
assert!(state.osc.stack.is_empty());
assert!(state.udp.stack.is_empty());
}
#[test]
fn test_midi_state_run_clears_transient_stacks() {
let mut state = MidiState::new();
state.cc_stack.push(MidiMessage::Pb(MidiPb {
channel: 0,
lsb: 0,
msb: 0,
}));
state
.osc
.stack
.push(("/path".to_string(), "body".to_string()));
state.udp.stack.push("datagram".to_string());
state.flush();
assert!(state.cc_stack.is_empty());
assert!(state.osc.stack.is_empty());
assert!(state.udp.stack.is_empty());
}
}