implement basic piano
- scan - configurable pins - no velocity yet
This commit is contained in:
parent
6dabe79aa5
commit
676040a93e
84
src/bin/geode_piano.rs
Normal file
84
src/bin/geode_piano.rs
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
geode-piano
|
||||
Copyright (C) 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! 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<USB>;
|
||||
});
|
||||
|
||||
#[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();
|
||||
}
|
@ -12,6 +12,7 @@ pub mod blinky;
|
||||
pub mod midi;
|
||||
pub mod pins;
|
||||
pub mod usb;
|
||||
pub mod matrix;
|
||||
|
||||
/// Unwrap, but log before panic
|
||||
///
|
||||
|
70
src/matrix.rs
Normal file
70
src/matrix.rs
Normal file
@ -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<const N_ROWS: usize, const N_COLS: usize> {
|
||||
/// 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<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<EndpointError> 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)),
|
15
src/midi/note_def.py
Normal file
15
src/midi/note_def.py
Normal file
@ -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},")
|
Loading…
Reference in New Issue
Block a user