Last active
December 29, 2021 06:13
-
-
Save selenologist/7c36c7446c881a4cb721d4bc424844ac to your computer and use it in GitHub Desktop.
Crappy paraphonic midi router for keystep pro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "ksp-paraphonic" | |
version = "0.1.0" | |
edition = "2018" | |
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |
[dependencies] | |
wmidi = "4.0.6" | |
midir = "0.7.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use midir::{MidiInput, MidiInputPort, MidiOutput, MidiOutputPort, MidiOutputConnection}; | |
use wmidi::{MidiMessage, Channel, Note, Velocity}; | |
use std::convert::TryFrom; | |
struct OutputVoice { | |
active: bool, | |
current: Note, | |
channel: Channel | |
} | |
struct State { | |
input_channel: Channel, | |
held: [(u64, Velocity); 128], // when note was held and velocity, or zero | |
voices: Vec<OutputVoice>, | |
conn_out: MidiOutputConnection | |
} | |
impl State { | |
pub fn note_on(&mut self, time: u64, note: Note, velocity: Velocity){ | |
self.held[note as usize] = (time, velocity); | |
self.update(); | |
} | |
pub fn note_off(&mut self, _time: u64, note: Note, velocity: Velocity){ | |
self.held[note as usize] = (0, velocity); | |
self.update(); | |
} | |
pub fn update(&mut self){ | |
let mut held_notes = | |
self.held | |
.iter() | |
.enumerate() | |
.filter_map( | |
|(note, &(time, velo))| { | |
if time > 0 { | |
Some((time, Note::from_u8_lossy(note as u8), velo)) | |
} | |
else { | |
None | |
} | |
}) | |
.collect::<Vec<_>>(); | |
held_notes.sort_by(|&(a_time, _, _), &(b_time, _, _)| a_time.cmp(&b_time).reverse()); | |
// get most recent notes for each output voice | |
let active_notes = held_notes.into_iter().take(self.voices.len()).rev().collect::<Vec<_>>(); | |
// how many notes will currently be activated | |
let num_active = active_notes.len(); | |
println!("num active: {}", num_active); | |
if num_active == 0 { | |
for voice in self.voices.iter_mut(){ | |
if voice.active { | |
Self::send_midi(&mut self.conn_out, &MidiMessage::NoteOff( | |
voice.channel, | |
voice.current, | |
self.held[voice.current as usize].1)).unwrap(); | |
} | |
voice.active = false; | |
} | |
} | |
else { | |
// voices to alloc to current note | |
let voices_alloc = self.voices.len() / num_active; | |
let mut current_voice = 0; | |
'outer: for (_, note, velo) in active_notes.iter() { | |
for _ in 0..voices_alloc { | |
// only send new note if there's actually a change | |
if !self.voices[current_voice].active || | |
self.voices[current_voice].current != *note { | |
// send new note first to achieve a tie | |
Self::send_midi(&mut self.conn_out, &MidiMessage::NoteOn(self.voices[current_voice].channel, *note, *velo)).unwrap(); | |
// turn off any previous note | |
if self.voices[current_voice].active { | |
Self::send_midi(&mut self.conn_out, &MidiMessage::NoteOff( | |
self.voices[current_voice].channel, | |
self.voices[current_voice].current, | |
self.held[self.voices[current_voice].current as usize].1)).unwrap(); | |
} | |
self.voices[current_voice].current = *note; | |
self.voices[current_voice].active = true; | |
} | |
current_voice += 1; | |
if current_voice >= self.voices.len() { | |
break 'outer; | |
} | |
} | |
} | |
} | |
} | |
pub fn send_midi(conn_out: &mut MidiOutputConnection, message: &MidiMessage) -> Result<(), midir::SendError>{ | |
println!("Sending {:?}", message); | |
let mut bytes = vec![0u8; message.bytes_size()]; | |
message.copy_to_slice(bytes.as_mut_slice()).unwrap(); | |
conn_out.send(&bytes) | |
} | |
} | |
fn on_midi(time: u64, message_bytes: &[u8], state: &mut State) { | |
match wmidi::MidiMessage::try_from(message_bytes) { | |
Ok(message) => { | |
println!("{} {:?}", time, message); | |
match message { | |
MidiMessage::NoteOn(channel, note, velocity) => { | |
if channel == state.input_channel { | |
state.note_on(time, note, velocity); | |
} | |
}, | |
MidiMessage::NoteOff(channel, note, velocity) => { | |
if channel == state.input_channel { | |
state.note_off(time, note, velocity); | |
} | |
}, | |
_ => {} | |
} | |
} | |
Err(e) => { | |
println!("Failed to read MIDI message {:?}", e); | |
} | |
} | |
} | |
fn main() { | |
let output = MidiOutput::new("KSP Paraphonic Adapter Out").expect("Can't create output"); | |
let output_port = output.ports().into_iter() | |
.find_map(|port| | |
if let Ok(s) = output.port_name(&port){ | |
println!("out port {}", s); | |
if s.contains("KeyStep Pro") { | |
println!("found KSP"); | |
Some(port) | |
} | |
else { | |
None | |
} | |
} | |
else { | |
None | |
}) | |
.expect("No KSP output found"); | |
let conn_out = output.connect(&output_port, "KSP Paraphonic Out").expect("Can't connect output"); | |
/*use midir::os::unix::{VirtualInput, VirtualOutput}; | |
let conn_out = output.create_virtual("KSP Paraphonic Out").expect("Can't create output");*/ | |
let state = State { | |
input_channel: Channel::Ch1, | |
held: [(0, Velocity::MIN); 128], | |
voices: vec![ | |
OutputVoice{ | |
active: false, | |
current: Note::CMinus1, | |
channel: Channel::Ch3 | |
}, | |
OutputVoice{ | |
active: false, | |
current: Note::CMinus1, | |
channel: Channel::Ch4 | |
}, | |
OutputVoice{ | |
active: false, | |
current: Note::CMinus1, | |
channel: Channel::Ch5 | |
}, | |
], | |
conn_out | |
}; | |
let input = MidiInput::new("KSP Paraphonic Adapter In").expect("Can't create input"); | |
let input_port = input.ports().into_iter().find_map(|port| { | |
if let Ok(s) = input.port_name(&port){ | |
println!("in port {}", s); | |
if s.contains("KeyStep Pro") { | |
println!("found KSP"); | |
Some(port) | |
} | |
else { | |
None | |
} | |
} | |
else { | |
None | |
}}) | |
.expect("Can't find KSP input"); | |
let input_conn = input.connect(&input_port, "KSP Paraphonic In", on_midi, state).expect("failed to connect input"); | |
//let input_conn = input.create_virtual("KSP Paraphonic In", on_midi, state).expect("Failed to connect input"); | |
// block on a channel with no other writers in order to sleep forever | |
// (Ctrl-C should still terminate the program as normal) | |
let (tx, rx) = std::sync::mpsc::channel(); | |
rx.recv().unwrap(); | |
// this point is now unreachable. 'Use' tx so it won't be optimised out / dropped early. | |
tx.send(()).unwrap(); | |
input_conn.close(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment