made it display the typing state
This commit is contained in:
parent
454c92c077
commit
0f00f0ef68
4 changed files with 126 additions and 40 deletions
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(¬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<u8>) -> Option<Vec<(u8, Vec<Keystroke>)>> {
|
||||
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<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]
|
||||
|
|
71
src/ui.rs
71
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<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(¤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!("<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)
|
||||
|
|
Loading…
Reference in a new issue