feat: more piano features

- basic velocity detection
- pedal
This commit is contained in:
dogeystamp 2024-04-20 14:19:32 -04:00
parent 676040a93e
commit 574105de7b
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
8 changed files with 160 additions and 27 deletions

2
Cargo.lock generated
View File

@ -771,7 +771,7 @@ dependencies = [
]
[[package]]
name = "geode-piano"
name = "geode_piano"
version = "0.1.0"
dependencies = [
"byte-slice-cast 1.2.2",

View File

@ -1,5 +1,5 @@
[package]
name = "geode-piano"
name = "geode_piano"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-only"

View File

@ -2,15 +2,17 @@
Digital piano firmware for the Raspberry Pi Pico.
This project only attempts to expose the keyboard as a MIDI device.
The purpose is to revive digital pianos that have working keys, but faulty electronics.
## installation
- Clone project.
- Go into project directory.
- Install the `thumbv6m-none-eabi` target using rustup.
- Install `elf2uf2-rs`.
- Follow the materials and wiring sections below.
- Set the Pico into BOOTSEL mode:
- Hold down the BOOTSEL button on the Pico. Keep holding it during the following steps.
- Hold down the BOOTSEL button on the Pico. Keep holding it during the following step.
- Reset the Pico: either replug the power, or short Pin 30 (RUN) to GND through a button or wire.
- Mount the Pico's storage on your device.
- `cargo run --release --bin [binary]`
@ -18,15 +20,44 @@ This project only attempts to expose the keyboard as a MIDI device.
If you are missing dependencies, consult [Alex Wilson's guide](https://www.alexdwilson.dev/learning-in-public/how-to-program-a-raspberry-pi-pico) on Rust Pico development.
## usage
The intended usage is to first plug the device into the piano keyboard, then use the `pin_scanner` binary to
scan the key-matrix. (See the next sections on how to wire it up.)
On every key, press it down half-way and then fully and note the pins connections detected at each level.
These correspond to the [`midi::KeyAction::N1`] and [`midi::KeyAction::N2`] actions respectively.
There should be two switches per key for velocity detection.
If there isn't, then the key is an [`midi::KeyAction::N`] (it will be stuck at a fixed velocity).
Put the connections in a spreadsheet and reorganize it so that GND pins are column headers, and the Input pins are row headers.
This will comprise the keymap.
The keymap is a an array with the same dimensions as the spreadsheet grid.
This is comprised of N1, N2, and N entries, indicating which note a key corresponds to.
Once the keymap is done, run the `piano_firmware` binary and plug the USB cable to your computer.
Open up a DAW and select Geode-Piano as a MIDI input device.
I use LMMS with the [Maestro Concert Grand v2](https://www.linuxsampler.org/instruments.html) samples.
You should be able to play now.
## materials
- 1 Raspberry Pi Pico (preferably with pre-soldered headers)
- 2 MCP23017 I/O extender chips, DIP format
- 2 pull-up resistors for I²C (1-10kΩ), these are optional but recommended
- 2 pull-up resistors for I²C (1-10kΩ), these are optional but [recommended](https://www.joshmcguigan.com/blog/internal-pull-up-resistor-i2c/)
- 1 USB to Micro-USB cable with data transfer
- Ribbon cable sockets. The following is for my own piano, yours might be different:
- 18-pin 1.25mm pitch FFC connector
- 22-pin 1.25mm pitch FFC connector
- Many jumper cables
- Breadboard
For the ribbon cable sockets, open up your piano and find the ribbon cables.
Unplug them from the PCB, and count the amount of pins on them.
Also, measure the distance between each pin,
or the distance between the first and last pin.
This will help you find the right pin pitch and pin count.
Usually, these measurements can be found on the datasheets for FFC sockets.
## wiring
**Ensure all wires are well plugged in every time you use this circuit.**
@ -54,3 +85,31 @@ For both MCP23017s:
- MCP A should be 0x20 (GND, GND, GND), MCP B 0x27 (3V3, 3V3, 3V3)
- MCP VDD -> power rail
- MCP VSS -> GND rail
### ribbon cables
Connect the following pins to the ribbon cable sockets in any order (use more or less pins depending on how many you need):
- GP15
- GP14
- GP13
- GP12
- GP11
- GP10
- GP9
- GP18
- GP19
- GP20
- GP21
- GP22
- All MCP GPIO pins except GPB7 and GPA7 on both chips (see [datasheet](https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf) for diagram of pins)
GPB7 and GPA7 have known issues and therefore can not be inputs.
Again, refer to the datasheet about this.
It is simpler to exclude them instead of working around that limitation.
I used male-to-female jumpers with the female end trimmed to reveal the metal part inside.
This was necessary to attach to the short pins on the jumpers.
The opening in the plastic on the female end should face inwards when connected to the sockets.
Then, plug the ribbon cables from your piano into the sockets.

View File

@ -27,21 +27,23 @@ 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;
use geode_piano::matrix::KeyMatrix;
use geode_piano::midi;
use geode_piano::usb::usb_task;
use geode_piano::{blinky, pin_array, pins, unwrap};
#[embassy_executor::task]
async fn piano_task(pin_driver: pins::TransparentPins) {
use geode_piano::midi::KeyAction::*;
use geode_piano::midi::Note::*;
// GND pins
let col_pins = [23];
let col_pins = [23, 24];
// Input pins
let row_pins = [20, 15, 4];
// Notes for each key
let keymap = [[C4, D4, E4]];
let keymap = [[N1(C4), N1(D4), N1(E4)], [N1(C4), N2(D4), N2(E4)]];
let mut mat = KeyMatrix::new(col_pins, row_pins, keymap);
mat.scan(pin_driver).await;
}
@ -63,7 +65,7 @@ async fn main(_spawner: Spawner) {
let scl = p.PIN_17;
let mut i2c_config = i2c::Config::default();
let freq = 100_000;
let freq = 400_000;
i2c_config.frequency = freq;
let i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, i2c_config);
@ -81,4 +83,12 @@ async fn main(_spawner: Spawner) {
log::info!("main: starting piano task");
_spawner.spawn(piano_task(pin_driver)).unwrap();
log::info!("main: starting sustain pedal task");
_spawner
.spawn(matrix::pedal(
midi::Controller::SustainPedal,
p.PIN_8.into(),
))
.unwrap();
}

View File

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![no_std]
#![no_main]
#![deny(rust_2018_idioms)]
@ -9,10 +8,10 @@ use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
pub mod blinky;
pub mod matrix;
pub mod midi;
pub mod pins;
pub mod usb;
pub mod matrix;
/// Unwrap, but log before panic
///

View File

@ -1,10 +1,24 @@
//! Key matrix scanner
//! Key matrix scanner + other interfacing utilities
use crate::pins;
use crate::midi;
use crate::pins;
use crate::unwrap;
use core::cmp::min;
use embassy_rp::gpio;
use embassy_time::{Duration, Ticker};
use embassy_time::{Duration, Instant, Ticker};
/// Task to handle pedals in MIDI
#[embassy_executor::task]
pub async fn pedal(pedal: midi::Controller, pin: gpio::AnyPin) {
let mut inp = gpio::Input::new(pin, gpio::Pull::Up);
let chan = midi::MidiChannel::new(0);
loop {
inp.wait_for_low().await;
chan.controller(pedal, 64).await;
inp.wait_for_high().await;
chan.controller(pedal, 0).await;
}
}
/// Key matrix for the piano.
pub struct KeyMatrix<const N_ROWS: usize, const N_COLS: usize> {
@ -12,7 +26,7 @@ pub struct KeyMatrix<const N_ROWS: usize, const N_COLS: usize> {
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],
keymap: [[midi::KeyAction; N_ROWS]; N_COLS],
}
impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
@ -25,7 +39,7 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
pub fn new(
col_pins: [u8; N_COLS],
row_pins: [u8; N_ROWS],
keymap: [[midi::Note; N_ROWS]; N_COLS],
keymap: [[midi::KeyAction; N_ROWS]; N_COLS],
) -> Self {
KeyMatrix {
col_pins,
@ -39,9 +53,18 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
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));
// scan frequency
// this might(?) panic if the scan takes longer than the tick
let mut ticker = Ticker::every(Duration::from_millis(2));
let chan = midi::MidiChannel::new(0);
let mut note_on = [false; 128];
const MAX_NOTES: usize = 128;
// is note currently on
let mut note_on = [false; MAX_NOTES];
// (for velocity detection) moment key is first touched
let mut note_first: [Option<Instant>; MAX_NOTES] = [None; MAX_NOTES];
loop {
for (i, col) in self.col_pins.iter().enumerate() {
@ -52,15 +75,46 @@ impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
// 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;
let key_action = self.keymap[i][j];
let key_active = mask & (1 << row) != 0;
match key_action {
midi::KeyAction::N1(note) => {
if !note_on[note as usize]
&& note_first[note as usize].is_none()
&& key_active
{
note_first[note as usize] = Some(Instant::now());
}
}
midi::KeyAction::N2(note) => {
if key_active {
if note_first[note as usize].is_some() && !note_on[note as usize] {
// millisecond duration of keypress
let dur =
note_first[note as usize].unwrap().elapsed().as_millis();
// 1905 millis is the minimum velocity
let velocity: u8 = 127 - min(dur / 15, 127) as u8;
note_first[note as usize] = None;
note_on[note as usize] = true;
chan.note_on(note, velocity).await;
}
} else if note_on[note as usize] {
note_on[note as usize] = false;
note_first[note as usize] = None;
chan.note_off(note, 0).await;
}
}
midi::KeyAction::N(note, velocity) => {
if key_active {
if !note_on[note as usize] {
note_on[note as usize] = true;
chan.note_on(note, velocity).await;
}
} else if note_on[note as usize] {
note_on[note as usize] = false;
chan.note_off(note, 0).await;
}
}
} else if note_on[note as usize] {
note_on[note as usize] = false;
chan.note_off(note, 0).await;
}
}
}

View File

@ -189,6 +189,16 @@ pub enum Note {
B8 = 119,
}
#[derive(Clone, Copy)]
pub enum KeyAction {
/// Switch that is first triggered when pressing a key.
N1(Note),
/// Switch triggered when key bottoms out.
N2(Note),
/// Basic switch with fixed velocity. Be careful not to mix with actions with velocity detection.
N(Note, u8),
}
pub struct Disconnected {}
impl From<EndpointError> for Disconnected {

View File

@ -1,5 +1,6 @@
//! Handle all USB communcation in this task.
//! If USB is handled in multiple tasks the code gets weird and unwieldy (`'static` everywhere)
//!
//! If USB is handled in multiple tasks the code gets weird and unwieldy (`'static` everywhere).
//! Code in this file is mostly from the examples folder in embassy-rs.
/*