diff --git a/Cargo.lock b/Cargo.lock index e6f976c..a140bc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,7 +771,7 @@ dependencies = [ ] [[package]] -name = "geode-piano" +name = "geode_piano" version = "0.1.0" dependencies = [ "byte-slice-cast 1.2.2", diff --git a/Cargo.toml b/Cargo.toml index 5d4c800..6dbc4f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "geode-piano" +name = "geode_piano" version = "0.1.0" edition = "2021" license = "GPL-3.0-only" diff --git a/README.md b/README.md index 64c76bb..9e29acd 100644 --- a/README.md +++ b/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. diff --git a/src/bin/geode_piano.rs b/src/bin/piano_firmware.rs similarity index 86% rename from src/bin/geode_piano.rs rename to src/bin/piano_firmware.rs index ea3fe49..3bfb222 100644 --- a/src/bin/geode_piano.rs +++ b/src/bin/piano_firmware.rs @@ -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(); } diff --git a/src/lib.rs b/src/lib.rs index ad452d8..0e6b469 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 /// diff --git a/src/matrix.rs b/src/matrix.rs index 5a8cbf5..4d7bf5d 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -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 { @@ -12,7 +26,7 @@ pub struct KeyMatrix { 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 KeyMatrix { @@ -25,7 +39,7 @@ impl KeyMatrix { 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 KeyMatrix { 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; MAX_NOTES] = [None; MAX_NOTES]; loop { for (i, col) in self.col_pins.iter().enumerate() { @@ -52,15 +75,46 @@ impl KeyMatrix { // 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; } } } diff --git a/src/midi/mod.rs b/src/midi/mod.rs index ad00dbd..b485718 100644 --- a/src/midi/mod.rs +++ b/src/midi/mod.rs @@ -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 for Disconnected { diff --git a/src/usb.rs b/src/usb.rs index 5d814d0..219445e 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -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. /*