feat: more piano features
- basic velocity detection - pedal
This commit is contained in:
parent
676040a93e
commit
574105de7b
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -771,7 +771,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "geode-piano"
|
||||
name = "geode_piano"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"byte-slice-cast 1.2.2",
|
||||
|
@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "geode-piano"
|
||||
name = "geode_piano"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-only"
|
||||
|
63
README.md
63
README.md
@ -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.
|
||||
|
@ -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();
|
||||
}
|
@ -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
|
||||
///
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
||||
/*
|
||||
|
Loading…
Reference in New Issue
Block a user