use parking_lot::Mutex; use std::sync::Arc; use crate::model::CcAccess; pub const MAX_MIDI_OUTPUTS: usize = 4; pub const MAX_MIDI_INPUTS: usize = 4; pub const MAX_MIDI_DEVICES: usize = 4; /// Raw CC memory storage type type CcMemoryInner = Arc>; /// CC memory storage: [device][channel][cc_number] -> value /// Wrapped in a newtype to implement CcAccess (orphan rule) #[derive(Clone)] pub struct CcMemory(CcMemoryInner); impl CcMemory { pub fn new() -> Self { Self(Arc::new(Mutex::new([[[0u8; 128]; 16]; MAX_MIDI_DEVICES]))) } #[cfg(feature = "cli")] fn inner(&self) -> &CcMemoryInner { &self.0 } #[allow(dead_code)] // used by integration tests pub fn set_cc(&self, device: usize, channel: usize, cc: usize, value: u8) { let mut mem = self.0.lock(); mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value; } } impl Default for CcMemory { fn default() -> Self { Self::new() } } impl CcAccess for CcMemory { fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8 { let mem = self.0.lock(); mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] } } #[derive(Clone, Debug)] pub struct MidiDeviceInfo { pub name: String, } #[cfg(feature = "cli")] pub fn list_midi_outputs() -> Vec { let Ok(midi_out) = midir::MidiOutput::new("cagire-probe") else { return Vec::new(); }; midi_out .ports() .iter() .filter_map(|port| { midi_out .port_name(port) .ok() .map(|name| MidiDeviceInfo { name }) }) .collect() } #[cfg(not(feature = "cli"))] pub fn list_midi_outputs() -> Vec { Vec::new() } #[cfg(feature = "cli")] pub fn list_midi_inputs() -> Vec { let Ok(midi_in) = midir::MidiInput::new("cagire-probe") else { return Vec::new(); }; midi_in .ports() .iter() .filter_map(|port| { midi_in .port_name(port) .ok() .map(|name| MidiDeviceInfo { name }) }) .collect() } #[cfg(not(feature = "cli"))] pub fn list_midi_inputs() -> Vec { Vec::new() } pub struct MidiState { #[cfg(feature = "cli")] output_conns: [Option; MAX_MIDI_OUTPUTS], #[cfg(feature = "cli")] input_conns: [Option>; MAX_MIDI_INPUTS], pub selected_outputs: [Option; MAX_MIDI_OUTPUTS], pub selected_inputs: [Option; MAX_MIDI_INPUTS], pub cc_memory: CcMemory, } impl Default for MidiState { fn default() -> Self { Self::new() } } impl MidiState { pub fn new() -> Self { Self { #[cfg(feature = "cli")] output_conns: [None, None, None, None], #[cfg(feature = "cli")] input_conns: [None, None, None, None], selected_outputs: [None; MAX_MIDI_OUTPUTS], selected_inputs: [None; MAX_MIDI_INPUTS], cc_memory: CcMemory::new(), } } #[cfg(feature = "cli")] pub fn connect_output(&mut self, slot: usize, port_index: usize) -> Result<(), String> { if slot >= MAX_MIDI_OUTPUTS { return Err("Invalid output slot".to_string()); } let midi_out = midir::MidiOutput::new(&format!("cagire-out-{slot}")).map_err(|e| e.to_string())?; let ports = midi_out.ports(); let port = ports.get(port_index).ok_or("MIDI output port not found")?; let conn = midi_out .connect(port, &format!("cagire-midi-out-{slot}")) .map_err(|e| e.to_string())?; self.output_conns[slot] = Some(conn); self.selected_outputs[slot] = Some(port_index); Ok(()) } #[cfg(not(feature = "cli"))] pub fn connect_output(&mut self, _slot: usize, _port_index: usize) -> Result<(), String> { Ok(()) } #[cfg(feature = "cli")] pub fn disconnect_output(&mut self, slot: usize) { if slot < MAX_MIDI_OUTPUTS { self.output_conns[slot] = None; self.selected_outputs[slot] = None; } } #[cfg(not(feature = "cli"))] pub fn disconnect_output(&mut self, slot: usize) { if slot < MAX_MIDI_OUTPUTS { self.selected_outputs[slot] = None; } } #[cfg(feature = "cli")] pub fn connect_input(&mut self, slot: usize, port_index: usize) -> Result<(), String> { if slot >= MAX_MIDI_INPUTS { return Err("Invalid input slot".to_string()); } let midi_in = midir::MidiInput::new(&format!("cagire-in-{slot}")).map_err(|e| e.to_string())?; let ports = midi_in.ports(); let port = ports.get(port_index).ok_or("MIDI input port not found")?; let cc_mem = Arc::clone(self.cc_memory.inner()); let conn = midi_in .connect( port, &format!("cagire-midi-in-{slot}"), move |_timestamp, message, (cc_mem, slot)| { if message.len() >= 3 { let status = message[0]; let data1 = message[1] as usize; let data2 = message[2]; if (status & 0xF0) == 0xB0 && data1 < 128 { let channel = (status & 0x0F) as usize; let mut mem = cc_mem.lock(); mem[*slot][channel][data1] = data2; } } }, (cc_mem, slot), ) .map_err(|e| e.to_string())?; self.input_conns[slot] = Some(conn); self.selected_inputs[slot] = Some(port_index); Ok(()) } #[cfg(not(feature = "cli"))] pub fn connect_input(&mut self, _slot: usize, _port_index: usize) -> Result<(), String> { Ok(()) } #[cfg(feature = "cli")] pub fn disconnect_input(&mut self, slot: usize) { if slot < MAX_MIDI_INPUTS { self.input_conns[slot] = None; self.selected_inputs[slot] = None; } } #[cfg(not(feature = "cli"))] pub fn disconnect_input(&mut self, slot: usize) { if slot < MAX_MIDI_INPUTS { self.selected_inputs[slot] = None; } } #[cfg(feature = "cli")] fn send_message(&mut self, device: u8, message: &[u8]) { let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1); if let Some(conn) = &mut self.output_conns[slot] { let _ = conn.send(message); } } #[cfg(not(feature = "cli"))] fn send_message(&mut self, _device: u8, _message: &[u8]) {} pub fn send_note_on(&mut self, device: u8, channel: u8, note: u8, velocity: u8) { let status = 0x90 | (channel & 0x0F); self.send_message(device, &[status, note & 0x7F, velocity & 0x7F]); } pub fn send_note_off(&mut self, device: u8, channel: u8, note: u8) { let status = 0x80 | (channel & 0x0F); self.send_message(device, &[status, note & 0x7F, 0]); } pub fn send_cc(&mut self, device: u8, channel: u8, cc: u8, value: u8) { let status = 0xB0 | (channel & 0x0F); self.send_message(device, &[status, cc & 0x7F, value & 0x7F]); } pub fn send_pitch_bend(&mut self, device: u8, channel: u8, value: u16) { let status = 0xE0 | (channel & 0x0F); let lsb = (value & 0x7F) as u8; let msb = ((value >> 7) & 0x7F) as u8; self.send_message(device, &[status, lsb, msb]); } pub fn send_pressure(&mut self, device: u8, channel: u8, value: u8) { let status = 0xD0 | (channel & 0x0F); self.send_message(device, &[status, value & 0x7F]); } pub fn send_program_change(&mut self, device: u8, channel: u8, program: u8) { let status = 0xC0 | (channel & 0x0F); self.send_message(device, &[status, program & 0x7F]); } pub fn send_realtime(&mut self, device: u8, msg: u8) { self.send_message(device, &[msg]); } }