works for now, save
This commit is contained in:
parent
dfb4c93e4e
commit
2a01c5ff97
5 changed files with 187 additions and 46 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"
|
eframe = "0.30.0"
|
||||||
egui = "0"
|
egui = { version = "0.30.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"
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use enigo::{Enigo, Keyboard, Settings};
|
use enigo::{Enigo, Key, Keyboard, Settings};
|
||||||
use midi_keys::{
|
use midi_keys::{
|
||||||
log::load_raw_log,
|
log::load_raw_log,
|
||||||
midi::daemon::Category,
|
midi::daemon::Category,
|
||||||
|
@ -14,6 +14,8 @@ use midi_keys::{
|
||||||
};
|
};
|
||||||
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();
|
||||||
|
@ -53,24 +55,85 @@ fn main() {
|
||||||
let mut steps = 0;
|
let mut steps = 0;
|
||||||
let mut state = TypingState::new(7);
|
let mut state = TypingState::new(7);
|
||||||
let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
||||||
|
|
||||||
|
let mut current_note = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match ws_recv.recv() {
|
match ws_recv.recv() {
|
||||||
Ok((_cid, _ts, m)) => {
|
Ok((_cid, ts, m)) => {
|
||||||
if let Some((note, _velocity)) = m.note_on_values() {
|
let note_on = m.note_on_values();
|
||||||
if let Some(degree) = midi_to_scale_degree(60, note) {
|
let note_off = m.note_off_values();
|
||||||
steps += 1;
|
|
||||||
println!("got degree: {degree}, steps: {steps}, state: {state:?}");
|
if !note_on.is_some() && !note_off.is_some() {
|
||||||
state.enter(degree);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
steps += 1;
|
||||||
|
state.enter(scale_degree);
|
||||||
|
|
||||||
|
println!("here: {steps}, {scale_degree}, {current_note:?}, {m:?}");
|
||||||
|
|
||||||
|
if steps % 2 == 0 {
|
||||||
|
if velocity > 20 {
|
||||||
|
state.set_bits(64);
|
||||||
|
}
|
||||||
|
|
||||||
if steps % 3 == 0 {
|
|
||||||
let (b, c) = state.emit();
|
let (b, c) = state.emit();
|
||||||
println!("got {b}, {c:?}");
|
println!("got {b}, {c:?}");
|
||||||
|
|
||||||
if let Some(c) = c {
|
if let Some(c) = c {
|
||||||
enigo.text(&format!("{c}"));
|
enigo.text(&format!("{c}")).unwrap();
|
||||||
|
} else if b == 120 {
|
||||||
|
enigo.key(Key::Backspace, enigo::Direction::Click).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some((note, velocity)) = note_on {
|
||||||
|
current_note = Some((note, velocity, ts));
|
||||||
|
} else if let Some(_) = note_off {
|
||||||
|
current_note = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//if let Some((note, velocity)) = m.note_on_values() {
|
||||||
|
// if let Some(degree) = midi_to_scale_degree(60, note) {
|
||||||
|
// steps += 1;
|
||||||
|
// println!("got degree: {degree}, steps: {steps}, state: {state:?}");
|
||||||
|
// state.enter(degree);
|
||||||
|
|
||||||
|
// if steps % 3 == 0 {
|
||||||
|
// let (b, c) = state.emit();
|
||||||
|
// println!("got {b}, {c:?}");
|
||||||
|
// if let Some(c) = c {
|
||||||
|
// enigo.text(&format!("{c}")).unwrap();
|
||||||
|
// } else if b == 255 {
|
||||||
|
// enigo.key(Key::Backspace, enigo::Direction::Click).unwrap();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
Err(err) => println!("err(ws): {err:?}"),
|
Err(err) => println!("err(ws): {err:?}"),
|
||||||
}
|
}
|
||||||
|
|
13
src/midi.rs
13
src/midi.rs
|
@ -23,6 +23,19 @@ impl Message {
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts the note value and velocity for any NoteOff messages.
|
||||||
|
/// This is a convenience function to let us avoid a match in all the places
|
||||||
|
/// we need to extract this.
|
||||||
|
pub fn note_off_values(&self) -> Option<(u8, u8)> {
|
||||||
|
match &self.parsed {
|
||||||
|
ParsedMessage::Voice(VoiceMessage {
|
||||||
|
category: VoiceCategory::NoteOff { note, velocity },
|
||||||
|
..
|
||||||
|
}) => Some((*note, *velocity)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
|
|
|
@ -14,11 +14,15 @@ impl TypingState {
|
||||||
self.current = self.current.saturating_mul(self.base).saturating_add(value);
|
self.current = self.current.saturating_mul(self.base).saturating_add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_bits(&mut self, mask: u8) {
|
||||||
|
self.current |= mask;
|
||||||
|
}
|
||||||
|
|
||||||
/// Decode the current accumulator as a character, or None if it's invalid.
|
/// Decode the current accumulator as a character, or None if it's invalid.
|
||||||
/// This resets the accumulator to 0!
|
/// This resets the accumulator to 0!
|
||||||
pub fn emit(&mut self) -> (u8, Option<char>) {
|
pub fn emit(&mut self) -> (u8, Option<char>) {
|
||||||
let x = self.current;
|
let x = self.current;
|
||||||
let c = decode(x);
|
let c = decode_alt(x);
|
||||||
|
|
||||||
self.current = 0;
|
self.current = 0;
|
||||||
|
|
||||||
|
@ -86,6 +90,17 @@ pub fn decode(x: u8) -> Option<char> {
|
||||||
Some(char::from(c))
|
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::*;
|
||||||
|
|
110
src/ui.rs
110
src/ui.rs
|
@ -23,6 +23,7 @@ pub struct DisplayState {
|
||||||
pub max_messages: usize,
|
pub max_messages: usize,
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayState {
|
impl DisplayState {
|
||||||
|
@ -34,6 +35,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,48 +114,39 @@ pub fn run(state: DisplayState) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||||
|
pub enum Tabs {
|
||||||
|
Messages,
|
||||||
|
WindSynthTyping,
|
||||||
|
}
|
||||||
|
|
||||||
struct MidiKeysApp {
|
struct MidiKeysApp {
|
||||||
midi_in: MidiInput,
|
midi_in: MidiInput,
|
||||||
|
ports: Vec<MidiInputPort>,
|
||||||
|
messages: Vec<CTM>,
|
||||||
|
|
||||||
state: DisplayState,
|
state: DisplayState,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MidiKeysApp {
|
impl MidiKeysApp {
|
||||||
fn new(_cc: &eframe::CreationContext<'_>, state: DisplayState) -> Self {
|
fn new(_cc: &eframe::CreationContext<'_>, state: DisplayState) -> Self {
|
||||||
// this is where to hook in for customizing eguji, like fonts and visuals.
|
// this is where to hook in for customizing egui, like fonts and visuals.
|
||||||
|
|
||||||
let midi_in: MidiInput =
|
let midi_in: MidiInput =
|
||||||
MidiInput::new("midi-keys").expect("could not connect to system MIDI");
|
MidiInput::new("midi-keys").expect("could not connect to system MIDI");
|
||||||
|
let ports = vec![];
|
||||||
|
let messages = vec![];
|
||||||
|
|
||||||
MidiKeysApp { midi_in, state }
|
MidiKeysApp { midi_in, ports, messages, state }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for MidiKeysApp {
|
pub fn instrument_panel(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
||||||
ctx.set_theme(egui::Theme::Light);
|
|
||||||
|
|
||||||
let ports = self.state.midi_input_ports.lock().clone();
|
|
||||||
let messages = self.state.midi_messages.lock().clone();
|
|
||||||
|
|
||||||
egui::TopBottomPanel::top("menu_bar_panel").show(ctx, |ui| {
|
|
||||||
egui::menu::bar(ui, |ui| {
|
|
||||||
ui.menu_button("File", |ui| {
|
|
||||||
if ui.button("Quit").clicked() {
|
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.menu_button("Help", |ui| {
|
|
||||||
if ui.button("About").clicked() {
|
|
||||||
// TODO: implement something
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::SidePanel::left("instrument_panel").show(ctx, |ui| {
|
egui::SidePanel::left("instrument_panel").show(ctx, |ui| {
|
||||||
ui.heading("Connections");
|
ui.heading("Connections");
|
||||||
|
|
||||||
for port in ports.iter() {
|
for port in self.ports.iter() {
|
||||||
let port_name = self
|
let port_name = self
|
||||||
.midi_in
|
.midi_in
|
||||||
.port_name(port)
|
.port_name(port)
|
||||||
|
@ -171,11 +164,16 @@ impl eframe::App for MidiKeysApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn messages_tab(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
|
self.instrument_panel(ctx, frame);
|
||||||
|
|
||||||
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::top("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");
|
||||||
ui.checkbox(&mut show_raw, "Display raw bytes");
|
ui.checkbox(&mut show_raw, "Display raw bytes");
|
||||||
|
@ -192,7 +190,7 @@ impl eframe::App for MidiKeysApp {
|
||||||
.max_width(f32::INFINITY)
|
.max_width(f32::INFINITY)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() {
|
for (idx, (conn, _ts, msg)) in self.messages.iter().rev().enumerate() {
|
||||||
if only_note_on_off
|
if only_note_on_off
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
&msg.parsed,
|
&msg.parsed,
|
||||||
|
@ -213,7 +211,7 @@ impl eframe::App for MidiKeysApp {
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let port = match ports.iter().find(|p| &p.id() == conn) {
|
let port = match self.ports.iter().find(|p| &p.id() == conn) {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
@ -232,7 +230,7 @@ impl eframe::App for MidiKeysApp {
|
||||||
|
|
||||||
frame.content_ui.label(RichText::new(port_name).strong());
|
frame.content_ui.label(RichText::new(port_name).strong());
|
||||||
|
|
||||||
//display_midi_message(idx, msg, &mut frame.content_ui, show_raw);
|
display_midi_message(idx, msg, &mut frame.content_ui, show_raw);
|
||||||
frame.end(ui);
|
frame.end(ui);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -240,7 +238,59 @@ impl eframe::App for MidiKeysApp {
|
||||||
ctx.request_repaint();
|
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| {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for MidiKeysApp {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
|
ctx.set_theme(egui::Theme::Light);
|
||||||
|
|
||||||
|
self.ports = self.state.midi_input_ports.lock().clone();
|
||||||
|
self.messages = self.state.midi_messages.lock().clone().into();
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("menu_bar_panel").show(ctx, |ui| {
|
||||||
|
egui::menu::bar(ui, |ui| {
|
||||||
|
ui.menu_button("File", |ui| {
|
||||||
|
if ui.button("Quit").clicked() {
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.menu_button("Help", |ui| {
|
||||||
|
if ui.button("About").clicked() {
|
||||||
|
// TODO: implement something
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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) {
|
fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) {
|
||||||
|
|
Loading…
Reference in a new issue