Skip to content

Commit e952cf7

Browse files
committed
Start of computer keyboard widget
1 parent 85f135c commit e952cf7

File tree

13 files changed

+466
-80
lines changed

13 files changed

+466
-80
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ members = [
2323
"modules",
2424
"patches",
2525
"player",
26+
"sdl2",
2627
"utils",
2728
"viz-udp-app",
2829
"viz-udp-app-lib",

computer-keyboard/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use caw_core::{Buf, Sig, SigT};
22
use caw_keyboard::{KeyEvent, KeyEvents, Note};
33
use itertools::izip;
44

5-
#[derive(Debug, Clone, Copy)]
5+
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
66
pub enum Key {
77
A,
88
B,
@@ -274,7 +274,7 @@ impl<K> Keyboard<K> {
274274

275275
/// Maps the keys on a US keyboard to musical notes. The 4 rows from the number row to the ZXCV row
276276
/// are used, where the QWER and ZXCV are white notes and the number and ZXCV rows are black notes.
277-
fn opinionated_note_by_key(start_note: Note) -> Vec<(Key, Note)> {
277+
pub fn opinionated_note_by_key(start_note: Note) -> Vec<(Key, Note)> {
278278
use Key::*;
279279
let top_row_base = start_note.add_octaves(1);
280280
let top_row = vec![

interactive/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ anyhow = "1.0"
1414
caw_player = { version = "0.8", path = "../player" }
1515
caw_core = { version = "0.5", path = "../core" }
1616
caw_computer_keyboard = { version = "0.4", path = "../computer-keyboard" }
17+
caw_sdl2 = { version = "0.1", path = "../sdl2" }
1718
line_2d = "0.5"
1819
rgb_int = "0.1"
1920
sdl2 = "0.38"

interactive/src/input.rs

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -65,58 +65,11 @@ impl Input {
6565
}
6666

6767
pub(crate) fn set_key(&self, scancode: Scancode, pressed: bool) {
68-
let key_state = match scancode {
69-
Scancode::A => &self.keyboard.a,
70-
Scancode::B => &self.keyboard.b,
71-
Scancode::C => &self.keyboard.c,
72-
Scancode::D => &self.keyboard.d,
73-
Scancode::E => &self.keyboard.e,
74-
Scancode::F => &self.keyboard.f,
75-
Scancode::G => &self.keyboard.g,
76-
Scancode::H => &self.keyboard.h,
77-
Scancode::I => &self.keyboard.i,
78-
Scancode::J => &self.keyboard.j,
79-
Scancode::K => &self.keyboard.k,
80-
Scancode::L => &self.keyboard.l,
81-
Scancode::M => &self.keyboard.m,
82-
Scancode::N => &self.keyboard.n,
83-
Scancode::O => &self.keyboard.o,
84-
Scancode::P => &self.keyboard.p,
85-
Scancode::Q => &self.keyboard.q,
86-
Scancode::R => &self.keyboard.r,
87-
Scancode::S => &self.keyboard.s,
88-
Scancode::T => &self.keyboard.t,
89-
Scancode::U => &self.keyboard.u,
90-
Scancode::V => &self.keyboard.v,
91-
Scancode::W => &self.keyboard.w,
92-
Scancode::X => &self.keyboard.x,
93-
Scancode::Y => &self.keyboard.y,
94-
Scancode::Z => &self.keyboard.z,
95-
Scancode::Num0 => &self.keyboard.n0,
96-
Scancode::Num1 => &self.keyboard.n1,
97-
Scancode::Num2 => &self.keyboard.n2,
98-
Scancode::Num3 => &self.keyboard.n3,
99-
Scancode::Num4 => &self.keyboard.n4,
100-
Scancode::Num5 => &self.keyboard.n5,
101-
Scancode::Num6 => &self.keyboard.n6,
102-
Scancode::Num7 => &self.keyboard.n7,
103-
Scancode::Num8 => &self.keyboard.n8,
104-
Scancode::Num9 => &self.keyboard.n9,
105-
Scancode::LeftBracket => &self.keyboard.left_bracket,
106-
Scancode::RightBracket => &self.keyboard.right_bracket,
107-
Scancode::Semicolon => &self.keyboard.semicolon,
108-
Scancode::Apostrophe => &self.keyboard.apostrophe,
109-
Scancode::Comma => &self.keyboard.comma,
110-
Scancode::Period => &self.keyboard.period,
111-
Scancode::Minus => &self.keyboard.minus,
112-
Scancode::Equals => &self.keyboard.equals,
113-
Scancode::Slash => &self.keyboard.slash,
114-
Scancode::Space => &self.keyboard.space,
115-
Scancode::Backspace => &self.keyboard.backspace,
116-
Scancode::Backslash => &self.keyboard.backslash,
117-
_ => return,
118-
};
119-
key_state.0.set(pressed);
68+
if let Some(key_state) =
69+
caw_sdl2::sdl2_scancode_get(&self.keyboard, scancode)
70+
{
71+
key_state.0.set(pressed);
72+
}
12073
}
12174

12275
pub(crate) fn set_mouse_position(&self, x_01: f32, y_01: f32) {

keyboard/src/a440_12tet.rs

Lines changed: 147 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
/// 12-tone equal temperament following the A440Hz convention
1+
//! 12-tone equal temperament following the A440Hz convention
22
use caw_core::{Buf, ConstBuf, Sig, SigCtx, SigT};
3+
use std::{fmt::Display, str::FromStr};
34

45
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
56
pub struct Octave(u8);
67

78
impl Octave {
89
pub const MAX_OCTAVE: u8 = 8;
910

10-
pub fn new(i: u8) -> Self {
11+
pub const fn new(i: u8) -> Self {
1112
assert!(i <= Self::MAX_OCTAVE);
1213
Self(i)
1314
}
1415

16+
pub const fn index(self) -> u8 {
17+
self.0
18+
}
19+
1520
pub const _0: Self = Self(0);
1621
pub const _1: Self = Self(1);
1722
pub const _2: Self = Self(2);
@@ -92,6 +97,48 @@ impl NoteName {
9297
self.relative_midi_index
9398
}
9499

100+
/// Returns a str representation of the note name where all accidentals are sharp, formatted
101+
/// like "C" or "C_sharp"
102+
pub const fn to_str_sharp(self) -> &'static str {
103+
match self.relative_midi_index {
104+
0 => "C",
105+
1 => "C_sharp",
106+
2 => "D",
107+
3 => "D_sharp",
108+
4 => "E",
109+
5 => "F",
110+
6 => "F_sharp",
111+
7 => "G",
112+
8 => "G_sharp",
113+
9 => "A",
114+
10 => "A_sharp",
115+
11 => "B",
116+
_ => unreachable!(),
117+
}
118+
}
119+
120+
/// Parses a str like "C" or "C_sharp"
121+
pub fn from_str_sharp(s: &str) -> Option<Self> {
122+
let relative_midi_index = match s {
123+
"C" => 0,
124+
"C_sharp" => 1,
125+
"D" => 2,
126+
"D_sharp" => 3,
127+
"E" => 4,
128+
"F" => 5,
129+
"F_sharp" => 6,
130+
"G" => 7,
131+
"G_sharp" => 8,
132+
"A" => 9,
133+
"A_sharp" => 10,
134+
"B" => 11,
135+
_ => return None,
136+
};
137+
Some(Self {
138+
relative_midi_index,
139+
})
140+
}
141+
95142
const fn wrapping_add_semitones(self, num_semitones: i8) -> Self {
96143
Self::from_index(
97144
(self.to_index() as i8 + num_semitones)
@@ -160,7 +207,7 @@ impl Note {
160207
}
161208
}
162209

163-
pub fn to_midi_index(self) -> u8 {
210+
pub const fn to_midi_index(self) -> u8 {
164211
self.midi_index
165212
}
166213

@@ -174,11 +221,20 @@ impl Note {
174221
}
175222
}
176223

177-
pub fn octave(self) -> Octave {
178-
Octave::new(self.midi_index / NOTES_PER_OCTAVE)
224+
pub const fn octave(self) -> Octave {
225+
Octave::new((self.midi_index - C0_MIDI_INDEX) / NOTES_PER_OCTAVE)
179226
}
180227

181-
pub fn add_semitones_checked(self, num_semitones: i16) -> Option<Self> {
228+
pub const fn note_name(self) -> NoteName {
229+
NoteName::from_index(
230+
(self.midi_index - C0_MIDI_INDEX) % NOTES_PER_OCTAVE,
231+
)
232+
}
233+
234+
pub const fn add_semitones_checked(
235+
self,
236+
num_semitones: i16,
237+
) -> Option<Self> {
182238
let midi_index = self.midi_index as i16 + num_semitones;
183239
if midi_index < 0 || midi_index > MAX_MIDI_INDEX as i16 {
184240
None
@@ -189,21 +245,79 @@ impl Note {
189245
}
190246
}
191247

192-
pub fn add_octaves_checked(self, num_octaves: i8) -> Option<Self> {
248+
pub const fn add_octaves_checked(self, num_octaves: i8) -> Option<Self> {
193249
self.add_semitones_checked(num_octaves as i16 * NOTES_PER_OCTAVE as i16)
194250
}
195251

196-
pub fn add_semitones(self, num_semitones: i16) -> Self {
252+
pub const fn add_semitones(self, num_semitones: i16) -> Self {
197253
Self {
198254
midi_index: (self.midi_index as i16 + num_semitones) as u8,
199255
}
200256
}
201257

202-
pub fn add_octaves(self, num_octaves: i8) -> Self {
258+
pub const fn add_octaves(self, num_octaves: i8) -> Self {
203259
self.add_semitones(num_octaves as i16 * NOTES_PER_OCTAVE as i16)
204260
}
205261
}
206262

263+
/// Example formats: "C_sharp-4", "C-4"
264+
impl Display for Note {
265+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266+
write!(
267+
f,
268+
"{}-{}",
269+
self.note_name().to_str_sharp(),
270+
self.octave().index()
271+
)
272+
}
273+
}
274+
275+
/// Expected format: "C_sharp-4", "C-4"
276+
impl FromStr for Note {
277+
type Err = String;
278+
279+
fn from_str(s: &str) -> Result<Self, Self::Err> {
280+
let mut split = s.split("-");
281+
if let Some(name) = split.next() {
282+
if let Some(name) = NoteName::from_str_sharp(name) {
283+
if let Some(octave_index) = split.next() {
284+
match octave_index.parse::<u8>() {
285+
Ok(octave_index) => {
286+
if octave_index <= Octave::MAX_OCTAVE {
287+
if split.next().is_none() {
288+
Ok(Note::new(
289+
name,
290+
Octave::new(octave_index),
291+
))
292+
} else {
293+
Err(format!(
294+
"Multiple dashes in note string."
295+
))
296+
}
297+
} else {
298+
Err(format!(
299+
"Octave index {} too high (max is {}).",
300+
octave_index,
301+
Octave::MAX_OCTAVE
302+
))
303+
}
304+
}
305+
Err(e) => {
306+
Err(format!("Failed to parse octave index: {}", e))
307+
}
308+
}
309+
} else {
310+
Err(format!("No dash in note string."))
311+
}
312+
} else {
313+
Err(format!("Failed to parse note name: {}", name))
314+
}
315+
} else {
316+
Err(format!("Failed to parse note name."))
317+
}
318+
}
319+
}
320+
207321
pub trait IntoNoteFreqHz<N>
208322
where
209323
N: SigT<Item = Note>,
@@ -862,3 +976,27 @@ pub mod note {
862976
pub const B7: Note = Note::B7;
863977
pub const B8: Note = Note::B8;
864978
}
979+
980+
#[cfg(test)]
981+
mod test {
982+
use super::*;
983+
984+
#[test]
985+
fn octave_round_trip() {
986+
assert_eq!(Note::new(note_name::C, OCTAVE_0).octave(), OCTAVE_0);
987+
}
988+
989+
#[test]
990+
fn note_name_round_trip() {
991+
assert_eq!(Note::new(note_name::D, OCTAVE_3).note_name(), note_name::D);
992+
}
993+
994+
#[test]
995+
fn string_round_trip() {
996+
assert_eq!(note::D6.to_string().parse::<Note>().unwrap(), note::D6);
997+
assert_eq!(
998+
note::A_SHARP5.to_string().parse::<Note>().unwrap(),
999+
note::A_SHARP5
1000+
);
1001+
}
1002+
}

midi-udp-widgets-app/src/main.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
use caw_keyboard::Note;
1+
//! An executable which displays a widget and runs a UDP client which sends MIDI messages over a
2+
//! UDP socket to a server (specified as a command-line argument). A caw synthesizer is expected to
3+
//! run a UDP server which receives MIDI over UDP.
4+
5+
use caw_keyboard::{Note, note};
26
use caw_midi_udp_client::*;
3-
use caw_widgets::{Button, Knob, Xy};
7+
use caw_widgets::{Button, ComputerKeyboard, Knob, Xy};
48
use clap::{Parser, Subcommand};
59

610
#[derive(Subcommand)]
@@ -20,6 +24,10 @@ enum Command {
2024
#[arg(long, default_value_t = 1)]
2125
controller_y: u8,
2226
},
27+
ComputerKeyboard {
28+
#[arg(long, default_value_t = note::B2)]
29+
start_note: Note,
30+
},
2331
}
2432

2533
#[derive(Parser)]
@@ -105,5 +113,18 @@ fn main() {
105113
.unwrap();
106114
}
107115
}
116+
Command::ComputerKeyboard { start_note } => {
117+
let mut buf = Vec::new();
118+
let mut computer_keyboard =
119+
ComputerKeyboard::new(cli.title.as_deref(), start_note)
120+
.unwrap();
121+
loop {
122+
computer_keyboard.tick(&mut buf).unwrap();
123+
for message in buf.drain(..) {
124+
let midi_event = MidiEvent { channel, message };
125+
client.send(midi_event).unwrap();
126+
}
127+
}
128+
}
108129
}
109130
}

sdl2/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "caw_sdl2"
3+
version = "0.1.0"
4+
edition = "2024"
5+
description = "Helpers for interfacing with sdl2"
6+
license = "MIT"
7+
homepage = "https://github.com/gridbugs/caw.git"
8+
repository = "https://github.com/gridbugs/caw.git"
9+
documentation = "https://docs.rs/caw_sdl2"
10+
11+
[dependencies]
12+
sdl2 = "0.38"
13+
caw_computer_keyboard = { version = "0.4", path = "../computer-keyboard" }

0 commit comments

Comments
 (0)