From 2a01c5ff97ad014bfb9a79d0c8b1c5a3e7522282 Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya Date: Thu, 6 Mar 2025 21:32:23 -0500 Subject: [PATCH] works for now, save --- Cargo.toml | 4 +- src/bin/main.rs | 89 +++++++++++++++++++++++++++++++++------ src/midi.rs | 13 ++++++ src/typing.rs | 17 +++++++- src/ui.rs | 110 +++++++++++++++++++++++++++++++++++------------- 5 files changed, 187 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 41522b8..0cde161 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/bin/main.rs b/src/bin/main.rs index 1a4f07c..64d44df 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -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:?}"), } diff --git a/src/midi.rs b/src/midi.rs index a67891c..0a8d925 100644 --- a/src/midi.rs +++ b/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)] diff --git a/src/typing.rs b/src/typing.rs index 8e83046..b5e02b8 100644 --- a/src/typing.rs +++ b/src/typing.rs @@ -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) { 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 { 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 { + 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::*; diff --git a/src/ui.rs b/src/ui.rs index 072ae96..4799247 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -23,6 +23,7 @@ pub struct DisplayState { pub max_messages: usize, pub only_note_on_off: Arc, pub show_raw: Arc, + 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, + messages: Vec, + 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) {