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"] }
|
egui = { version = "0.31.0", features = ["log"] }
|
||||||
enigo = { version = "0.2.1", features = ["serde"] }
|
enigo = { version = "0.2.1", features = ["serde"] }
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
itertools = "0.14.0"
|
||||||
midir = "0.10.0"
|
midir = "0.10.0"
|
||||||
nom = "7.1.3"
|
nom = "7.1.3"
|
||||||
thiserror = "1.0.63"
|
thiserror = "1.0.63"
|
||||||
|
|
|
@ -10,7 +10,7 @@ use midi_keys::{
|
||||||
midi::daemon::Category,
|
midi::daemon::Category,
|
||||||
parser::parse_message,
|
parser::parse_message,
|
||||||
typing::{
|
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},
|
ui::{display_state_daemon, DisplayQueues, DisplayState},
|
||||||
};
|
};
|
||||||
|
@ -36,13 +36,12 @@ fn main() {
|
||||||
let _handle = display_state_daemon(queues, state.clone());
|
let _handle = display_state_daemon(queues, state.clone());
|
||||||
println!("started daemon");
|
println!("started daemon");
|
||||||
|
|
||||||
|
|
||||||
let melodic_mapping = standard_melodic_mapping();
|
let melodic_mapping = standard_melodic_mapping();
|
||||||
melodic_mapping.print_sequences();
|
melodic_mapping.print_sequences();
|
||||||
|
|
||||||
|
let typing_state = state.melodic_typing_state.clone();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
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 enigo = Enigo::new(&Settings::default()).unwrap();
|
||||||
let mut shift = false;
|
let mut shift = false;
|
||||||
|
|
||||||
|
@ -58,6 +57,7 @@ fn main() {
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut state = typing_state.lock();
|
||||||
state.enter(scale_degree);
|
state.enter(scale_degree);
|
||||||
|
|
||||||
match state.emit() {
|
match state.emit() {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
use itertools::Itertools;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Decoded {
|
pub enum Decoded {
|
||||||
Keystroke(Keystroke),
|
Keystroke(Keystroke),
|
||||||
Partial(Vec<u8>, Vec<Keystroke>),
|
Partial(Vec<u8>, Vec<(Vec<u8>, Keystroke)>),
|
||||||
Invalid(Vec<u8>),
|
Invalid(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,33 +48,68 @@ impl Mapping {
|
||||||
Some(key.clone())
|
Some(key.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prefix_range(&self, notes: &Vec<u8>) -> Option<Vec<Keystroke>> {
|
pub fn prefix_range(&self, notes: &Vec<u8>) -> Option<Vec<(Vec<u8>, Keystroke)>> {
|
||||||
let low = self
|
// we only want to match prefixes with exactly this length of notes
|
||||||
|
let valid_seqs: Vec<_> = self
|
||||||
.sequences
|
.sequences
|
||||||
.partition_point(|(n, _key)| n[..notes.len()] < **notes);
|
.iter()
|
||||||
let high = self
|
.filter(|(n, _key)| n.starts_with(¬es))
|
||||||
.sequences
|
.cloned()
|
||||||
.partition_point(|(n, _key)| n[..notes.len()] <= **notes);
|
.collect();
|
||||||
|
|
||||||
Some(
|
if valid_seqs.is_empty() {
|
||||||
self.sequences[low..high]
|
None
|
||||||
.iter()
|
} else {
|
||||||
.map(|(_n, key)| key.clone())
|
Some(valid_seqs)
|
||||||
.collect(),
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
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) {
|
pub fn print_sequences(&self) {
|
||||||
let notes: Vec<_> = "CDEFGAB".chars().collect();
|
|
||||||
for (seq, key) in self.sequences.iter() {
|
for (seq, key) in self.sequences.iter() {
|
||||||
for deg in seq {
|
for deg in seq {
|
||||||
print!("{} ", notes.get(*deg as usize).unwrap());
|
print!("{} ", c_scale_note_name(*deg));
|
||||||
}
|
}
|
||||||
println!("=> {key:?}");
|
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 {
|
pub fn standard_melodic_mapping() -> Mapping {
|
||||||
let modspecial = [
|
let modspecial = [
|
||||||
Keystroke::Modifier(Modifier::Shift),
|
Keystroke::Modifier(Modifier::Shift),
|
||||||
|
@ -126,6 +162,14 @@ impl TypingState {
|
||||||
|
|
||||||
decoded
|
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.
|
/// Chunks a u8 into a sequence of values in a given base.
|
||||||
|
@ -182,17 +226,17 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn encodes_lowercase() {
|
fn encodes_lowercase() {
|
||||||
let mapping = standard_melodic_mapping();
|
let mapping = standard_melodic_mapping();
|
||||||
assert_eq!(mapping.encode(Keystroke::Char('a')), Some(vec![0,3]));
|
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('b')), Some(vec![0, 4]));
|
||||||
assert_eq!(mapping.encode(Keystroke::Char('z')), Some(vec![4,0]));
|
assert_eq!(mapping.encode(Keystroke::Char('z')), Some(vec![4, 0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encodes_numbers() {
|
fn encodes_numbers() {
|
||||||
let mapping = standard_melodic_mapping();
|
let mapping = standard_melodic_mapping();
|
||||||
assert_eq!(mapping.encode(Keystroke::Char('0')), Some(vec![4,1]));
|
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('1')), Some(vec![4, 2]));
|
||||||
assert_eq!(mapping.encode(Keystroke::Char('9')), Some(vec![5,3]));
|
assert_eq!(mapping.encode(Keystroke::Char('9')), Some(vec![5, 3]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
71
src/ui.rs
71
src/ui.rs
|
@ -7,11 +7,15 @@ use std::{
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
use crossbeam::{channel::Receiver, select};
|
use crossbeam::{channel::Receiver, select};
|
||||||
use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, ScrollArea, SelectableLabel};
|
use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, ScrollArea, SelectableLabel};
|
||||||
use midir::{MidiInput, MidiInputPort};
|
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
|
/// State used to display the UI. It's intended to be shared between the
|
||||||
/// renderer and the daemon which updates the state.
|
/// renderer and the daemon which updates the state.
|
||||||
|
@ -24,10 +28,15 @@ pub struct DisplayState {
|
||||||
pub only_note_on_off: Arc<AtomicBool>,
|
pub only_note_on_off: Arc<AtomicBool>,
|
||||||
pub show_raw: Arc<AtomicBool>,
|
pub show_raw: Arc<AtomicBool>,
|
||||||
pub selected_tab: Tabs,
|
pub selected_tab: Tabs,
|
||||||
|
pub melodic_typing_state: Arc<Mutex<TypingState>>,
|
||||||
|
pub scratchpad: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayState {
|
impl DisplayState {
|
||||||
pub fn new() -> DisplayState {
|
pub fn new() -> DisplayState {
|
||||||
|
let melodic_mapping = standard_melodic_mapping();
|
||||||
|
let melodic_typing_state = TypingState::new(melodic_mapping);
|
||||||
|
|
||||||
DisplayState {
|
DisplayState {
|
||||||
midi_input_ports: Arc::new(Mutex::new(vec![])),
|
midi_input_ports: Arc::new(Mutex::new(vec![])),
|
||||||
midi_messages: Arc::new(Mutex::new(VecDeque::new())),
|
midi_messages: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
|
@ -35,7 +44,9 @@ impl DisplayState {
|
||||||
max_messages: 10_000_usize,
|
max_messages: 10_000_usize,
|
||||||
only_note_on_off: Arc::new(AtomicBool::new(true)),
|
only_note_on_off: Arc::new(AtomicBool::new(true)),
|
||||||
show_raw: 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)]
|
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||||
pub enum Tabs {
|
pub enum Tabs {
|
||||||
Messages,
|
Messages,
|
||||||
|
@ -129,7 +139,6 @@ struct MidiKeysApp {
|
||||||
state: DisplayState,
|
state: DisplayState,
|
||||||
|
|
||||||
first_render: bool,
|
first_render: bool,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MidiKeysApp {
|
impl MidiKeysApp {
|
||||||
|
@ -143,7 +152,13 @@ impl MidiKeysApp {
|
||||||
|
|
||||||
cc.egui_ctx.set_zoom_factor(1.25);
|
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) {
|
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 only_note_on_off = self.state.only_note_on_off.load(Ordering::Relaxed);
|
||||||
let mut show_raw = self.state.show_raw.load(Ordering::Relaxed);
|
let mut show_raw = self.state.show_raw.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
|
||||||
egui::TopBottomPanel::bottom("filter_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::bottom("filter_panel").show(ctx, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.checkbox(&mut only_note_on_off, "Only note on/off");
|
ui.checkbox(&mut only_note_on_off, "Only note on/off");
|
||||||
|
@ -242,16 +256,43 @@ impl MidiKeysApp {
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn typing_tab(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
pub fn typing_tab(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
self.instrument_panel(ctx, frame);
|
self.instrument_panel(ctx, frame);
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
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| {
|
egui::TopBottomPanel::top("tab_selector").show(ctx, |ui| {
|
||||||
ui.horizontal_wrapped(|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::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 {
|
if self.state.selected_tab == Tabs::Messages {
|
||||||
self.messages_tab(ctx, frame);
|
self.messages_tab(ctx, frame);
|
||||||
} else if self.state.selected_tab == Tabs::WindSynthTyping {
|
} else if self.state.selected_tab == Tabs::WindSynthTyping {
|
||||||
self.typing_tab(ctx, frame);
|
self.typing_tab(ctx, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) {
|
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 {
|
if raw {
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal_top(|ui| {
|
||||||
ui.label("Bytes:");
|
ui.label("Bytes:");
|
||||||
Frame::none().show(ui, |ui| {
|
Frame::new().show(ui, |ui| {
|
||||||
ui.spacing_mut().item_spacing = (0.0, 0.0).into();
|
ui.spacing_mut().item_spacing = (0.0, 0.0).into();
|
||||||
for byte in &msg.raw {
|
for byte in &msg.raw {
|
||||||
Frame::none()
|
Frame::new()
|
||||||
.inner_margin(2.0)
|
.inner_margin(2.0)
|
||||||
.stroke((0.5, Color32::GRAY))
|
.stroke((0.5, Color32::GRAY))
|
||||||
.outer_margin(0.0)
|
.outer_margin(0.0)
|
||||||
|
|
Loading…
Reference in a new issue