use midir::{MidiInput, MidiOutput, MidiOutputConnection};
use std::net::UdpSocket;
#[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)]
pub enum MidiMessage {
Cc(MidiCc),
Pb(MidiPb),
}
pub struct MidiState {
pub out: Option<MidiOutputConnection>,
pub stack: Vec<MidiNote>,
pub mono_stack: [Option<MidiNote>; 16],
pub cc_stack: Vec<MidiMessage>,
pub osc_stack: Vec<(String, String)>,
pub udp_stack: Vec<String>,
pub cc_offset: u8,
pub device_name: String,
pub input_device_name: String,
pub output_index: i32,
pub input_index: i32,
pub udp_socket: Option<UdpSocket>,
pub udp_port: u16,
}
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("udp_port", &self.udp_port)
.field("stack_len", &self.stack.len())
.field("cc_stack_len", &self.cc_stack.len())
.finish_non_exhaustive()
}
}
impl MidiState {
pub fn new() -> Self {
let mut state = Self {
out: None,
stack: Vec::new(),
mono_stack: std::array::from_fn(|_| None),
cc_stack: Vec::new(),
osc_stack: Vec::new(),
udp_stack: Vec::new(),
cc_offset: 64,
device_name: String::from("No Midi Device"),
input_device_name: String::from("No Input Device"),
output_index: -1,
input_index: -1,
udp_socket: UdpSocket::bind("0.0.0.0:0").ok(),
udp_port: 49161,
};
state.select_next_output();
state
}
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");
self.out = None;
return;
}
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"));
self.out = midi.connect(port, "o2-output").ok();
}
}
pub fn select_next_input(&mut self) {
if let Ok(midi) = MidiInput::new("o2") {
let ports = midi.ports();
if ports.is_empty() {
self.input_index = -1;
self.input_device_name = String::from("No Input Device");
return;
}
self.input_index = (self.input_index + 1) % ports.len() as i32;
let port = &ports[self.input_index as usize];
self.input_device_name = midi
.port_name(port)
.unwrap_or_else(|_| String::from("Unknown Device"));
}
}
pub fn run(&mut self) {
if let Some(conn) = self.out.as_mut() {
self.stack.retain_mut(|note| {
if !note.is_played {
let _ = conn.send(&[0x90 + note.channel, note.note_id, note.velocity]);
note.is_played = true;
}
if note.length < 1 {
let _ = conn.send(&[0x80 + 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 {
let _ = conn.send(&[0x80 + note.channel, note.note_id, 0]);
}
*slot = None;
continue;
}
if !note.is_played {
let _ = conn.send(&[0x90 + note.channel, note.note_id, note.velocity]);
note.is_played = true;
}
note.length = note.length.saturating_sub(1);
}
}
for msg in &self.cc_stack {
match msg {
MidiMessage::Cc(cc) => {
let knob_val = self.cc_offset.saturating_add(cc.knob).min(127);
let _ = conn.send(&[0xB0 + cc.channel, knob_val, cc.value]);
}
MidiMessage::Pb(pb) => {
let _ = conn.send(&[0xE0 + pb.channel, pb.lsb, pb.msb]);
}
}
}
} else {
self.stack.retain_mut(|n| {
if n.length < 1 {
false
} else {
n.length = n.length.saturating_sub(1);
true
}
});
for slot in self.mono_stack.iter_mut() {
if let Some(note) = slot {
if note.length < 1 {
*slot = None;
continue;
}
note.length = note.length.saturating_sub(1);
}
}
}
if let Some(sock) = &self.udp_socket {
for msg in &self.udp_stack {
let _ = sock.send_to(msg.as_bytes(), ("127.0.0.1", self.udp_port));
}
}
self.cc_stack.clear();
self.osc_stack.clear();
self.udp_stack.clear();
}
pub fn silence(&mut self) {
if let Some(conn) = &mut self.out {
for note in &self.stack {
if note.is_played {
let _ = conn.send(&[0x80 + note.channel, note.note_id, 0]);
}
}
for n in self.mono_stack.iter().flatten() {
if n.is_played {
let _ = conn.send(&[0x80 + n.channel, n.note_id, 0]);
}
}
for ch in 0..16 {
let _ = conn.send(&[0xB0 + ch, 123, 0]);
}
}
self.stack.clear();
self.mono_stack = std::array::from_fn(|_| None);
self.cc_stack.clear();
self.osc_stack.clear();
self.udp_stack.clear();
}
pub fn send_pg(&mut self, channel: u8, bank: Option<u8>, sub: Option<u8>, pgm: Option<u8>) {
if let Some(conn) = &mut self.out {
if let Some(b) = bank {
let _ = conn.send(&[0xB0 + channel, 0, b]);
}
if let Some(s) = sub {
let _ = conn.send(&[0xB0 + channel, 32, s]);
}
if let Some(p) = pgm {
let _ = conn.send(&[0xC0 + channel, p.min(127)]);
}
}
}
pub fn send_clock_start(&mut self) {
if let Some(conn) = self.out.as_mut() {
let _ = conn.send(&[0xFA]);
}
}
pub fn send_clock_stop(&mut self) {
if let Some(conn) = self.out.as_mut() {
let _ = conn.send(&[0xFC]);
}
}
}
impl Default for MidiState {
fn default() -> Self {
Self::new()
}
}
#[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);
if state.out.is_some() {
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.run();
assert!(state.cc_stack.is_empty());
assert!(state.osc_stack.is_empty());
assert!(state.udp_stack.is_empty());
}
}