works for now, save
This commit is contained in:
parent
dfb4c93e4e
commit
2a01c5ff97
5 changed files with 187 additions and 46 deletions
|
@ -6,8 +6,8 @@ edition = "2021"
|
|||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
crossbeam = "0.8.4"
|
||||
eframe = "0"
|
||||
egui = "0"
|
||||
eframe = "0.30.0"
|
||||
egui = { version = "0.30.0", features = ["log"] }
|
||||
enigo = { version = "0.2.1", features = ["serde"] }
|
||||
hex = "0.4.3"
|
||||
midir = "0.10.0"
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use enigo::{Enigo, Keyboard, Settings};
|
||||
use enigo::{Enigo, Key, Keyboard, Settings};
|
||||
use midi_keys::{
|
||||
log::load_raw_log,
|
||||
midi::daemon::Category,
|
||||
|
@ -14,6 +14,8 @@ use midi_keys::{
|
|||
};
|
||||
use midir::{MidiInput, MidiInputPort};
|
||||
|
||||
const MIN_NOTE_DURATION_MS: u64 = 5;
|
||||
|
||||
fn main() {
|
||||
let (dbg_send, dbg_recv) = crossbeam::channel::unbounded();
|
||||
let (ws_send, ws_recv) = crossbeam::channel::unbounded();
|
||||
|
@ -53,24 +55,85 @@ fn main() {
|
|||
let mut steps = 0;
|
||||
let mut state = TypingState::new(7);
|
||||
let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
||||
|
||||
let mut current_note = None;
|
||||
|
||||
loop {
|
||||
match ws_recv.recv() {
|
||||
Ok((_cid, _ts, m)) => {
|
||||
if let Some((note, _velocity)) = m.note_on_values() {
|
||||
if let Some(degree) = midi_to_scale_degree(60, note) {
|
||||
steps += 1;
|
||||
println!("got degree: {degree}, steps: {steps}, state: {state:?}");
|
||||
state.enter(degree);
|
||||
Ok((_cid, ts, m)) => {
|
||||
let note_on = m.note_on_values();
|
||||
let note_off = m.note_off_values();
|
||||
|
||||
if steps % 3 == 0 {
|
||||
let (b, c) = state.emit();
|
||||
println!("got {b}, {c:?}");
|
||||
if let Some(c) = c {
|
||||
enigo.text(&format!("{c}"));
|
||||
}
|
||||
if !note_on.is_some() && !note_off.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("{m:?}, {current_note:?}");
|
||||
|
||||
if current_note.is_some() {
|
||||
let (note, velocity, prev_ts) = match current_note {
|
||||
Some((n, v, t)) => (n, v, t),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let duration = (ts - prev_ts) / 1000;
|
||||
if duration < MIN_NOTE_DURATION_MS {
|
||||
current_note = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
let scale_degree = match midi_to_scale_degree(60, note) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
current_note = None;
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
steps += 1;
|
||||
state.enter(scale_degree);
|
||||
|
||||
println!("here: {steps}, {scale_degree}, {current_note:?}, {m:?}");
|
||||
|
||||
if steps % 2 == 0 {
|
||||
if velocity > 20 {
|
||||
state.set_bits(64);
|
||||
}
|
||||
|
||||
let (b, c) = state.emit();
|
||||
println!("got {b}, {c:?}");
|
||||
|
||||
if let Some(c) = c {
|
||||
enigo.text(&format!("{c}")).unwrap();
|
||||
} else if b == 120 {
|
||||
enigo.key(Key::Backspace, enigo::Direction::Click).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((note, velocity)) = note_on {
|
||||
current_note = Some((note, velocity, ts));
|
||||
} else if let Some(_) = note_off {
|
||||
current_note = None;
|
||||
}
|
||||
|
||||
//if let Some((note, velocity)) = m.note_on_values() {
|
||||
// if let Some(degree) = midi_to_scale_degree(60, note) {
|
||||
// steps += 1;
|
||||
// println!("got degree: {degree}, steps: {steps}, state: {state:?}");
|
||||
// state.enter(degree);
|
||||
|
||||
// if steps % 3 == 0 {
|
||||
// let (b, c) = state.emit();
|
||||
// println!("got {b}, {c:?}");
|
||||
// if let Some(c) = c {
|
||||
// enigo.text(&format!("{c}")).unwrap();
|
||||
// } else if b == 255 {
|
||||
// enigo.key(Key::Backspace, enigo::Direction::Click).unwrap();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
Err(err) => println!("err(ws): {err:?}"),
|
||||
}
|
||||
|
|
13
src/midi.rs
13
src/midi.rs
|
@ -23,6 +23,19 @@ impl Message {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the note value and velocity for any NoteOff messages.
|
||||
/// This is a convenience function to let us avoid a match in all the places
|
||||
/// we need to extract this.
|
||||
pub fn note_off_values(&self) -> Option<(u8, u8)> {
|
||||
match &self.parsed {
|
||||
ParsedMessage::Voice(VoiceMessage {
|
||||
category: VoiceCategory::NoteOff { note, velocity },
|
||||
..
|
||||
}) => Some((*note, *velocity)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
|
|
|
@ -14,11 +14,15 @@ impl TypingState {
|
|||
self.current = self.current.saturating_mul(self.base).saturating_add(value);
|
||||
}
|
||||
|
||||
pub fn set_bits(&mut self, mask: u8) {
|
||||
self.current |= mask;
|
||||
}
|
||||
|
||||
/// Decode the current accumulator as a character, or None if it's invalid.
|
||||
/// This resets the accumulator to 0!
|
||||
pub fn emit(&mut self) -> (u8, Option<char>) {
|
||||
let x = self.current;
|
||||
let c = decode(x);
|
||||
let c = decode_alt(x);
|
||||
|
||||
self.current = 0;
|
||||
|
||||
|
@ -86,6 +90,17 @@ pub fn decode(x: u8) -> Option<char> {
|
|||
Some(char::from(c))
|
||||
}
|
||||
|
||||
/// Converts a number to its corresponding characer for use in text entry.
|
||||
/// But louder.
|
||||
pub fn decode_alt(x: u8) -> Option<char> {
|
||||
let c = match x {
|
||||
0..=25 => 'a' as u8 + x,
|
||||
64..=89 => 'A' as u8 + x - 64,
|
||||
_ => return None,
|
||||
};
|
||||
Some(char::from(c))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
110
src/ui.rs
110
src/ui.rs
|
@ -23,6 +23,7 @@ pub struct DisplayState {
|
|||
pub max_messages: usize,
|
||||
pub only_note_on_off: Arc<AtomicBool>,
|
||||
pub show_raw: Arc<AtomicBool>,
|
||||
pub selected_tab: Tabs,
|
||||
}
|
||||
|
||||
impl DisplayState {
|
||||
|
@ -34,6 +35,7 @@ impl DisplayState {
|
|||
max_messages: 10_000_usize,
|
||||
only_note_on_off: Arc::new(AtomicBool::new(true)),
|
||||
show_raw: Arc::new(AtomicBool::new(true)),
|
||||
selected_tab: Tabs::Messages,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,48 +114,39 @@ pub fn run(state: DisplayState) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
pub enum Tabs {
|
||||
Messages,
|
||||
WindSynthTyping,
|
||||
}
|
||||
|
||||
struct MidiKeysApp {
|
||||
midi_in: MidiInput,
|
||||
ports: Vec<MidiInputPort>,
|
||||
messages: Vec<CTM>,
|
||||
|
||||
state: DisplayState,
|
||||
|
||||
}
|
||||
|
||||
impl MidiKeysApp {
|
||||
fn new(_cc: &eframe::CreationContext<'_>, state: DisplayState) -> Self {
|
||||
// this is where to hook in for customizing eguji, like fonts and visuals.
|
||||
// this is where to hook in for customizing egui, like fonts and visuals.
|
||||
|
||||
let midi_in: MidiInput =
|
||||
MidiInput::new("midi-keys").expect("could not connect to system MIDI");
|
||||
let ports = vec![];
|
||||
let messages = vec![];
|
||||
|
||||
MidiKeysApp { midi_in, state }
|
||||
MidiKeysApp { midi_in, ports, messages, state }
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for MidiKeysApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
ctx.set_theme(egui::Theme::Light);
|
||||
|
||||
let ports = self.state.midi_input_ports.lock().clone();
|
||||
let messages = self.state.midi_messages.lock().clone();
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Quit").clicked() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
});
|
||||
ui.menu_button("Help", |ui| {
|
||||
if ui.button("About").clicked() {
|
||||
// TODO: implement something
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
pub fn instrument_panel(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::SidePanel::left("instrument_panel").show(ctx, |ui| {
|
||||
ui.heading("Connections");
|
||||
|
||||
for port in ports.iter() {
|
||||
for port in self.ports.iter() {
|
||||
let port_name = self
|
||||
.midi_in
|
||||
.port_name(port)
|
||||
|
@ -171,11 +164,16 @@ impl eframe::App for MidiKeysApp {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn messages_tab(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
self.instrument_panel(ctx, frame);
|
||||
|
||||
let mut only_note_on_off = self.state.only_note_on_off.load(Ordering::Relaxed);
|
||||
let mut show_raw = self.state.show_raw.load(Ordering::Relaxed);
|
||||
|
||||
egui::TopBottomPanel::top("filter_panel").show(ctx, |ui| {
|
||||
|
||||
egui::TopBottomPanel::bottom("filter_panel").show(ctx, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.checkbox(&mut only_note_on_off, "Only note on/off");
|
||||
ui.checkbox(&mut show_raw, "Display raw bytes");
|
||||
|
@ -192,7 +190,7 @@ impl eframe::App for MidiKeysApp {
|
|||
.max_width(f32::INFINITY)
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() {
|
||||
for (idx, (conn, _ts, msg)) in self.messages.iter().rev().enumerate() {
|
||||
if only_note_on_off
|
||||
&& !matches!(
|
||||
&msg.parsed,
|
||||
|
@ -213,7 +211,7 @@ impl eframe::App for MidiKeysApp {
|
|||
{
|
||||
continue;
|
||||
}
|
||||
let port = match ports.iter().find(|p| &p.id() == conn) {
|
||||
let port = match self.ports.iter().find(|p| &p.id() == conn) {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
|
@ -232,7 +230,7 @@ impl eframe::App for MidiKeysApp {
|
|||
|
||||
frame.content_ui.label(RichText::new(port_name).strong());
|
||||
|
||||
//display_midi_message(idx, msg, &mut frame.content_ui, show_raw);
|
||||
display_midi_message(idx, msg, &mut frame.content_ui, show_raw);
|
||||
frame.end(ui);
|
||||
}
|
||||
});
|
||||
|
@ -240,7 +238,59 @@ impl eframe::App for MidiKeysApp {
|
|||
ctx.request_repaint();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
pub fn typing_tab(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
self.instrument_panel(ctx, frame);
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
Grid::new("typing-tab-grid-1").show(ui, |ui| {
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for MidiKeysApp {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
ctx.set_theme(egui::Theme::Light);
|
||||
|
||||
self.ports = self.state.midi_input_ports.lock().clone();
|
||||
self.messages = self.state.midi_messages.lock().clone().into();
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Quit").clicked() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
});
|
||||
ui.menu_button("Help", |ui| {
|
||||
if ui.button("About").clicked() {
|
||||
// TODO: implement something
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("tab_selector").show(ctx, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.radio_value(&mut self.state.selected_tab, Tabs::Messages, "Messages");
|
||||
ui.radio_value(&mut self.state.selected_tab, Tabs::WindSynthTyping, "Typing");
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if self.state.selected_tab == Tabs::Messages {
|
||||
self.messages_tab(ctx, frame);
|
||||
} else if self.state.selected_tab == Tabs::WindSynthTyping {
|
||||
self.typing_tab(ctx, frame);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) {
|
||||
|
|
Loading…
Reference in a new issue