made it display the typing state

This commit is contained in:
Nicole Tietz-Sokolskaya 2025-03-26 23:10:54 -04:00
parent 454c92c077
commit 0f00f0ef68
4 changed files with 126 additions and 40 deletions

View file

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

View file

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

View file

@ -1,9 +1,10 @@
use itertools::Itertools;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Decoded {
Keystroke(Keystroke),
Partial(Vec<u8>, Vec<Keystroke>),
Partial(Vec<u8>, Vec<(Vec<u8>, Keystroke)>),
Invalid(Vec<u8>),
}
@ -47,33 +48,68 @@ impl Mapping {
Some(key.clone())
}
pub fn prefix_range(&self, notes: &Vec<u8>) -> Option<Vec<Keystroke>> {
let low = self
pub fn prefix_range(&self, notes: &Vec<u8>) -> Option<Vec<(Vec<u8>, 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(&notes))
.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<u8>) -> Option<Vec<(u8, Vec<Keystroke>)>> {
let next_sequences = self.prefix_range(notes)?;
let seqs = self
.sequences
.iter()
.filter(|(n, _key)| n.starts_with(&notes))
.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<u8> {
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]

View file

@ -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<AtomicBool>,
pub show_raw: Arc<AtomicBool>,
pub selected_tab: Tabs,
pub melodic_typing_state: Arc<Mutex<TypingState>>,
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(&current_notes);
Grid::new("typing_melodic_grid".to_string()).show(ui, |ui| {
ui.label("Current notes:");
for note in &current_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!("<space>"),
Keystroke::Char('\n') => format!("<newline>"),
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)