Compare commits
4 Commits
676040a93e
...
ea70d75837
Author | SHA1 | Date | |
---|---|---|---|
ea70d75837 | |||
4fcc682406 | |||
444a86a4d4 | |||
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"
|
||||
|
83
README.md
83
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,18 +20,48 @@ 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
|
||||
- Many jumper cables
|
||||
- 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 (40 male-to-female, ? male-to-male)
|
||||
- Two alligator clips
|
||||
- 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.**
|
||||
**Ensure all wires, especially GND and power wires, are well plugged in every time you use this circuit.**
|
||||
|
||||
### rails
|
||||
|
||||
@ -54,3 +86,46 @@ 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.
|
||||
|
||||
Plugging this many jumper cables into a single socket can cause [crosstalk](https://en.m.wikipedia.org/wiki/Crosstalk).
|
||||
Twisting cables and spacing them from each other may help prevent this.
|
||||
To test for crosstalk, run `pin_scanner` and connect the socket contacts with each other with a jumper cable.
|
||||
Each scan should return exactly two connections, these being the forward and reverse connection over the jumper cable.
|
||||
If there are more connections, that means there is crosstalk.
|
||||
|
||||
Once the wiring is done, plug the ribbon cables from your piano into the sockets.
|
||||
|
||||
## sustain pedal
|
||||
|
||||
Using jumper wires and alligator clips, wire the Tip of the pedal's TRS jack into the GND rail.
|
||||
Then, wire the Ring (middle metal part, surrounded by two black bands), into the pedal pin (by default GP8).
|
||||
To attach the alligator clips to the [TRS jack](https://en.m.wikipedia.org/wiki/Phone_connector_(audio)#Design), you can strip the outer layer of a paperclip and wrap the metallic part around the jack.
|
||||
|
||||
Because the sustain pedal is normally-closed, failure to wire this appropriately could result in the sustain pedal being constantly on.
|
||||
To disable the sustain pedal, comment out the `pedal_task` in `src/bin/piano_firmware.rs`.
|
||||
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
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();
|
||||
}
|
495
src/bin/piano_firmware.rs
Normal file
495
src/bin/piano_firmware.rs
Normal file
@ -0,0 +1,495 @@
|
||||
/*
|
||||
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::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 = [32, 33, 34, 4, 36, 6, 7, 37, 38, 39, 15, 19, 24, 25, 26, 31];
|
||||
// Input pins
|
||||
let row_pins = [
|
||||
1, 2, 3, 5, 8, 9, 10, 12, 13, 14, 16, 17, 18, 20, 21, 22, 23, 27, 28, 29, 30, 35,
|
||||
];
|
||||
// Notes for each key
|
||||
let keymap = [
|
||||
[
|
||||
N1(GS5),
|
||||
N1(AS5),
|
||||
N1(C6),
|
||||
NOP,
|
||||
N1(F5),
|
||||
NOP,
|
||||
NOP,
|
||||
N1(G5),
|
||||
N1(A5),
|
||||
N1(B5),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(FS5),
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(F1),
|
||||
NOP,
|
||||
N1(A1),
|
||||
N1(G1),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(B1),
|
||||
N1(C2),
|
||||
N1(GS1),
|
||||
N1(AS1),
|
||||
N1(FS1),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(A0),
|
||||
NOP,
|
||||
N1(CS1),
|
||||
N1(B0),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(DS1),
|
||||
N1(E1),
|
||||
N1(C1),
|
||||
N1(D1),
|
||||
N1(AS0),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(CS2),
|
||||
NOP,
|
||||
N1(F2),
|
||||
N1(DS2),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(G2),
|
||||
N1(GS2),
|
||||
N1(E2),
|
||||
N1(FS2),
|
||||
N1(D2),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(A0),
|
||||
NOP,
|
||||
N2(CS1),
|
||||
N2(B0),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(DS1),
|
||||
N2(E1),
|
||||
N2(C1),
|
||||
N2(D1),
|
||||
N2(AS0),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(F1),
|
||||
NOP,
|
||||
N2(A1),
|
||||
N2(G1),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(B1),
|
||||
N2(C2),
|
||||
N2(GS1),
|
||||
N2(AS1),
|
||||
N2(FS1),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
N2(GS5),
|
||||
N2(AS5),
|
||||
N2(C6),
|
||||
NOP,
|
||||
N2(F5),
|
||||
NOP,
|
||||
NOP,
|
||||
N2(G5),
|
||||
N2(A5),
|
||||
N2(B5),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(FS5),
|
||||
],
|
||||
[
|
||||
N2(C7),
|
||||
N2(D7),
|
||||
N2(E7),
|
||||
NOP,
|
||||
N2(A6),
|
||||
NOP,
|
||||
NOP,
|
||||
N2(B6),
|
||||
N2(CS7),
|
||||
N2(DS7),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(AS6),
|
||||
],
|
||||
[
|
||||
N2(E6),
|
||||
N2(FS6),
|
||||
N2(GS6),
|
||||
NOP,
|
||||
N2(CS6),
|
||||
NOP,
|
||||
NOP,
|
||||
N2(DS6),
|
||||
N2(F6),
|
||||
N2(G6),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(D6),
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(A2),
|
||||
NOP,
|
||||
N1(CS3),
|
||||
N1(B2),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(DS3),
|
||||
N1(E3),
|
||||
N1(C3),
|
||||
N1(D3),
|
||||
N1(AS2),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(CS4),
|
||||
NOP,
|
||||
N1(F4),
|
||||
N1(DS4),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(G4),
|
||||
N1(GS4),
|
||||
N1(E4),
|
||||
N1(FS4),
|
||||
N1(D4),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(F3),
|
||||
NOP,
|
||||
N1(A3),
|
||||
N1(G3),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(B3),
|
||||
N1(C4),
|
||||
N1(GS3),
|
||||
N1(AS3),
|
||||
N1(FS3),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(A2),
|
||||
NOP,
|
||||
N2(CS3),
|
||||
N2(B2),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(DS3),
|
||||
N2(E3),
|
||||
N2(C3),
|
||||
N2(D3),
|
||||
N2(AS2),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(A4),
|
||||
NOP,
|
||||
N1(CS5),
|
||||
N1(B4),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(DS5),
|
||||
N1(E5),
|
||||
N1(C5),
|
||||
N1(D5),
|
||||
N1(AS4),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(A4),
|
||||
NOP,
|
||||
N2(CS5),
|
||||
N2(B4),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(DS5),
|
||||
N2(E5),
|
||||
N2(C5),
|
||||
N2(D5),
|
||||
N2(AS4),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(F3),
|
||||
NOP,
|
||||
N2(A3),
|
||||
N2(G3),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(B3),
|
||||
N2(C4),
|
||||
N2(GS3),
|
||||
N2(AS3),
|
||||
N2(FS3),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(CS4),
|
||||
NOP,
|
||||
N2(F4),
|
||||
N2(DS4),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(G4),
|
||||
N2(GS4),
|
||||
N2(E4),
|
||||
N2(FS4),
|
||||
N2(D4),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(CS2),
|
||||
NOP,
|
||||
N2(F2),
|
||||
N2(DS2),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(G2),
|
||||
N2(GS2),
|
||||
N2(E2),
|
||||
N2(FS2),
|
||||
N2(D2),
|
||||
NOP,
|
||||
],
|
||||
[
|
||||
N1(E6),
|
||||
N1(FS6),
|
||||
N1(GS6),
|
||||
NOP,
|
||||
N1(CS6),
|
||||
NOP,
|
||||
NOP,
|
||||
N1(DS6),
|
||||
N1(F6),
|
||||
N1(G6),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(D6),
|
||||
],
|
||||
[
|
||||
N1(C7),
|
||||
N1(D7),
|
||||
N1(E7),
|
||||
NOP,
|
||||
N1(A6),
|
||||
NOP,
|
||||
NOP,
|
||||
N1(B6),
|
||||
N1(CS7),
|
||||
N1(DS7),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(AS6),
|
||||
],
|
||||
[
|
||||
N1(GS7),
|
||||
N1(AS7),
|
||||
N1(C8),
|
||||
NOP,
|
||||
N1(F7),
|
||||
NOP,
|
||||
NOP,
|
||||
N1(G7),
|
||||
N1(A7),
|
||||
N1(B7),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N1(FS7),
|
||||
],
|
||||
[
|
||||
N2(GS7),
|
||||
N2(AS7),
|
||||
N2(C8),
|
||||
NOP,
|
||||
N2(F7),
|
||||
NOP,
|
||||
NOP,
|
||||
N2(G7),
|
||||
N2(A7),
|
||||
N2(B7),
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
NOP,
|
||||
N2(FS7),
|
||||
],
|
||||
];
|
||||
|
||||
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 = 400_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();
|
||||
|
||||
log::info!("main: starting sustain pedal task");
|
||||
_spawner
|
||||
.spawn(matrix::pedal(
|
||||
midi::Controller::SustainPedal,
|
||||
p.PIN_8.into(),
|
||||
false,
|
||||
))
|
||||
.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
|
||||
///
|
||||
|
105
src/matrix.rs
105
src/matrix.rs
@ -1,10 +1,30 @@
|
||||
//! 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
|
||||
///
|
||||
/// `norm_open` represents a normally open switch
|
||||
#[embassy_executor::task]
|
||||
pub async fn pedal(pedal: midi::Controller, pin: gpio::AnyPin, norm_open: bool) {
|
||||
let mut inp = gpio::Input::new(pin, gpio::Pull::Up);
|
||||
let chan = midi::MidiChannel::new(0);
|
||||
loop {
|
||||
let on_val = if norm_open { 64 } else { 0 };
|
||||
let off_val = if norm_open { 0 } else { 64 };
|
||||
inp.wait_for_low().await;
|
||||
chan.controller(pedal, on_val).await;
|
||||
log::debug!("{pedal:?} set to {on_val}");
|
||||
inp.wait_for_high().await;
|
||||
chan.controller(pedal, off_val).await;
|
||||
log::debug!("{pedal:?} set to {off_val}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Key matrix for the piano.
|
||||
pub struct KeyMatrix<const N_ROWS: usize, const N_COLS: usize> {
|
||||
@ -12,7 +32,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_COLS]; N_ROWS],
|
||||
}
|
||||
|
||||
impl<const N_ROWS: usize, const N_COLS: usize> KeyMatrix<N_ROWS, N_COLS> {
|
||||
@ -25,7 +45,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_COLS]; N_ROWS],
|
||||
) -> Self {
|
||||
KeyMatrix {
|
||||
col_pins,
|
||||
@ -39,11 +59,27 @@ 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(8));
|
||||
|
||||
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];
|
||||
|
||||
let mut counter = 0;
|
||||
|
||||
loop {
|
||||
counter += 1;
|
||||
counter %= 50;
|
||||
let profile = counter == 0;
|
||||
let prof_start = Instant::now();
|
||||
|
||||
for (i, col) in self.col_pins.iter().enumerate() {
|
||||
unwrap(pin_driver.set_output(*col)).await;
|
||||
let input = unwrap(pin_driver.read_all()).await;
|
||||
@ -52,18 +88,57 @@ 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[j][i];
|
||||
let key_active = mask & (1 << row) != 0;
|
||||
match key_action {
|
||||
midi::KeyAction::N1(note) => {
|
||||
if key_active {
|
||||
if note_first[note as usize].is_none() {
|
||||
note_first[note as usize] = Some(Instant::now());
|
||||
}
|
||||
} else if note_first[note as usize].is_some() {
|
||||
note_first[note as usize] = None;
|
||||
}
|
||||
}
|
||||
} else if note_on[note as usize] {
|
||||
note_on[note as usize] = false;
|
||||
chan.note_off(note, 0).await;
|
||||
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();
|
||||
let velocity: u8 = if dur <= 80 {
|
||||
(127 - dur) as u8
|
||||
} else {
|
||||
(127 - min(dur, 250) / 5 - 70) as u8
|
||||
};
|
||||
log::debug!("{note:?} velocity {velocity} from dur {dur}ms");
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
midi::KeyAction::NOP => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if profile {
|
||||
log::trace!("profile: scan took {}ms", prof_start.elapsed().as_millis())
|
||||
}
|
||||
ticker.next().await;
|
||||
}
|
||||
}
|
||||
|
69
src/midi/keymap.py
Normal file
69
src/midi/keymap.py
Normal file
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Helper to generate keymaps.
|
||||
|
||||
Takes in data through stdin with this format:
|
||||
|
||||
[note name] [GND pin]
|
||||
[n1 input pin]
|
||||
[n2 input pin]
|
||||
|
||||
Use cargo fmt to de-messify the output once pasted in the source.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
|
||||
|
||||
@dataclass
|
||||
class Note:
|
||||
note_name: str
|
||||
gnd_pin: int
|
||||
n1_pin: int
|
||||
n2_pin: int
|
||||
|
||||
|
||||
data: list[Note] = []
|
||||
col_dedup: set[int] = set()
|
||||
row_dedup: set[int] = set()
|
||||
|
||||
for i in range(88):
|
||||
note_name, gnd_pin = input().split()
|
||||
gnd_pin = int(gnd_pin)
|
||||
n1_pin = int(input())
|
||||
n2_pin = int(input())
|
||||
data.append(Note(note_name, gnd_pin, n1_pin, n2_pin))
|
||||
col_dedup.add(gnd_pin)
|
||||
row_dedup.add(n1_pin)
|
||||
row_dedup.add(n2_pin)
|
||||
|
||||
col_pins = list(col_dedup)
|
||||
row_pins = list(row_dedup)
|
||||
|
||||
# [row][column]
|
||||
mat = [["" for _ in range(len(col_pins))] for _ in range(len(row_pins))]
|
||||
|
||||
for d in data:
|
||||
# this is inefficient but whatever
|
||||
mat[row_pins.index(d.n1_pin)][col_pins.index(d.gnd_pin)] = f"N1({d.note_name})"
|
||||
mat[row_pins.index(d.n1_pin)][col_pins.index(d.gnd_pin)] = f"NOP"
|
||||
mat[row_pins.index(d.n2_pin)][col_pins.index(d.gnd_pin)] = f"N({d.note_name}, 64)"
|
||||
|
||||
empty_counter = 0
|
||||
|
||||
for i in range(len(mat)):
|
||||
for j in range(len(mat[i])):
|
||||
if mat[i][j] == "":
|
||||
mat[i][j] = "NOP"
|
||||
empty_counter += 1
|
||||
|
||||
|
||||
print("[")
|
||||
for col in mat:
|
||||
print(f"[{', '.join(col)}],")
|
||||
print("]")
|
||||
|
||||
print(f"{len(row_pins)} rows, {len(col_pins)} cols", file=sys.stderr)
|
||||
print(f"row pins: [{', '.join([str(i) for i in row_pins])}]", file=sys.stderr)
|
||||
print(f"col pins: [{', '.join([str(i) for i in col_pins])}]", file=sys.stderr)
|
||||
print(f"{empty_counter} empty cells", file=sys.stderr)
|
@ -42,7 +42,7 @@ impl NoteMsg {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Controller {
|
||||
SustainPedal = 64,
|
||||
}
|
||||
@ -86,7 +86,7 @@ impl MidiMsg {
|
||||
/// Note identifiers
|
||||
///
|
||||
/// See src/midi/note_def.py for how this is generated
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Note {
|
||||
A0 = 21,
|
||||
AS0 = 22,
|
||||
@ -189,6 +189,18 @@ 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),
|
||||
/// NOP
|
||||
NOP,
|
||||
}
|
||||
|
||||
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