From 0f00f0ef688fc9869980ce56b23237450321a5c8 Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya Date: Wed, 26 Mar 2025 23:10:54 -0400 Subject: [PATCH] made it display the typing state --- Cargo.toml | 1 + src/bin/main.rs | 8 ++--- src/typing.rs | 86 +++++++++++++++++++++++++++++++++++++------------ src/ui.rs | 71 +++++++++++++++++++++++++++++++--------- 4 files changed, 126 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 48a66c9..98282c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ eframe = "0.31.0" egui = { version = "0.31.0", features = ["log"] } enigo = { version = "0.2.1", features = ["serde"] } hex = "0.4.3" +itertools = "0.14.0" midir = "0.10.0" nom = "7.1.3" thiserror = "1.0.63" diff --git a/src/bin/main.rs b/src/bin/main.rs index 3680509..49ef42b 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -10,7 +10,7 @@ use midi_keys::{ midi::daemon::Category, parser::parse_message, typing::{ - midi_to_scale_degree, standard_melodic_mapping, Keystroke, Modifier, Special, TypingState + midi_to_scale_degree, standard_melodic_mapping, Keystroke, Modifier, Special }, ui::{display_state_daemon, DisplayQueues, DisplayState}, }; @@ -36,13 +36,12 @@ fn main() { let _handle = display_state_daemon(queues, state.clone()); println!("started daemon"); - let melodic_mapping = standard_melodic_mapping(); melodic_mapping.print_sequences(); + let typing_state = state.melodic_typing_state.clone(); + std::thread::spawn(move || { - let melodic_mapping = standard_melodic_mapping(); - let mut state = TypingState::new(melodic_mapping); let mut enigo = Enigo::new(&Settings::default()).unwrap(); let mut shift = false; @@ -58,6 +57,7 @@ fn main() { None => continue, }; + let mut state = typing_state.lock(); state.enter(scale_degree); match state.emit() { diff --git a/src/typing.rs b/src/typing.rs index bcbeddb..56797f6 100644 --- a/src/typing.rs +++ b/src/typing.rs @@ -1,9 +1,10 @@ +use itertools::Itertools; use std::collections::HashMap; #[derive(Debug, Clone)] pub enum Decoded { Keystroke(Keystroke), - Partial(Vec, Vec), + Partial(Vec, Vec<(Vec, Keystroke)>), Invalid(Vec), } @@ -47,33 +48,68 @@ impl Mapping { Some(key.clone()) } - pub fn prefix_range(&self, notes: &Vec) -> Option> { - let low = self + pub fn prefix_range(&self, notes: &Vec) -> Option, Keystroke)>> { + // we only want to match prefixes with exactly this length of notes + let valid_seqs: Vec<_> = self .sequences - .partition_point(|(n, _key)| n[..notes.len()] < **notes); - let high = self - .sequences - .partition_point(|(n, _key)| n[..notes.len()] <= **notes); + .iter() + .filter(|(n, _key)| n.starts_with(¬es)) + .cloned() + .collect(); - Some( - self.sequences[low..high] - .iter() - .map(|(_n, key)| key.clone()) - .collect(), - ) + if valid_seqs.is_empty() { + None + } else { + Some(valid_seqs) + } + } + + pub fn collated_remaining(&self, notes: &Vec) -> Option)>> { + let next_sequences = self.prefix_range(notes)?; + + let seqs = self + .sequences + .iter() + .filter(|(n, _key)| n.starts_with(¬es)) + .map(|(n, key)| (*n.get(notes.len()).unwrap(), key.clone())); + + let mut grouped_seqs = Vec::new(); + + for (note, keys) in &seqs.chunk_by(|(n, _key)| *n) { + let all_keys: Vec<_> = keys.map(|(n,k)| k.clone()).collect(); + grouped_seqs.push((note, all_keys)); + } + + // grouped.map(|(n, keys)| (*n, keys.collect())); + // .collect(); + + Some(grouped_seqs) } pub fn print_sequences(&self) { - let notes: Vec<_> = "CDEFGAB".chars().collect(); for (seq, key) in self.sequences.iter() { for deg in seq { - print!("{} ", notes.get(*deg as usize).unwrap()); + print!("{} ", c_scale_note_name(*deg)); } println!("=> {key:?}"); } } } +pub fn c_scale_note_name(degree: u8) -> String { + match degree { + 0 => "C", + 1 => "D", + 2 => "E", + 3 => "F", + 4 => "G", + 5 => "A", + 6 => "B", + _ => "?", + } + .to_string() +} + pub fn standard_melodic_mapping() -> Mapping { let modspecial = [ Keystroke::Modifier(Modifier::Shift), @@ -126,6 +162,14 @@ impl TypingState { decoded } + + pub fn current_notes(&self) -> Vec { + self.current.clone() + } + + pub fn mapping(&self) -> &Mapping { + &self.mapping + } } /// Chunks a u8 into a sequence of values in a given base. @@ -182,17 +226,17 @@ mod tests { #[test] fn encodes_lowercase() { let mapping = standard_melodic_mapping(); - assert_eq!(mapping.encode(Keystroke::Char('a')), Some(vec![0,3])); - assert_eq!(mapping.encode(Keystroke::Char('b')), Some(vec![0,4])); - assert_eq!(mapping.encode(Keystroke::Char('z')), Some(vec![4,0])); + assert_eq!(mapping.encode(Keystroke::Char('a')), Some(vec![0, 3])); + assert_eq!(mapping.encode(Keystroke::Char('b')), Some(vec![0, 4])); + assert_eq!(mapping.encode(Keystroke::Char('z')), Some(vec![4, 0])); } #[test] fn encodes_numbers() { let mapping = standard_melodic_mapping(); - assert_eq!(mapping.encode(Keystroke::Char('0')), Some(vec![4,1])); - assert_eq!(mapping.encode(Keystroke::Char('1')), Some(vec![4,2])); - assert_eq!(mapping.encode(Keystroke::Char('9')), Some(vec![5,3])); + assert_eq!(mapping.encode(Keystroke::Char('0')), Some(vec![4, 1])); + assert_eq!(mapping.encode(Keystroke::Char('1')), Some(vec![4, 2])); + assert_eq!(mapping.encode(Keystroke::Char('9')), Some(vec![5, 3])); } #[test] diff --git a/src/ui.rs b/src/ui.rs index 08b50d9..7a5d020 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -7,11 +7,15 @@ use std::{ thread::JoinHandle, }; +use itertools::Itertools; use crossbeam::{channel::Receiver, select}; use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, ScrollArea, SelectableLabel}; use midir::{MidiInput, MidiInputPort}; -use crate::midi::{daemon::CTM, Message, ParsedMessage, VoiceCategory, VoiceMessage}; +use crate::{ + midi::{daemon::CTM, Message, ParsedMessage, VoiceCategory, VoiceMessage}, + typing::{c_scale_note_name, standard_melodic_mapping, Keystroke, TypingState}, +}; /// State used to display the UI. It's intended to be shared between the /// renderer and the daemon which updates the state. @@ -24,10 +28,15 @@ pub struct DisplayState { pub only_note_on_off: Arc, pub show_raw: Arc, pub selected_tab: Tabs, + pub melodic_typing_state: Arc>, + pub scratchpad: String, } impl DisplayState { pub fn new() -> DisplayState { + let melodic_mapping = standard_melodic_mapping(); + let melodic_typing_state = TypingState::new(melodic_mapping); + DisplayState { midi_input_ports: Arc::new(Mutex::new(vec![])), midi_messages: Arc::new(Mutex::new(VecDeque::new())), @@ -35,7 +44,9 @@ 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, + selected_tab: Tabs::WindSynthTyping, + melodic_typing_state: Arc::new(Mutex::new(melodic_typing_state)), + scratchpad: String::new(), } } @@ -114,7 +125,6 @@ pub fn run(state: DisplayState) { ); } - #[derive(Debug, PartialEq, Copy, Clone)] pub enum Tabs { Messages, @@ -129,7 +139,6 @@ struct MidiKeysApp { state: DisplayState, first_render: bool, - } impl MidiKeysApp { @@ -143,7 +152,13 @@ impl MidiKeysApp { cc.egui_ctx.set_zoom_factor(1.25); - MidiKeysApp { midi_in, ports, messages, state, first_render: true } + MidiKeysApp { + midi_in, + ports, + messages, + state, + first_render: true, + } } pub fn instrument_panel(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { @@ -176,7 +191,6 @@ impl MidiKeysApp { 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::bottom("filter_panel").show(ctx, |ui| { ui.horizontal_wrapped(|ui| { ui.checkbox(&mut only_note_on_off, "Only note on/off"); @@ -242,16 +256,43 @@ impl 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| { + let typing_state = self.state.melodic_typing_state.lock(); + let current_notes = typing_state.current_notes(); + let options = typing_state.mapping().collated_remaining(¤t_notes); + Grid::new("typing_melodic_grid".to_string()).show(ui, |ui| { + ui.label("Current notes:"); + for note in ¤t_notes { + ui.label(c_scale_note_name(*note)); + } + ui.end_row(); }); + + if let Some(options) = options { + Grid::new("typing_melodic_options_grid".to_string()).show(ui, |ui| { + for (next_note, keys) in options { + ui.label(c_scale_note_name(next_note)); + let keys_disp = keys.iter().map(|k| match k { + Keystroke::Char(' ') => format!(""), + Keystroke::Char('\n') => format!(""), + Keystroke::Char(c) => format!("{c}"), + Keystroke::Modifier(modifier) => format!("<{modifier:?}>"), + Keystroke::Special(special) => format!("<{special:?}>"), + }).join(", "); + ui.label(format!("{keys_disp}")); + ui.end_row(); + } + }); + } else { + ui.label("Nothing you can do from here. Sorry."); + } + ctx.request_repaint(); }); } } @@ -288,20 +329,20 @@ impl eframe::App for MidiKeysApp { 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"); - + 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) { @@ -367,10 +408,10 @@ fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) if raw { ui.horizontal_top(|ui| { ui.label("Bytes:"); - Frame::none().show(ui, |ui| { + Frame::new().show(ui, |ui| { ui.spacing_mut().item_spacing = (0.0, 0.0).into(); for byte in &msg.raw { - Frame::none() + Frame::new() .inner_margin(2.0) .stroke((0.5, Color32::GRAY)) .outer_margin(0.0)