diff --git a/Cargo.toml b/Cargo.toml index 0cde161..48a66c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" [dependencies] anyhow = "1.0.86" crossbeam = "0.8.4" -eframe = "0.30.0" -egui = { version = "0.30.0", features = ["log"] } +eframe = "0.31.0" +egui = { version = "0.31.0", features = ["log"] } enigo = { version = "0.2.1", features = ["serde"] } hex = "0.4.3" midir = "0.10.0" diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..cfb01ae --- /dev/null +++ b/TASKS.md @@ -0,0 +1,42 @@ +# a task breakdown in possibly absurd detail because adhd + + +- [ ] drums + - [ ] turn on the drums + - [ ] open midi-keys + - [ ] hit some drums + - [ ] open a text file `drum_mapping.md` + - [ ] write in text file which midi notes are which drums + - [ ] write the encode/decode function for drums + - [ ] print to console what the letters are in base-2 + - [ ] save the notes into `notes_cheat_sheet.md` + - [ ] write the drum hot loop +- [x] wind synth + - [x] add min length of note + - [x] take into account velocity + - [x] change `decode` and `encode` to both be the new alt variant + - [x] add punctuation to decode/encode + - [x] refactor the hot loop + - [x] fix the output of cheat sheet to be correct with new variant + - [x] print to console what the letters are in base-7 / notes + - [x] save the notes into `notes_cheat_sheet.md` + - [x] experiment with mode where shift and caps lock are keys I can press +- [ ] composing + - [ ] open musescore + - [ ] create a new score with 3 parts (wind synth, keyboard synth, drum set) + - [ ] enter the wind synth notes for "hello" as straight quarternotes + - [ ] experiment with rhythms on the wind synth notes +- [ ] ui + - [ ] make default size larger + - [ ] fix disconnect/reconnect bug + - [ ] recreate disconnect/reconnect bug + - [ ] add print statements into some of the queue receives + - [ ] debug the bug + - [ ] make display of typing state + - [ ] expose typing state to the UI + - [ ] show current bytes in buffer (raw) + - [ ] show current bytes as note names + - [ ] show remaining partials as: next note, list of things you can type from there (in alphanum order) +- [ ] improvements + - [ ] swap out printlns for logging + diff --git a/cheat_sheet.md b/cheat_sheet.md new file mode 100644 index 0000000..862a8cb --- /dev/null +++ b/cheat_sheet.md @@ -0,0 +1,50 @@ + +# Melodic + +C C => Modifier(Shift) +C D => Special(CapsLock) +C E => Special(Backspace) +C F => Char('a') +C G => Char('b') +C A => Char('c') +C B => Char('d') +D C => Char('e') +D D => Char('f') +D E => Char('g') +D F => Char('h') +D G => Char('i') +D A => Char('j') +D B => Char('k') +E C => Char('l') +E D => Char('m') +E E => Char('n') +E F => Char('o') +E G => Char('p') +E A => Char('q') +E B => Char('r') +F C => Char('s') +F D => Char('t') +F E => Char('u') +F F => Char('v') +F G => Char('w') +F A => Char('x') +F B => Char('y') +G C => Char('z') +G D => Char('0') +G E => Char('1') +G F => Char('2') +G G => Char('3') +G A => Char('4') +G B => Char('5') +A C => Char('6') +A D => Char('7') +A E => Char('8') +A F => Char('9') +A G => Char('.') +A A => Char(',') +A B => Char('!') +B C => Char(' ') +B D => Char('\n') + + + diff --git a/src/bin/main.rs b/src/bin/main.rs index e6a5045..3680509 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -10,16 +10,14 @@ use midi_keys::{ midi::daemon::Category, parser::parse_message, typing::{ - base_values, encode, midi_to_scale_degree, Keystroke, Modifier, Special, TypingState, + midi_to_scale_degree, standard_melodic_mapping, Keystroke, Modifier, Special, TypingState }, ui::{display_state_daemon, DisplayQueues, DisplayState}, }; use midir::{MidiInput, MidiInputPort}; -const MIN_NOTE_DURATION_MS: u64 = 5; - fn main() { - let (dbg_send, dbg_recv) = crossbeam::channel::unbounded(); + let (dbg_send, _dbg_recv) = crossbeam::channel::unbounded(); let (ws_send, ws_recv) = crossbeam::channel::unbounded(); let (drum_send, drum_recv) = crossbeam::channel::unbounded(); @@ -38,107 +36,56 @@ fn main() { let _handle = display_state_daemon(queues, state.clone()); println!("started daemon"); - for c in "hello, world".chars() { - let v = encode(Keystroke::Char(c)).unwrap(); - println!("{c}: {:?} - {:?}", v, base_values(v, 7, 2)); - } - for c in "Hello, WORLD!".chars() { - if c.is_uppercase() { - let v = encode(Keystroke::Modifier(Modifier::Shift)).unwrap(); - println!("{c}: {:?} - {:?}", v, base_values(v, 7, 2)); - } - let v = encode(Keystroke::Char(c.to_ascii_lowercase())).unwrap(); - println!("{c}: {:?} - {:?}", v, base_values(v, 7, 2)); - } + let melodic_mapping = standard_melodic_mapping(); + melodic_mapping.print_sequences(); - //std::thread::spawn(move || loop { - // match dbg_recv.recv() { - // Ok(m) => println!("debug: {m:?}"), - // Err(err) => println!("err: {err:?}"), - // } - //}); std::thread::spawn(move || { - let mut steps = 0; - let mut state = TypingState::new(7); + let melodic_mapping = standard_melodic_mapping(); + let mut state = TypingState::new(melodic_mapping); let mut enigo = Enigo::new(&Settings::default()).unwrap(); - - let mut current_note = None; let mut shift = false; loop { match ws_recv.recv() { - Ok((_cid, ts, m)) => { - let note_on = m.note_on_values(); - let note_off = m.note_off_values(); + Ok((_cid, _ts, m)) => { + let note = match m.note_on_values() { + Some((note, _velocity)) => note, + None => continue, + }; + let scale_degree = match midi_to_scale_degree(60, note) { + Some(deg) => deg, + None => continue, + }; - if !note_on.is_some() && !note_off.is_some() { - continue; - } + state.enter(scale_degree); - 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; + match state.emit() { + midi_keys::typing::Decoded::Keystroke(keystroke) => match keystroke { + Keystroke::Char(mut c) => { + if shift { + c = c.to_ascii_uppercase(); + shift = false; + } + enigo.text(&format!("{c}")).unwrap(); } - }; - - steps += 1; - state.enter(scale_degree); - - println!("here: {steps}, {scale_degree}, {current_note:?}, {m:?}"); - - if steps % 2 == 0 { - let (b, c) = state.emit(); - println!("got {b}, {c:?}"); - - if let Some(c) = c { - match c { - Keystroke::Char(c) => { - if shift { - println!("well that's a shifty character"); - let shifted_c = c.to_ascii_uppercase(); - enigo.text(&format!("{shifted_c}")).unwrap(); - shift = false; - } else { - println!("no shifty characters here"); - enigo.text(&format!("{c}")).unwrap(); - } - } - Keystroke::Modifier(Modifier::Shift) => { - println!("SHIFTY BEHAVIOR"); - shift = !shift; - } - Keystroke::Special(Special::CapsLock) => { - enigo.key(Key::CapsLock, Direction::Click).unwrap() - } - Keystroke::Special(Special::Backspace) => { - enigo.key(Key::Backspace, Direction::Click).unwrap() - } - }; + Keystroke::Modifier(Modifier::Shift) => { + println!("Shifting!"); + shift = !shift; } + Keystroke::Special(Special::CapsLock) => { + enigo.key(Key::CapsLock, Direction::Click).unwrap() + } + Keystroke::Special(Special::Backspace) => { + enigo.key(Key::Backspace, Direction::Click).unwrap() + } + }, + midi_keys::typing::Decoded::Partial(notes, _keys) => { + println!("Buffer: {notes:?}"); + } + midi_keys::typing::Decoded::Invalid(notes) => { + println!("Invalid: {notes:?}"); } - } - - if let Some((note, velocity)) = note_on { - current_note = Some((note, velocity, ts)); - } else if let Some(_) = note_off { - current_note = None; } } Err(err) => println!("err(ws): {err:?}"), diff --git a/src/typing.rs b/src/typing.rs index 02ceb8b..bcbeddb 100644 --- a/src/typing.rs +++ b/src/typing.rs @@ -1,35 +1,130 @@ -#[derive(Debug, Copy, Clone)] +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub enum Decoded { + Keystroke(Keystroke), + Partial(Vec, Vec), + Invalid(Vec), +} + +#[derive(Debug, Clone)] +pub struct Mapping { + sequences: Vec<(Vec, Keystroke)>, + key_to_seq: HashMap>, +} + +impl Mapping { + pub fn new(mut sequences: Vec<(Vec, Keystroke)>) -> Mapping { + sequences.sort(); + let key_to_seq = sequences.iter().cloned().map(|(a, b)| (b, a)).collect(); + + Mapping { + sequences, + key_to_seq, + } + } + + pub fn encode(&self, key: Keystroke) -> Option> { + self.key_to_seq.get(&key).cloned() + } + + pub fn decode(&self, notes: &Vec) -> Decoded { + if let Some(key) = self.decode_exact(notes) { + return Decoded::Keystroke(key.clone()); + } else if let Some(keys) = self.prefix_range(notes) { + return Decoded::Partial(notes.clone(), keys); + } else { + return Decoded::Invalid(notes.clone()); + } + } + + pub fn decode_exact(&self, notes: &Vec) -> Option { + let index = self + .sequences + .binary_search_by(|(n, _key)| n.cmp(¬es)) + .ok()?; + let (_notes, key) = self.sequences.get(index)?; + Some(key.clone()) + } + + pub fn prefix_range(&self, notes: &Vec) -> Option> { + let low = self + .sequences + .partition_point(|(n, _key)| n[..notes.len()] < **notes); + let high = self + .sequences + .partition_point(|(n, _key)| n[..notes.len()] <= **notes); + + Some( + self.sequences[low..high] + .iter() + .map(|(_n, key)| key.clone()) + .collect(), + ) + } + + 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()); + } + println!("=> {key:?}"); + } + } +} + +pub fn standard_melodic_mapping() -> Mapping { + let modspecial = [ + Keystroke::Modifier(Modifier::Shift), + Keystroke::Special(Special::CapsLock), + Keystroke::Special(Special::Backspace), + ]; + + let alpha = 'a'..='z'; + let num = '0'..='9'; + let punct = ".,! \n".chars(); + let chars = alpha.chain(num).chain(punct).map(|c| Keystroke::Char(c)); + + let keys = modspecial.iter().cloned().chain(chars); + + let sequences: Vec<_> = keys + .enumerate() + .map(|(k, key)| (base_values(k as u32, 7, 2), key)) + .collect(); + + Mapping::new(sequences) +} + +#[derive(Debug, Clone)] pub struct TypingState { - base: u32, - current: u32, + mapping: Mapping, + current: Vec, } impl TypingState { - pub fn new(base: u32) -> TypingState { - TypingState { base, current: 0 } + pub fn new(mapping: Mapping) -> TypingState { + TypingState { + mapping, + current: vec![], + } } - /// Add the value into the current accumulator. + /// Add the value into the typing state. pub fn enter(&mut self, value: u8) { - self.current = self - .current - .saturating_mul(self.base) - .saturating_add(value.into()); + self.current.push(value); } - pub fn set_bits(&mut self, mask: u32) { - self.current |= mask; - } + /// Attempts to decode the current value stored. It will reset if it's either + /// invalid or valid, but will retain the current buffer if it's partial. + pub fn emit(&mut self) -> Decoded { + let decoded = self.mapping.decode(&self.current); - /// Decode the current accumulator as a keystroke, or None if it's invalid. - /// This resets the accumulator to 0! - pub fn emit(&mut self) -> (u32, Option) { - let x = self.current; - let c = decode(x); + if !matches!(decoded, Decoded::Partial(..)) { + self.current.clear(); + } - self.current = 0; - - (x, c) + decoded } } @@ -62,149 +157,42 @@ pub fn midi_to_scale_degree(root: u8, note: u8) -> Option { }) } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)] pub enum Keystroke { Char(char), Modifier(Modifier), Special(Special), } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)] pub enum Modifier { Shift, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)] pub enum Special { CapsLock, Backspace, } -/// Converts a character to its assigned number for use in text entry. This is -/// stores them as u32 to keep things simple and fixed length. If we want to -/// support the full Unicode space, we must expand this to u32. -pub fn encode(key: Keystroke) -> Option { - Some(match key { - Keystroke::Modifier(Modifier::Shift) => 0, - Keystroke::Special(Special::CapsLock) => 1, - Keystroke::Special(Special::Backspace) => 2, - - Keystroke::Char(c @ 'a'..='z') => c as u32 - 'a' as u32 + 3, - Keystroke::Char(c @ '0'..='9') => c as u32 - '0' as u32 + 29, - Keystroke::Char('.') => 39, - Keystroke::Char(',') => 40, - Keystroke::Char('!') => 41, - Keystroke::Char(' ') => 42, - Keystroke::Char('\n') => 43, - - _ => return None, - }) -} - -/// Converts a number to its corresponding character for use in text entry. -/// If a character isn't implemented, it will return None. -pub fn decode(x: u32) -> Option { - Some(match x { - 0 => Keystroke::Modifier(Modifier::Shift), - 1 => Keystroke::Special(Special::CapsLock), - 2 => Keystroke::Special(Special::Backspace), - - 3..29 => Keystroke::Char(char::from_u32(x - 3 + ('a' as u32))?), - 29..39 => Keystroke::Char(char::from_u32(x - 29 + ('0' as u32))?), - 39 => Keystroke::Char('.'), - 40 => Keystroke::Char(','), - 41 => Keystroke::Char('!'), - 42 => Keystroke::Char(' '), - 43 => Keystroke::Char('\n'), - - _ => return None, - }) -} - -pub fn encode_old(c: char) -> Option { - Some(match c { - 'a'..='z' => c as u8 - 'a' as u8, - 'A'..='Z' => c as u8 - 'A' as u8 + 26, - '0'..='9' => c as u8 - '0' as u8 + 26 * 2, - '.' => 62, - ',' => 63, - '!' => 64, - ' ' => 65, - _ => return None, - }) -} - -/// Converts a number to its corresponding character for use in text entry. -/// If a character isn't implemented, it will return None. -pub fn decode_old(x: u8) -> Option { - let c = match x { - 0..=25 => 'a' as u8 + x, - 26..=51 => 'A' as u8 + x - 26, - 52..=61 => '0' as u8 + x - 26 * 2, - 62 => '.' as u8, - 63 => ',' as u8, - 64 => '!' as u8, - 65 => ' ' as u8, - _ => return None, - }; - 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::*; #[test] fn encodes_lowercase() { - assert_eq!(encode(Keystroke::Char('a')), Some(3)); - assert_eq!(encode(Keystroke::Char('b')), Some(4)); - assert_eq!(encode(Keystroke::Char('z')), Some(28)); + 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])); } #[test] fn encodes_numbers() { - assert_eq!(encode(Keystroke::Char('0')), Some(29)); - assert_eq!(encode(Keystroke::Char('1')), Some(30)); - assert_eq!(encode(Keystroke::Char('9')), Some(38)); - } - - #[test] - fn encodes_punctuation() { - assert_eq!(encode(Keystroke::Char('.')), Some(39)); - assert_eq!(encode(Keystroke::Char(',')), Some(40)); - assert_eq!(encode(Keystroke::Char('!')), Some(41)); - } - - #[test] - fn decodes_lowercase() { - assert_eq!(decode(3), Some(Keystroke::Char('a'))); - assert_eq!(decode(4), Some(Keystroke::Char('b'))); - assert_eq!(decode(28), Some(Keystroke::Char('z'))); - } - - #[test] - fn decodes_numbers() { - assert_eq!(decode(29), Some(Keystroke::Char('0'))); - assert_eq!(decode(30), Some(Keystroke::Char('1'))); - assert_eq!(decode(38), Some(Keystroke::Char('9'))); - } - - #[test] - fn decodes_punctuation() { - assert_eq!(decode(39), Some(Keystroke::Char('.'))); - assert_eq!(decode(40), Some(Keystroke::Char(','))); - assert_eq!(decode(41), Some(Keystroke::Char('!'))); + 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])); } #[test] diff --git a/src/ui.rs b/src/ui.rs index 4799247..232f118 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -8,7 +8,7 @@ use std::{ }; use crossbeam::{channel::Receiver, select}; -use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, Rounding, ScrollArea, SelectableLabel}; +use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, ScrollArea, SelectableLabel}; use midir::{MidiInput, MidiInputPort}; use crate::midi::{daemon::CTM, Message, ParsedMessage, VoiceCategory, VoiceMessage}; @@ -128,6 +128,8 @@ struct MidiKeysApp { state: DisplayState, + first_render: bool, + } impl MidiKeysApp { @@ -139,7 +141,7 @@ impl MidiKeysApp { let ports = vec![]; let messages = vec![]; - MidiKeysApp { midi_in, ports, messages, state } + MidiKeysApp { midi_in, ports, messages, state, first_render: true } } pub fn instrument_panel(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { @@ -220,7 +222,7 @@ impl MidiKeysApp { .inner_margin(4.0) .outer_margin(4.0) .stroke((1.0, Color32::BLACK)) - .rounding(Rounding::same(2.0)) + .corner_radius(2) .begin(ui); let port_name = self @@ -254,6 +256,13 @@ impl MidiKeysApp { impl eframe::App for MidiKeysApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + if self.first_render { + let window_size = (1200., 800.).into(); + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(window_size)); + + self.first_render = false; + } + ctx.set_theme(egui::Theme::Light); self.ports = self.state.midi_input_ports.lock().clone(); @@ -301,21 +310,21 @@ fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) VoiceCategory::NoteOff { note, velocity } => ( "NoteOff", vec![ - ("note", note_name(note)), + ("note", midi_note_name(note)), ("velocity", format!("{}", velocity)), ], ), VoiceCategory::NoteOn { note, velocity } => ( "NoteOn", vec![ - ("note", note_name(note)), + ("note", midi_note_name(note)), ("velocity", format!("{}", velocity)), ], ), VoiceCategory::AfterTouch { note, pressure } => ( "AfterTouch", vec![ - ("note", note_name(note)), + ("note", midi_note_name(note)), ("pressure", format!("{}", pressure)), ], ), @@ -372,7 +381,7 @@ fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) } } -fn note_name(midi_note: u8) -> String { +pub fn midi_note_name(midi_note: u8) -> String { let octave = ((midi_note as i32) - 12) / 12; let note = match midi_note % 12 { 0 => "C", @@ -395,13 +404,13 @@ fn note_name(midi_note: u8) -> String { #[cfg(test)] mod tests { - use crate::ui::note_name; + use crate::ui::midi_note_name; #[test] pub fn names_of_notes() { - assert_eq!(note_name(60), "C4"); - assert_eq!(note_name(36), "C2"); - assert_eq!(note_name(57), "A3"); - assert_eq!(note_name(35), "B1"); + assert_eq!(midi_note_name(60), "C4"); + assert_eq!(midi_note_name(36), "C2"); + assert_eq!(midi_note_name(57), "A3"); + assert_eq!(midi_note_name(35), "B1"); } }