Skip to content

Instantly share code, notes, and snippets.

@selenologist
Last active December 29, 2021 06:13
Show Gist options
  • Save selenologist/7c36c7446c881a4cb721d4bc424844ac to your computer and use it in GitHub Desktop.
Save selenologist/7c36c7446c881a4cb721d4bc424844ac to your computer and use it in GitHub Desktop.
Crappy paraphonic midi router for keystep pro
[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"
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