Skip to content

Instantly share code, notes, and snippets.

@TimovNiedek
Last active March 11, 2023 14:12
Show Gist options
  • Save TimovNiedek/2fffef4ea5ae503a1c90301d26db035b to your computer and use it in GitHub Desktop.
Save TimovNiedek/2fffef4ea5ae503a1c90301d26db035b to your computer and use it in GitHub Desktop.
This is an implementation of using MIDI messages to control values in the nannou model. The MIDI connection is running on a separate thread per device. Custom devices can be added by extending midi.rs. Tested with nannou = "0.15", midir = "0.7.0".
use nannou::prelude::*;
mod midi;
use std::sync::mpsc;
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
_window: window::Id,
minilogue_data: mpsc::Receiver<midi::MidiMessage>,
minilogue_notes: Vec<midi::PlayedNote>,
alpha_ctrl: f32,
}
fn model(app: &App) -> Model {
let _window = app.new_window().view(view).build().unwrap();
let minilogue_data = midi::make_thread_for_device(midi::MidiDevice::MINILOGUEXD);
let minilogue_notes = vec![];
let alpha_ctrl = 1.0;
Model {
_window,
minilogue_data,
minilogue_notes,
alpha_ctrl,
}
}
fn update(_app: &App, model: &mut Model, _update: Update) {
let mut done = false;
while !done {
let midi_result = model.minilogue_data.try_recv();
let (midi_cc, midi_note) = midi::parse_midi_event(&midi_result);
let has_cc = match midi_cc {
Some(c) => {
let value_mapped = map_range(c.value, 0, 127, 0.0, 1.0);
println!("Controller change: {} to value {}", c.controller_num, c.value);
if c.controller_num == 43 {
// Minilogue Cutoff Control
model.alpha_ctrl = value_mapped;
}
true
},
None => {false},
};
midi::update_model_notes(&mut model.minilogue_notes, &midi_note);
let has_note = match midi_note {
Some(n) => {
println!("Note event: {}, note value {}, velocity {}", n.event_type, n.note_value, n.velocity);
true
},
None => {false}
};
done = !has_cc && !has_note;
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
let boundary = app.window_rect();
if app.elapsed_frames() == 1 {
draw.background().color(BLACK);
}
for note in model.minilogue_notes.iter() {
let note_x = map_range(note.note_value as f32, 24.0, 108.0, boundary.left(), boundary.right());
draw.ellipse().w_h(100.0, 100.0).x_y(note_x, 0.0).color(rgba(1.0, 0.0, 0.0, model.alpha_ctrl));
}
draw.rect().w_h(boundary.right() - boundary.left(), boundary.top() - boundary.bottom()).color(srgba(0.0,0.0,0.0, 0.1));
draw.to_frame(app, &frame).unwrap();
}
extern crate midir;
use midir::{MidiInput, MidiInputPort, Ignore};
use std::error::Error;
use std::thread;
use std::sync::mpsc;
use std::fmt;
pub struct MidiMessage {
status: u8,
data_1: u8,
data_2: u8,
}
pub enum MidiNoteEventType {
ON,
OFF
}
impl fmt::Display for MidiNoteEventType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MidiNoteEventType::ON => {
write!(f, "ON")
}
MidiNoteEventType::OFF => {
write!(f, "OFF")
}
}
}
}
pub struct MidiNoteEvent {
pub event_type: MidiNoteEventType,
pub note_value: u8,
pub velocity: u8,
}
pub struct MidiControlEvent {
pub controller_num: u8,
pub value: u8,
}
pub struct PlayedNote {
pub note_value: u8,
pub note_velocity: u8,
}
impl PartialEq for PlayedNote
{
fn eq(&self, other: &Self) -> bool {
self.note_value == other.note_value
}
}
#[derive(PartialEq)]
pub enum MidiDevice {
// Add enum option for specific devices here.
MICROBRUTE,
MICROFREAK,
MINILOGUEXD,
OXYGEN49,
LAUNCHPADPRO,
LOOPMIDI,
}
pub fn midi_device_from_port_name(port_name: &String) -> Option<MidiDevice> {
match &port_name[..] {
// Add mapping from port name to midi device here. Device name may vary based on edition and setup; run init_midi once to show available input ports.
"Oxygen 49" => Some(MidiDevice::OXYGEN49),
"Arturia MicroFreak" => Some(MidiDevice::MICROFREAK),
"MicroBrute" => Some(MidiDevice::MICROBRUTE),
"MIDIIN2 (minilogue xd)" => Some(MidiDevice::MINILOGUEXD),
"MIDIIN2 (Launchpad Pro)" => Some(MidiDevice::LAUNCHPADPRO),
"loopMIDI Port" => Some(MidiDevice::LOOPMIDI),
_ => None
}
}
pub fn init_midi(device: MidiDevice) -> Result<(MidiInput, MidiInputPort), Box<dyn Error>> {
let mut midi_in = MidiInput::new("midir reading input")?;
midi_in.ignore(Ignore::None);
let in_ports = midi_in.ports();
println!("\nAvailable input ports:");
for (i, p) in in_ports.iter().enumerate() {
println!("{}: {}", i, midi_in.port_name(p).unwrap());
}
// Get an input port
let mut port_index = 0;
let in_port = loop {
let port = in_ports.get(port_index).unwrap();
let port_name = midi_in.port_name(&port).unwrap();
let port_device = midi_device_from_port_name(&port_name);
match port_device {
Some(d) => {
if d == device {
break port;
}
},
_ => {}
}
port_index += 1;
};
println!("\nOpening connection for port {}", port_index);
Ok((midi_in, in_port.clone()))
}
pub fn make_thread_for_device(device: MidiDevice) -> mpsc::Receiver<MidiMessage> {
let (talk_to_nannou, data_from_my_thread) = mpsc::channel();
thread::spawn(move || {
match init_midi(device) {
Ok((midi_in, in_port)) => {
let _conn_in = midi_in.connect(&in_port, "midir-read-input", move |_stamp, message, _| {
let status = message[0];
match message.len() {
2 => {
// println!("MIDI message length of 2 currently not implemented.");
},
3 => {
let data1 = message[1];
let data2 = message[2];
// let msg = format!("{}: [{:X}, {:X}, {:X}] (len = {})", stamp, status, data1, data2, message.len());
let midi_msg = MidiMessage{status: status, data_1: data1, data_2:data2};
let result = talk_to_nannou.send(midi_msg);
match result {
Ok(_) => {},
Err(e) => println!("Error sending msg: {}", e)
}
},
_ => {
// println!("MIDI message length of {} currently not implemented.", message.len());
}
};
}, ());
thread::park();
},
Err(_) => {},
}
});
return data_from_my_thread;
}
pub fn parse_midi_cc_message(msg: &MidiMessage) -> Option<MidiControlEvent> {
if msg.status >= 0xB0 && msg.status < 0xC0 {
// control message
return Some(MidiControlEvent{controller_num: msg.data_1, value: msg.data_2});
}
return None;
}
pub fn parse_midi_note_message(msg: &MidiMessage) -> Option<MidiNoteEvent> {
if msg.status >= 0x80 && msg.status < 0x90 {
return Some(MidiNoteEvent{event_type: MidiNoteEventType::OFF, note_value: msg.data_1, velocity: msg.data_2});
} else if msg.status >= 0x90 && msg.status < 0xA0 {
return Some(MidiNoteEvent{event_type: MidiNoteEventType::ON, note_value: msg.data_1, velocity: msg.data_2});
}
return None;
}
pub fn parse_midi_event(midi_result: &Result<MidiMessage, mpsc::TryRecvError>) -> (Option<MidiControlEvent>, Option<MidiNoteEvent>) {
match midi_result {
Ok(v) => {
return (parse_midi_cc_message(&v), parse_midi_note_message(&v))
},
Err(_) => {return (None, None)},
}
}
pub fn update_model_notes(model_notes: &mut Vec<PlayedNote>, note_event: &Option<MidiNoteEvent>) {
match note_event {
Some(e) => {
match e.event_type {
MidiNoteEventType::ON => {
let played_note = PlayedNote{note_value: e.note_value, note_velocity: e.velocity};
if e.velocity > 0 {
// If note velocity is above 0, add it to the played notes list
if !model_notes.contains(&played_note) {
model_notes.push(played_note);
}
} else {
// If note velocity is 0, find whether the note was already played and remove it
// Note: this was added as a workaround for the Launchpad Pro
// which appears to send note on events with velocity 0 instead of note off events.
let position = model_notes.iter().position(|x| x == &played_note);
match position {
Some(p) => {
model_notes.remove(p);
},
None => {}
}
}
},
MidiNoteEventType::OFF => {
let played_note = PlayedNote{note_value: e.note_value, note_velocity: e.velocity};
let position = model_notes.iter().position(|x| x == &played_note);
match position {
Some(p) => {
model_notes.remove(p);
},
None => {}
}
}
}
}
None => {}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment