works for now, save

This commit is contained in:
Nicole Tietz-Sokolskaya 2025-03-06 21:32:23 -05:00
parent dfb4c93e4e
commit 2a01c5ff97
5 changed files with 187 additions and 46 deletions

View file

@ -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"

View file

@ -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:?}"),
}

View file

@ -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)]

View file

@ -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
View file

@ -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) {