266 lines
8.1 KiB
Rust
266 lines
8.1 KiB
Rust
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<Mutex<[[[u8; 128]; 16]; MAX_MIDI_DEVICES]>>;
|
|
|
|
/// 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<MidiDeviceInfo> {
|
|
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<MidiDeviceInfo> {
|
|
Vec::new()
|
|
}
|
|
|
|
#[cfg(feature = "cli")]
|
|
pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
|
|
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<MidiDeviceInfo> {
|
|
Vec::new()
|
|
}
|
|
|
|
pub struct MidiState {
|
|
#[cfg(feature = "cli")]
|
|
output_conns: [Option<midir::MidiOutputConnection>; MAX_MIDI_OUTPUTS],
|
|
#[cfg(feature = "cli")]
|
|
input_conns: [Option<midir::MidiInputConnection<(CcMemoryInner, usize)>>; MAX_MIDI_INPUTS],
|
|
pub selected_outputs: [Option<usize>; MAX_MIDI_OUTPUTS],
|
|
pub selected_inputs: [Option<usize>; 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]);
|
|
}
|
|
}
|