finalize edits for refactored melodic
This commit is contained in:
parent
c2cc52e391
commit
adbe60a3d8
6 changed files with 281 additions and 245 deletions
|
@ -6,8 +6,8 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
crossbeam = "0.8.4"
|
crossbeam = "0.8.4"
|
||||||
eframe = "0.30.0"
|
eframe = "0.31.0"
|
||||||
egui = { version = "0.30.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"
|
||||||
midir = "0.10.0"
|
midir = "0.10.0"
|
||||||
|
|
42
TASKS.md
Normal file
42
TASKS.md
Normal file
|
@ -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
|
||||||
|
|
50
cheat_sheet.md
Normal file
50
cheat_sheet.md
Normal file
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|
101
src/bin/main.rs
101
src/bin/main.rs
|
@ -10,16 +10,14 @@ use midi_keys::{
|
||||||
midi::daemon::Category,
|
midi::daemon::Category,
|
||||||
parser::parse_message,
|
parser::parse_message,
|
||||||
typing::{
|
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},
|
ui::{display_state_daemon, DisplayQueues, DisplayState},
|
||||||
};
|
};
|
||||||
use midir::{MidiInput, MidiInputPort};
|
use midir::{MidiInput, MidiInputPort};
|
||||||
|
|
||||||
const MIN_NOTE_DURATION_MS: u64 = 5;
|
|
||||||
|
|
||||||
fn main() {
|
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 (ws_send, ws_recv) = crossbeam::channel::unbounded();
|
||||||
let (drum_send, drum_recv) = crossbeam::channel::unbounded();
|
let (drum_send, drum_recv) = crossbeam::channel::unbounded();
|
||||||
|
|
||||||
|
@ -38,90 +36,41 @@ fn main() {
|
||||||
let _handle = display_state_daemon(queues, state.clone());
|
let _handle = display_state_daemon(queues, state.clone());
|
||||||
println!("started daemon");
|
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() {
|
let melodic_mapping = standard_melodic_mapping();
|
||||||
if c.is_uppercase() {
|
melodic_mapping.print_sequences();
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
//std::thread::spawn(move || loop {
|
|
||||||
// match dbg_recv.recv() {
|
|
||||||
// Ok(m) => println!("debug: {m:?}"),
|
|
||||||
// Err(err) => println!("err: {err:?}"),
|
|
||||||
// }
|
|
||||||
//});
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut steps = 0;
|
let melodic_mapping = standard_melodic_mapping();
|
||||||
let mut state = TypingState::new(7);
|
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 current_note = None;
|
|
||||||
let mut shift = false;
|
let mut shift = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match ws_recv.recv() {
|
match ws_recv.recv() {
|
||||||
Ok((_cid, ts, m)) => {
|
Ok((_cid, _ts, m)) => {
|
||||||
let note_on = m.note_on_values();
|
let note = match m.note_on_values() {
|
||||||
let note_off = m.note_off_values();
|
Some((note, _velocity)) => note,
|
||||||
|
None => continue,
|
||||||
if !note_on.is_some() && !note_off.is_some() {
|
};
|
||||||
continue;
|
let scale_degree = match midi_to_scale_degree(60, note) {
|
||||||
}
|
Some(deg) => deg,
|
||||||
|
|
||||||
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,
|
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
steps += 1;
|
|
||||||
state.enter(scale_degree);
|
state.enter(scale_degree);
|
||||||
|
|
||||||
println!("here: {steps}, {scale_degree}, {current_note:?}, {m:?}");
|
match state.emit() {
|
||||||
|
midi_keys::typing::Decoded::Keystroke(keystroke) => match keystroke {
|
||||||
if steps % 2 == 0 {
|
Keystroke::Char(mut c) => {
|
||||||
let (b, c) = state.emit();
|
|
||||||
println!("got {b}, {c:?}");
|
|
||||||
|
|
||||||
if let Some(c) = c {
|
|
||||||
match c {
|
|
||||||
Keystroke::Char(c) => {
|
|
||||||
if shift {
|
if shift {
|
||||||
println!("well that's a shifty character");
|
c = c.to_ascii_uppercase();
|
||||||
let shifted_c = c.to_ascii_uppercase();
|
|
||||||
enigo.text(&format!("{shifted_c}")).unwrap();
|
|
||||||
shift = false;
|
shift = false;
|
||||||
} else {
|
}
|
||||||
println!("no shifty characters here");
|
|
||||||
enigo.text(&format!("{c}")).unwrap();
|
enigo.text(&format!("{c}")).unwrap();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Keystroke::Modifier(Modifier::Shift) => {
|
Keystroke::Modifier(Modifier::Shift) => {
|
||||||
println!("SHIFTY BEHAVIOR");
|
println!("Shifting!");
|
||||||
shift = !shift;
|
shift = !shift;
|
||||||
}
|
}
|
||||||
Keystroke::Special(Special::CapsLock) => {
|
Keystroke::Special(Special::CapsLock) => {
|
||||||
|
@ -130,16 +79,14 @@ fn main() {
|
||||||
Keystroke::Special(Special::Backspace) => {
|
Keystroke::Special(Special::Backspace) => {
|
||||||
enigo.key(Key::Backspace, Direction::Click).unwrap()
|
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:?}"),
|
Err(err) => println!("err(ws): {err:?}"),
|
||||||
}
|
}
|
||||||
|
|
266
src/typing.rs
266
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<u8>, Vec<Keystroke>),
|
||||||
|
Invalid(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Mapping {
|
||||||
|
sequences: Vec<(Vec<u8>, Keystroke)>,
|
||||||
|
key_to_seq: HashMap<Keystroke, Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapping {
|
||||||
|
pub fn new(mut sequences: Vec<(Vec<u8>, 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<Vec<u8>> {
|
||||||
|
self.key_to_seq.get(&key).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(&self, notes: &Vec<u8>) -> 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<u8>) -> Option<Keystroke> {
|
||||||
|
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<u8>) -> Option<Vec<Keystroke>> {
|
||||||
|
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 {
|
pub struct TypingState {
|
||||||
base: u32,
|
mapping: Mapping,
|
||||||
current: u32,
|
current: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypingState {
|
impl TypingState {
|
||||||
pub fn new(base: u32) -> TypingState {
|
pub fn new(mapping: Mapping) -> TypingState {
|
||||||
TypingState { base, current: 0 }
|
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) {
|
pub fn enter(&mut self, value: u8) {
|
||||||
self.current = self
|
self.current.push(value);
|
||||||
.current
|
|
||||||
.saturating_mul(self.base)
|
|
||||||
.saturating_add(value.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_bits(&mut self, mask: u32) {
|
/// Attempts to decode the current value stored. It will reset if it's either
|
||||||
self.current |= mask;
|
/// 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);
|
||||||
|
|
||||||
|
if !matches!(decoded, Decoded::Partial(..)) {
|
||||||
|
self.current.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode the current accumulator as a keystroke, or None if it's invalid.
|
decoded
|
||||||
/// This resets the accumulator to 0!
|
|
||||||
pub fn emit(&mut self) -> (u32, Option<Keystroke>) {
|
|
||||||
let x = self.current;
|
|
||||||
let c = decode(x);
|
|
||||||
|
|
||||||
self.current = 0;
|
|
||||||
|
|
||||||
(x, c)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,149 +157,42 @@ pub fn midi_to_scale_degree(root: u8, note: u8) -> Option<u8> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
|
||||||
pub enum Keystroke {
|
pub enum Keystroke {
|
||||||
Char(char),
|
Char(char),
|
||||||
Modifier(Modifier),
|
Modifier(Modifier),
|
||||||
Special(Special),
|
Special(Special),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
|
||||||
pub enum Modifier {
|
pub enum Modifier {
|
||||||
Shift,
|
Shift,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
|
||||||
pub enum Special {
|
pub enum Special {
|
||||||
CapsLock,
|
CapsLock,
|
||||||
Backspace,
|
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<u32> {
|
|
||||||
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<Keystroke> {
|
|
||||||
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<u8> {
|
|
||||||
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<char> {
|
|
||||||
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<char> {
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encodes_lowercase() {
|
fn encodes_lowercase() {
|
||||||
assert_eq!(encode(Keystroke::Char('a')), Some(3));
|
let mapping = standard_melodic_mapping();
|
||||||
assert_eq!(encode(Keystroke::Char('b')), Some(4));
|
assert_eq!(mapping.encode(Keystroke::Char('a')), Some(vec![0,3]));
|
||||||
assert_eq!(encode(Keystroke::Char('z')), Some(28));
|
assert_eq!(mapping.encode(Keystroke::Char('b')), Some(vec![0,4]));
|
||||||
|
assert_eq!(mapping.encode(Keystroke::Char('z')), Some(vec![4,0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encodes_numbers() {
|
fn encodes_numbers() {
|
||||||
assert_eq!(encode(Keystroke::Char('0')), Some(29));
|
let mapping = standard_melodic_mapping();
|
||||||
assert_eq!(encode(Keystroke::Char('1')), Some(30));
|
assert_eq!(mapping.encode(Keystroke::Char('0')), Some(vec![4,1]));
|
||||||
assert_eq!(encode(Keystroke::Char('9')), Some(38));
|
assert_eq!(mapping.encode(Keystroke::Char('1')), Some(vec![4,2]));
|
||||||
}
|
assert_eq!(mapping.encode(Keystroke::Char('9')), Some(vec![5,3]));
|
||||||
|
|
||||||
#[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('!')));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
33
src/ui.rs
33
src/ui.rs
|
@ -8,7 +8,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crossbeam::{channel::Receiver, select};
|
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 midir::{MidiInput, MidiInputPort};
|
||||||
|
|
||||||
use crate::midi::{daemon::CTM, Message, ParsedMessage, VoiceCategory, VoiceMessage};
|
use crate::midi::{daemon::CTM, Message, ParsedMessage, VoiceCategory, VoiceMessage};
|
||||||
|
@ -128,6 +128,8 @@ struct MidiKeysApp {
|
||||||
|
|
||||||
state: DisplayState,
|
state: DisplayState,
|
||||||
|
|
||||||
|
first_render: bool,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MidiKeysApp {
|
impl MidiKeysApp {
|
||||||
|
@ -139,7 +141,7 @@ impl MidiKeysApp {
|
||||||
let ports = vec![];
|
let ports = vec![];
|
||||||
let messages = 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) {
|
pub fn instrument_panel(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
@ -220,7 +222,7 @@ impl MidiKeysApp {
|
||||||
.inner_margin(4.0)
|
.inner_margin(4.0)
|
||||||
.outer_margin(4.0)
|
.outer_margin(4.0)
|
||||||
.stroke((1.0, Color32::BLACK))
|
.stroke((1.0, Color32::BLACK))
|
||||||
.rounding(Rounding::same(2.0))
|
.corner_radius(2)
|
||||||
.begin(ui);
|
.begin(ui);
|
||||||
|
|
||||||
let port_name = self
|
let port_name = self
|
||||||
|
@ -254,6 +256,13 @@ impl MidiKeysApp {
|
||||||
|
|
||||||
impl eframe::App for MidiKeysApp {
|
impl eframe::App for MidiKeysApp {
|
||||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
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);
|
ctx.set_theme(egui::Theme::Light);
|
||||||
|
|
||||||
self.ports = self.state.midi_input_ports.lock().clone();
|
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 } => (
|
VoiceCategory::NoteOff { note, velocity } => (
|
||||||
"NoteOff",
|
"NoteOff",
|
||||||
vec![
|
vec![
|
||||||
("note", note_name(note)),
|
("note", midi_note_name(note)),
|
||||||
("velocity", format!("{}", velocity)),
|
("velocity", format!("{}", velocity)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
VoiceCategory::NoteOn { note, velocity } => (
|
VoiceCategory::NoteOn { note, velocity } => (
|
||||||
"NoteOn",
|
"NoteOn",
|
||||||
vec![
|
vec![
|
||||||
("note", note_name(note)),
|
("note", midi_note_name(note)),
|
||||||
("velocity", format!("{}", velocity)),
|
("velocity", format!("{}", velocity)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
VoiceCategory::AfterTouch { note, pressure } => (
|
VoiceCategory::AfterTouch { note, pressure } => (
|
||||||
"AfterTouch",
|
"AfterTouch",
|
||||||
vec![
|
vec![
|
||||||
("note", note_name(note)),
|
("note", midi_note_name(note)),
|
||||||
("pressure", format!("{}", pressure)),
|
("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 octave = ((midi_note as i32) - 12) / 12;
|
||||||
let note = match midi_note % 12 {
|
let note = match midi_note % 12 {
|
||||||
0 => "C",
|
0 => "C",
|
||||||
|
@ -395,13 +404,13 @@ fn note_name(midi_note: u8) -> String {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::ui::note_name;
|
use crate::ui::midi_note_name;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn names_of_notes() {
|
pub fn names_of_notes() {
|
||||||
assert_eq!(note_name(60), "C4");
|
assert_eq!(midi_note_name(60), "C4");
|
||||||
assert_eq!(note_name(36), "C2");
|
assert_eq!(midi_note_name(36), "C2");
|
||||||
assert_eq!(note_name(57), "A3");
|
assert_eq!(midi_note_name(57), "A3");
|
||||||
assert_eq!(note_name(35), "B1");
|
assert_eq!(midi_note_name(35), "B1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue