From 676040a93eb1c988e2604cd65566ac868a878fc6 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Fri, 19 Apr 2024 21:25:04 -0400 Subject: [PATCH] implement basic piano - scan - configurable pins - no velocity yet --- src/bin/geode_piano.rs | 84 ++++++++++++++++++++++ src/lib.rs | 1 + src/matrix.rs | 70 +++++++++++++++++++ src/{midi.rs => midi/mod.rs} | 132 +++++++++++++++++++++++++++++++++-- src/midi/note_def.py | 15 ++++ 5 files changed, 295 insertions(+), 7 deletions(-) create mode 100644 src/bin/geode_piano.rs create mode 100644 src/matrix.rs rename src/{midi.rs => midi/mod.rs} (63%) create mode 100644 src/midi/note_def.py diff --git a/src/bin/geode_piano.rs b/src/bin/geode_piano.rs new file mode 100644 index 0000000..ea3fe49 --- /dev/null +++ b/src/bin/geode_piano.rs @@ -0,0 +1,84 @@ +/* + geode-piano + Copyright (C) 2024 dogeystamp + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +//! Main firmware for geode-piano. Reads key-matrix and sends MIDI output. + +#![no_std] +#![no_main] +#![deny(rust_2018_idioms)] + +use embassy_executor::Spawner; +use embassy_rp::bind_interrupts; +use embassy_rp::i2c; +use embassy_rp::peripherals::USB; +use embassy_rp::usb::{Driver, InterruptHandler}; +use geode_piano::usb::usb_task; +use geode_piano::matrix::KeyMatrix; +use geode_piano::{blinky, pin_array, pins, unwrap}; + +#[embassy_executor::task] +async fn piano_task(pin_driver: pins::TransparentPins) { + use geode_piano::midi::Note::*; + + // GND pins + let col_pins = [23]; + // Input pins + let row_pins = [20, 15, 4]; + // Notes for each key + let keymap = [[C4, D4, E4]]; + + let mut mat = KeyMatrix::new(col_pins, row_pins, keymap); + mat.scan(pin_driver).await; +} + +bind_interrupts!(struct Irqs { + USBCTRL_IRQ => InterruptHandler; +}); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + let driver = Driver::new(p.USB, Irqs); + unwrap(_spawner.spawn(usb_task(driver, log::LevelFilter::Debug))).await; + unwrap(_spawner.spawn(blinky::blink_task(p.PIN_25.into()))).await; + + log::debug!("main: init i2c"); + let sda = p.PIN_16; + let scl = p.PIN_17; + + let mut i2c_config = i2c::Config::default(); + let freq = 100_000; + i2c_config.frequency = freq; + let i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, i2c_config); + + log::debug!("main: starting transparent pin driver"); + let pin_driver = unwrap(pins::TransparentPins::new( + i2c, + [0x20, 0x27], + pin_array!( + p.PIN_15, p.PIN_14, p.PIN_13, p.PIN_12, p.PIN_11, p.PIN_10, p.PIN_9, p.PIN_18, + p.PIN_19, p.PIN_20, p.PIN_21, p.PIN_22 + ), + true, + )) + .await; + + log::info!("main: starting piano task"); + _spawner.spawn(piano_task(pin_driver)).unwrap(); +} diff --git a/src/lib.rs b/src/lib.rs index 42d666d..ad452d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod blinky; pub mod midi; pub mod pins; pub mod usb; +pub mod matrix; /// Unwrap, but log before panic /// diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..5a8cbf5 --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,70 @@ +//! Key matrix scanner + +use crate::pins; +use crate::midi; +use crate::unwrap; +use embassy_rp::gpio; +use embassy_time::{Duration, Ticker}; + +/// Key matrix for the piano. +pub struct KeyMatrix { + /// GND pins at the top of each column + col_pins: [u8; N_COLS], + /// Input pins at the left of each row + row_pins: [u8; N_ROWS], + keymap: [[midi::Note; N_ROWS]; N_COLS], +} + +impl KeyMatrix { + /// New function. + /// + /// `col_pins` are GND pins at the top of the columns, and `row_pins` are the input pins at + /// the ends of the rows. + /// + /// `keymap` represents the note that every combination of col/row gives. + pub fn new( + col_pins: [u8; N_COLS], + row_pins: [u8; N_ROWS], + keymap: [[midi::Note; N_ROWS]; N_COLS], + ) -> Self { + KeyMatrix { + col_pins, + row_pins, + keymap, + } + } + + pub async fn scan(&mut self, mut pin_driver: pins::TransparentPins) { + for i in pin_driver.pins { + unwrap(pin_driver.set_input(i)).await; + unwrap(pin_driver.set_pull(i, gpio::Pull::Up)).await; + } + let mut ticker = Ticker::every(Duration::from_millis(10)); + let chan = midi::MidiChannel::new(0); + let mut note_on = [false; 128]; + + loop { + for (i, col) in self.col_pins.iter().enumerate() { + unwrap(pin_driver.set_output(*col)).await; + let input = unwrap(pin_driver.read_all()).await; + unwrap(pin_driver.set_input(*col)).await; + + // values that are logical ON + let mask = input ^ (((1 << pin_driver.n_usable_pins()) - 1) ^ (1 << col)); + for (j, row) in self.row_pins.iter().enumerate() { + let note = self.keymap[i][j]; + if mask & (1 << row) != 0 { + if !note_on[note as usize] { + note_on[note as usize] = true; + chan.note_on(note, 40).await; + } + } else if note_on[note as usize] { + note_on[note as usize] = false; + chan.note_off(note, 0).await; + } + } + } + ticker.next().await; + } + } +} diff --git a/src/midi.rs b/src/midi/mod.rs similarity index 63% rename from src/midi.rs rename to src/midi/mod.rs index 1405896..ad00dbd 100644 --- a/src/midi.rs +++ b/src/midi/mod.rs @@ -24,14 +24,20 @@ use embassy_rp::usb::{Driver, Instance}; use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel}; use embassy_usb::{class::midi::MidiClass, driver::EndpointError}; +//////////////////////////////// +//////////////////////////////// +// MIDI message types +//////////////////////////////// +//////////////////////////////// + struct NoteMsg { on: bool, - note: u8, + note: Note, velocity: u8, } impl NoteMsg { - fn new(on: bool, note: u8, velocity: u8) -> Self { + fn new(on: bool, note: Note, velocity: u8) -> Self { NoteMsg { on, note, velocity } } } @@ -71,6 +77,118 @@ impl MidiMsg { } } +//////////////////////////////// +//////////////////////////////// +// Public MIDI interface +//////////////////////////////// +//////////////////////////////// + +/// Note identifiers +/// +/// See src/midi/note_def.py for how this is generated +#[derive(Clone, Copy)] +pub enum Note { + A0 = 21, + AS0 = 22, + B0 = 23, + C1 = 24, + CS1 = 25, + D1 = 26, + DS1 = 27, + E1 = 28, + F1 = 29, + FS1 = 30, + G1 = 31, + GS1 = 32, + A1 = 33, + AS1 = 34, + B1 = 35, + C2 = 36, + CS2 = 37, + D2 = 38, + DS2 = 39, + E2 = 40, + F2 = 41, + FS2 = 42, + G2 = 43, + GS2 = 44, + A2 = 45, + AS2 = 46, + B2 = 47, + C3 = 48, + CS3 = 49, + D3 = 50, + DS3 = 51, + E3 = 52, + F3 = 53, + FS3 = 54, + G3 = 55, + GS3 = 56, + A3 = 57, + AS3 = 58, + B3 = 59, + C4 = 60, + CS4 = 61, + D4 = 62, + DS4 = 63, + E4 = 64, + F4 = 65, + FS4 = 66, + G4 = 67, + GS4 = 68, + A4 = 69, + AS4 = 70, + B4 = 71, + C5 = 72, + CS5 = 73, + D5 = 74, + DS5 = 75, + E5 = 76, + F5 = 77, + FS5 = 78, + G5 = 79, + GS5 = 80, + A5 = 81, + AS5 = 82, + B5 = 83, + C6 = 84, + CS6 = 85, + D6 = 86, + DS6 = 87, + E6 = 88, + F6 = 89, + FS6 = 90, + G6 = 91, + GS6 = 92, + A6 = 93, + AS6 = 94, + B6 = 95, + C7 = 96, + CS7 = 97, + D7 = 98, + DS7 = 99, + E7 = 100, + F7 = 101, + FS7 = 102, + G7 = 103, + GS7 = 104, + A7 = 105, + AS7 = 106, + B7 = 107, + C8 = 108, + CS8 = 109, + D8 = 110, + DS8 = 111, + E8 = 112, + F8 = 113, + FS8 = 114, + G8 = 115, + GS8 = 116, + A8 = 117, + AS8 = 118, + B8 = 119, +} + pub struct Disconnected {} impl From for Disconnected { @@ -94,14 +212,14 @@ pub async fn midi_session<'d, T: Instance + 'd>( MsgType::Note(note) => { let status: u8 = (if note.on { 0b1001_0000 } else { 0b1000_0000 }) | msg.channel; // i'll be honest i have no idea where the first number here comes from - let packet = [8, status, note.note, note.velocity]; - log::debug!("midi_session: note {:?}", packet); + let packet = [8, status, note.note as u8, note.velocity]; + log::trace!("midi_session: note {:?}", packet); midi.write_packet(&packet).await? } MsgType::Controller(ctrl) => { let status: u8 = (0b1011_0000) | msg.channel; let packet = [8, status, ctrl.controller as u8, ctrl.value]; - log::debug!("midi_session: control {:?}", packet); + log::trace!("midi_session: control {:?}", packet); midi.write_packet(&packet).await? } } @@ -119,7 +237,7 @@ impl MidiChannel { } /// MIDI Note-On - pub async fn note_on(&self, note: u8, velocity: u8) { + pub async fn note_on(&self, note: Note, velocity: u8) { MIDI_QUEUE .send(MidiMsg::new( MsgType::Note(NoteMsg::new(true, note, velocity)), @@ -129,7 +247,7 @@ impl MidiChannel { } /// MIDI Note-Off - pub async fn note_off(&self, note: u8, velocity: u8) { + pub async fn note_off(&self, note: Note, velocity: u8) { MIDI_QUEUE .send(MidiMsg::new( MsgType::Note(NoteMsg::new(false, note, velocity)), diff --git a/src/midi/note_def.py b/src/midi/note_def.py new file mode 100644 index 0000000..010bc36 --- /dev/null +++ b/src/midi/note_def.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +"""Generates the MIDI `Note` enum based on https://gist.github.com/dimitre/439f5ab75a0c2e66c8c63fc9e8f7ea77.""" + +import csv + +with open("note_freq_440_432.csv") as f: + reader = csv.reader(f) + for row in reader: + note_code: int = int(row[0]) + note_name: str = row[1] + octave: int = int(row[2]) + + identifier = f"{note_name.replace('#', 'S')}{octave}" + print(f"{identifier} = {note_code},")