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]]
|
[[package]]
|
||||||
name = "geode-piano"
|
name = "geode_piano"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byte-slice-cast 1.2.2",
|
"byte-slice-cast 1.2.2",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "geode-piano"
|
name = "geode_piano"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
63
README.md
63
README.md
@ -2,15 +2,17 @@
|
|||||||
|
|
||||||
Digital piano firmware for the Raspberry Pi Pico.
|
Digital piano firmware for the Raspberry Pi Pico.
|
||||||
This project only attempts to expose the keyboard as a MIDI device.
|
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
|
## installation
|
||||||
|
|
||||||
- Clone project.
|
- Clone project.
|
||||||
- Go into project directory.
|
- Go into project directory.
|
||||||
|
- Install the `thumbv6m-none-eabi` target using rustup.
|
||||||
- Install `elf2uf2-rs`.
|
- Install `elf2uf2-rs`.
|
||||||
- Follow the materials and wiring sections below.
|
- Follow the materials and wiring sections below.
|
||||||
- Set the Pico into BOOTSEL mode:
|
- 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.
|
- 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.
|
- Mount the Pico's storage on your device.
|
||||||
- `cargo run --release --bin [binary]`
|
- `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.
|
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
|
## materials
|
||||||
|
|
||||||
- 1 Raspberry Pi Pico (preferably with pre-soldered headers)
|
- 1 Raspberry Pi Pico (preferably with pre-soldered headers)
|
||||||
- 2 MCP23017 I/O extender chips, DIP format
|
- 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
|
- 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
|
- Many jumper cables
|
||||||
- Breadboard
|
- 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
|
## wiring
|
||||||
|
|
||||||
**Ensure all wires are well plugged in every time you use this circuit.**
|
**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 A should be 0x20 (GND, GND, GND), MCP B 0x27 (3V3, 3V3, 3V3)
|
||||||
- MCP VDD -> power rail
|
- MCP VDD -> power rail
|
||||||
- MCP VSS -> GND 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::i2c;
|
||||||
use embassy_rp::peripherals::USB;
|
use embassy_rp::peripherals::USB;
|
||||||
use embassy_rp::usb::{Driver, InterruptHandler};
|
use embassy_rp::usb::{Driver, InterruptHandler};
|
||||||
use geode_piano::usb::usb_task;
|
use geode_piano::matrix;
|
||||||
use geode_piano::matrix::KeyMatrix;
|
use geode_piano::matrix::KeyMatrix;
|
||||||
|
use geode_piano::midi;
|
||||||
|
use geode_piano::usb::usb_task;
|
||||||
use geode_piano::{blinky, pin_array, pins, unwrap};
|
use geode_piano::{blinky, pin_array, pins, unwrap};
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
async fn piano_task(pin_driver: pins::TransparentPins) {
|
async fn piano_task(pin_driver: pins::TransparentPins) {
|
||||||
|
use geode_piano::midi::KeyAction::*;
|
||||||
use geode_piano::midi::Note::*;
|
use geode_piano::midi::Note::*;
|
||||||
|
|
||||||
// GND pins
|
// GND pins
|
||||||
let col_pins = [23];
|
let col_pins = [23, 24];
|
||||||
// Input pins
|
// Input pins
|
||||||
let row_pins = [20, 15, 4];
|
let row_pins = [20, 15, 4];
|
||||||
// Notes for each key
|
// 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);
|
let mut mat = KeyMatrix::new(col_pins, row_pins, keymap);
|
||||||
mat.scan(pin_driver).await;
|
mat.scan(pin_driver).await;
|
||||||
}
|
}
|
||||||
@ -63,7 +65,7 @@ async fn main(_spawner: Spawner) {
|
|||||||
let scl = p.PIN_17;
|
let scl = p.PIN_17;
|
||||||
|
|
||||||
let mut i2c_config = i2c::Config::default();
|
let mut i2c_config = i2c::Config::default();
|
||||||
let freq = 100_000;
|
let freq = 400_000;
|
||||||
i2c_config.frequency = freq;
|
i2c_config.frequency = freq;
|
||||||
let i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, i2c_config);
|
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");
|
log::info!("main: starting piano task");
|
||||||
_spawner.spawn(piano_task(pin_driver)).unwrap();
|
_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")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
#![deny(rust_2018_idioms)]
|
#![deny(rust_2018_idioms)]
|
||||||
@ -9,10 +8,10 @@ use embassy_time::Timer;
|
|||||||
use {defmt_rtt as _, panic_probe as _};
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
pub mod blinky;
|
pub mod blinky;
|
||||||
|
pub mod matrix;
|
||||||
pub mod midi;
|
pub mod midi;
|
||||||
pub mod pins;
|
pub mod pins;
|
||||||
pub mod usb;
|
pub mod usb;
|
||||||
pub mod matrix;
|
|
||||||
|
|
||||||
/// Unwrap, but log before panic
|
/// 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::midi;
|
||||||
|
use crate::pins;
|
||||||
use crate::unwrap;
|
use crate::unwrap;
|
||||||
|
use core::cmp::min;
|
||||||
use embassy_rp::gpio;
|
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.
|
/// Key matrix for the piano.
|
||||||
pub struct KeyMatrix<const N_ROWS: usize, const N_COLS: usize> {
|
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],
|
col_pins: [u8; N_COLS],
|
||||||
/// Input pins at the left of each row
|
/// Input pins at the left of each row
|
||||||
row_pins: [u8; N_ROWS],
|
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> {
|
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(
|
pub fn new(
|
||||||
col_pins: [u8; N_COLS],
|
col_pins: [u8; N_COLS],
|
||||||
row_pins: [u8; N_ROWS],
|
row_pins: [u8; N_ROWS],
|
||||||
keymap: [[midi::Note; N_ROWS]; N_COLS],
|
keymap: [[midi::KeyAction; N_ROWS]; N_COLS],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
KeyMatrix {
|
KeyMatrix {
|
||||||
col_pins,
|
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_input(i)).await;
|
||||||
unwrap(pin_driver.set_pull(i, gpio::Pull::Up)).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 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 {
|
loop {
|
||||||
for (i, col) in self.col_pins.iter().enumerate() {
|
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
|
// values that are logical ON
|
||||||
let mask = input ^ (((1 << pin_driver.n_usable_pins()) - 1) ^ (1 << col));
|
let mask = input ^ (((1 << pin_driver.n_usable_pins()) - 1) ^ (1 << col));
|
||||||
for (j, row) in self.row_pins.iter().enumerate() {
|
for (j, row) in self.row_pins.iter().enumerate() {
|
||||||
let note = self.keymap[i][j];
|
let key_action = self.keymap[i][j];
|
||||||
if mask & (1 << row) != 0 {
|
let key_active = mask & (1 << row) != 0;
|
||||||
if !note_on[note as usize] {
|
match key_action {
|
||||||
note_on[note as usize] = true;
|
midi::KeyAction::N1(note) => {
|
||||||
chan.note_on(note, 40).await;
|
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,
|
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 {}
|
pub struct Disconnected {}
|
||||||
|
|
||||||
impl From<EndpointError> for Disconnected {
|
impl From<EndpointError> for Disconnected {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
//! Handle all USB communcation in this task.
|
//! 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.
|
//! Code in this file is mostly from the examples folder in embassy-rs.
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
Loading…
Reference in New Issue
Block a user