Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af86289acd | |||
| 103cd75096 |
@@ -20,14 +20,14 @@
|
||||
|
||||
Open-KeyPrec is an open-sourced, pressure-sensitive electric keyboard percussion (mallet) MIDI instrument. It has 61 keys, spanning 5 octaves of playing range from C2 to C7 like a standard Marimba. It also has a MIDI control panel with 16 buttons, 12 knobs (8 × 280° potentiometers and 4 × 360° rotary encoders).
|
||||
|
||||
The sensors are controlled by a development board, and the output is converted into a standard MIDI device. You can use audio workstation softwares like GarageBand or Reaper (or open-source alternatives like LMMS, Zrythm, or Ardour) to simulate the sounds of all kinds of different instruments.
|
||||
The sensors are controlled by an ESP32 development board, and the output is converted into a standard MIDI device. You can use audio workstation softwares like GarageBand or Reaper (or open-source alternatives like LMMS, Zrythm, or Ardour) to simulate the sounds of all kinds of different instruments.
|
||||
|
||||
## Code
|
||||
|
||||
**Source code structure**
|
||||
|
||||
* `/src` - PC driver written in Rust
|
||||
* `/firmware` - Development board firmware written in C++
|
||||
* `/firmware` - ESP32 module firmware written in C++
|
||||
* `/circuits` - Printed circuit boards
|
||||
* `/models` - 3D-printable models
|
||||
|
||||
@@ -38,22 +38,6 @@ The sensors are controlled by a development board, and the output is converted i
|
||||
* CLion: IDE for Rust & C++
|
||||
* PlatformIO: Firmware development toolkit
|
||||
|
||||
**Pin Configuration**
|
||||
|
||||
| Pins | I/O | Description |
|
||||
|------|-----|-------------------------------------------|
|
||||
| 4 | O | Mux Select for C2-B6 |
|
||||
| 5 | IA | Mux Input for C2-B6 |
|
||||
| 1 | IA | Input for C7 |
|
||||
| 8 | I | (Panel) Input for 4 rotary encoders |
|
||||
| 3 | O | Mux Select for buttons and potentiometers |
|
||||
| 2 | I | Mux Input for buttons |
|
||||
| 1 | IA | Mux Input for potentiometers |
|
||||
| 3 | O | RGB LED Control |
|
||||
|
||||
Total ADC channels required: 7
|
||||
Total GPIO required: 23
|
||||
|
||||
## Build
|
||||
|
||||
### Materials Required
|
||||
@@ -69,36 +53,10 @@ Total GPIO required: 23
|
||||
|
||||
### 1. Order PCBs
|
||||
|
||||
I ordered my PCBs from JLC, but any PCB service would work. They should accept the exported Gerber zip files in `circuits/Exports` folder.
|
||||
|
||||
I created two circuit boards for this keyboard, one is the multiplexer ([Mux v2.zip](circuits/Mux%20v2.zip)) which uses the CD74HC4067 1:16 multiplexer ic to convert the 61 separate analog inputs requirement into only 5 analog inputs and 4 digital switch outputs.
|
||||
It also haso diodes and resistors for voltage protection. The other pcb ([OKP-Panel-SMT.zip](circuits/OKP-Panel-SMT.zip)) is for the MIDI control panel containing the buttons and knobs.
|
||||
|
||||
1. Go to jlcpcb.com (Or jlc.com if you read Chinese)
|
||||
2. Upload the Gerber file
|
||||
3. Set parameters: FR-4, 2 Layers, Color, leave default for other options
|
||||
|
||||
**PCB Assembly (SMT)**
|
||||
|
||||
If you're not comfortable soldering small SMD components, you can order PCB Assembly from JLC.
|
||||
You can also select which parts you would like to SMT based on your soldering abilities.
|
||||
For example, in the Mux pcb, I ordered SMT for only the 0603 Zener diodes and the CD74HC4067 ic, and hand-soldered everything else.
|
||||
|
||||
1. Order "PCB Assembly" (SMT) and click confirm
|
||||
2. In the SMT menu, upload the BOM and CPL (PickAndPlace) files
|
||||
3. After uploading, remove the parts you want to manually solder (they're pretty expensive, since someone in the factory still have to manually solder them for you)
|
||||
4. Order manually-soldered parts somewhere else (e.g. szlcsc.com)
|
||||
!!!!!!!!!! TODO !!!!!!!!!!
|
||||
|
||||
## Usage
|
||||
|
||||
### Flash Firmware
|
||||
|
||||
1. Install PlatformIO Core ([Guide](https://platformio.org/install/cli))
|
||||
2. Open the `firmware` directory
|
||||
3. In your preferred IDE, edit `src/config.h` with your pin configuration.
|
||||
4. Edit `platformio.ini` to match your dev board.
|
||||
5. Connect dev board, build and upload firmware using `platformio run --target upload`
|
||||
|
||||
### Setup MIDI Device
|
||||
|
||||
!!!!!!!!!! TODO !!!!!!!!!!
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
-17
@@ -13,16 +13,6 @@
|
||||
;board = wemos_d1_uno32
|
||||
;framework = arduino
|
||||
|
||||
[env:s3]
|
||||
platform = espressif32
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
|
||||
lib_deps =
|
||||
adafruit/Adafruit NeoPixel@^1.11.0
|
||||
paulstoffregen/Encoder@^1.4.2
|
||||
|
||||
; ESP32-S2 and C3 both has a problem where the ADC jumps around a LOT
|
||||
;[env:s2]
|
||||
;platform = espressif32
|
||||
;board = lolin_s2_mini
|
||||
@@ -33,14 +23,11 @@ lib_deps =
|
||||
;board = dfrobot_beetle_esp32c3
|
||||
;framework = arduino
|
||||
|
||||
; YD STM32F411CEU6
|
||||
;[env:stm32f411ce]
|
||||
;platform = ststm32
|
||||
;board = blackpill_f411ce
|
||||
;framework = arduino
|
||||
;upload_protocol = dfu
|
||||
[env:s3]
|
||||
platform = espressif32
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
|
||||
; Arduino nano is good but too few pins
|
||||
;[env:n328p]
|
||||
;platform = atmelavr
|
||||
;board = nanoatmega328
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#ifndef FIRMWARE_CONFIG_H
|
||||
#define FIRMWARE_CONFIG_H
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
// ========================================
|
||||
// Main Keyboard Pin Configuration
|
||||
// ========================================
|
||||
|
||||
// LED indicator for pulling update
|
||||
const int LED_REFRESH = 45;
|
||||
|
||||
// 4 1:16 Multiplexers: GPIO pins for each analog multiplexer that handles 12 sensors of an octave
|
||||
// Notes are connected in order: Mux #1 (0-11), Mux #2 (12-23), Mux #3 (24-35), Mux #4 (36-47), Mux #5 (48-59)
|
||||
const int NUM_MUX = 5;
|
||||
const int PINS_PER_MUX = 12;
|
||||
const int MUX_IN[NUM_MUX] = {4, 5, 6, 7, 8};
|
||||
|
||||
// Select pins for every multiplexer, each multiplexer has 4 select pins, all sel0 are connected to 14, etc.
|
||||
const int NUM_MUX_SEL = 4;
|
||||
const int MUX_SEL_OUT[NUM_MUX_SEL] = {14, 13, 12, 11};
|
||||
|
||||
// LED Light strip
|
||||
const int LK_PIN = 2;
|
||||
const int LK_LIGHTS_PER_METER = 60;
|
||||
constexpr float LK_NUM_METERS = 2;
|
||||
const int LK_KEY_SPACING_MM = 7;
|
||||
const int LK_KEY_LEN_MM = 30;
|
||||
const int LK_OFFSET_MM = 25; // Length in mm from the start of the strip to the first key
|
||||
const float LK_LIGHTS_PER_MM = LK_LIGHTS_PER_METER / 1000.0;
|
||||
constexpr int LK_NUM_LIGHTS = (int) (LK_LIGHTS_PER_METER * LK_NUM_METERS + 0.5);
|
||||
|
||||
// ========================================
|
||||
// MIDI Panel Pin Configuration
|
||||
// ========================================
|
||||
|
||||
// 3 1:8 Multiplexers for the midi panel
|
||||
const int P_PINS_PER_MUX = 8;
|
||||
const int P_KEY_MUX_IN = 36;
|
||||
const int P_BUTTON_MUX_IN = 35; // Digital signal inputs for button multiplexers
|
||||
const int P_KNOB_MUX_IN = 10; // Analog signal inputs for potentiometer
|
||||
|
||||
// Select pins for every multiplexer, each multiplexer has 3 select pins
|
||||
const int P_NUM_MUX_SEL = 3;
|
||||
const int P_MUX_SEL_OUT[P_NUM_MUX_SEL] = {37, 38, 39};
|
||||
|
||||
// Rotary encoder pins
|
||||
const int P_NUM_ROTARY = 4;
|
||||
const int P_ROTARY_A[P_NUM_ROTARY] = {18, 17, 16, 15};
|
||||
const int P_ROTARY_B[P_NUM_ROTARY] = {19, 20, 21, 47};
|
||||
|
||||
// LED light strips
|
||||
const int P_LED_KEY = 42;
|
||||
const int P_LED_KNOB = 41;
|
||||
const int P_LED_ROTARY = 40;
|
||||
|
||||
// ========================================
|
||||
// Constants
|
||||
// ========================================
|
||||
|
||||
typedef struct note
|
||||
{
|
||||
char name[4];
|
||||
u16 midi;
|
||||
} Note;
|
||||
|
||||
// 61 Notes from C2 to C7
|
||||
const int NUM_NOTES = 61;
|
||||
const int NUM_SHARP_NOTES = 25;
|
||||
const int NUM_REGULAR_NOTES = NUM_NOTES - NUM_SHARP_NOTES;
|
||||
|
||||
const Note notes[] = {
|
||||
{"C2", 36}, {"C#2", 37}, {"D2", 38}, {"D#2", 39}, {"E2", 40}, {"F2", 41}, {"F#2", 42}, {"G2", 43}, {"G#2", 44}, {"A2", 45}, {"A#2", 46}, {"B2", 47},
|
||||
{"C3", 48}, {"C#3", 49}, {"D3", 50}, {"D#3", 51}, {"E3", 52}, {"F3", 53}, {"F#3", 54}, {"G3", 55}, {"G#3", 56}, {"A3", 57}, {"A#3", 58}, {"B3", 59},
|
||||
{"C4", 60}, {"C#4", 61}, {"D4", 62}, {"D#4", 63}, {"E4", 64}, {"F4", 65}, {"F#4", 66}, {"G4", 67}, {"G#4", 68}, {"A4", 69}, {"A#4", 70}, {"B4", 71},
|
||||
{"C5", 72}, {"C#5", 73}, {"D5", 74}, {"D#5", 75}, {"E5", 76}, {"F5", 77}, {"F#5", 78}, {"G5", 79}, {"G#5", 80}, {"A5", 81}, {"A#5", 82}, {"B5", 83},
|
||||
{"C6", 84}, {"C#6", 85}, {"D6", 86}, {"D#6", 87}, {"E6", 88}, {"F6", 89}, {"F#6", 90}, {"G6", 91}, {"G#6", 92}, {"A6", 93}, {"A#6", 94}, {"B6", 95},
|
||||
{"C7", 96}
|
||||
};
|
||||
|
||||
#endif //FIRMWARE_CONFIG_H
|
||||
@@ -1,127 +0,0 @@
|
||||
//
|
||||
// Created by Hykilpikonna on 4/21/23.
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "utils.h"
|
||||
#include "Adafruit_NeoPixel.h"
|
||||
#include <unordered_map>
|
||||
#include <queue>
|
||||
|
||||
const int ANIM_QUEUE_SIZE = 100;
|
||||
const int FRAME_DELAY_MS = 20;
|
||||
|
||||
using namespace std;
|
||||
|
||||
class KeyboardLights
|
||||
{
|
||||
private:
|
||||
Adafruit_NeoPixel led_key;
|
||||
unordered_map<int, int> key_to_light;
|
||||
TaskHandle_t thread{};
|
||||
|
||||
u32 animation[ANIM_QUEUE_SIZE][LK_NUM_LIGHTS]{};
|
||||
u32 last_frame[LK_NUM_LIGHTS]{};
|
||||
int anim_index = 0;
|
||||
|
||||
static int wrap(int i) { return (i + LK_NUM_LIGHTS) % LK_NUM_LIGHTS; }
|
||||
|
||||
void update()
|
||||
{
|
||||
// Render this frame
|
||||
auto frame = animation[anim_index];
|
||||
for (int i = 0; i < LK_NUM_LIGHTS; i++)
|
||||
{
|
||||
if (frame[i] == 0 && last_frame[i] == 0) continue;
|
||||
last_frame[i] = frame[i];
|
||||
led_key.setPixelColor(i, frame[i]);
|
||||
}
|
||||
led_key.show();
|
||||
|
||||
// Clear the current frame
|
||||
for (int i = 0; i < LK_NUM_LIGHTS; i++) frame[i] = 0;
|
||||
anim_index = (anim_index + 1) % ANIM_QUEUE_SIZE;
|
||||
}
|
||||
|
||||
[[noreturn]] static void loop(void *pv)
|
||||
{
|
||||
auto *lights = (KeyboardLights *) pv;
|
||||
|
||||
while (true)
|
||||
{
|
||||
lights->update();
|
||||
vTaskDelay(FRAME_DELAY_MS / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
KeyboardLights() : led_key(LK_NUM_LIGHTS, LK_PIN, NEO_GRB + NEO_KHZ800)
|
||||
{
|
||||
// Initialize key to light map
|
||||
int regi = 0;
|
||||
for (int i = NUM_NOTES - 1; i >= 0; i--)
|
||||
{
|
||||
auto note = notes[i];
|
||||
auto ki = (float) regi;
|
||||
if (note.name[1] == '#') ki -= 0.5;
|
||||
|
||||
// Calculate the length from the start of the keyboard to the key
|
||||
ki *= LK_KEY_SPACING_MM + LK_KEY_LEN_MM;
|
||||
ki += LK_KEY_LEN_MM / 2.0; // Center of the key
|
||||
|
||||
// Calculate the index of the light at the same length position
|
||||
ki *= LK_LIGHTS_PER_MM;
|
||||
|
||||
// Convert key index to mm, then to light index
|
||||
key_to_light[i] = (int) round(ki);
|
||||
|
||||
// Increment the regular note index if it's not a sharp note
|
||||
if (note.name[1] != '#') regi++;
|
||||
}
|
||||
}
|
||||
|
||||
void begin()
|
||||
{
|
||||
pinModeSafe(LK_PIN, OUTPUT);
|
||||
|
||||
led_key.begin();
|
||||
xTaskCreate(loop, "loopLights", 4096, this, 1, &thread);
|
||||
}
|
||||
|
||||
void hit(int key)
|
||||
{
|
||||
// 1. Calculate the starting index
|
||||
int start = key_to_light[key];
|
||||
|
||||
Serial.printf("Key %d -> Light %d\n", key, start);
|
||||
|
||||
// 2. Start animation
|
||||
|
||||
// Fade out for one light
|
||||
// for (int i = 0; i < 50; i++)
|
||||
// {
|
||||
// animation[(anim_index + i) % ANIM_QUEUE_SIZE][start] = Adafruit_NeoPixel::ColorHSV(0, 255, (50 - i) * 2);
|
||||
// }
|
||||
|
||||
// Fade out from the center to both sides
|
||||
int hue = random(0, 65535);
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
// Pick a random hue
|
||||
Serial.printf("Hue: %d\n", hue);
|
||||
int v = (25 - i) * 2;
|
||||
int s = 255;
|
||||
auto frame = animation[(anim_index + i) % ANIM_QUEUE_SIZE];
|
||||
|
||||
frame[start] = Adafruit_NeoPixel::ColorHSV(hue, s, v);
|
||||
|
||||
// Sides
|
||||
for (int j = 1; j <= min(i, 10); j++)
|
||||
{
|
||||
frame[(start + j) % LK_NUM_LIGHTS] = Adafruit_NeoPixel::ColorHSV(hue, s, MAX(v - j * 2, 0));
|
||||
frame[(start - j + LK_NUM_LIGHTS) % LK_NUM_LIGHTS] = Adafruit_NeoPixel::ColorHSV(hue, s, MAX(v - j * 2, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+140
-100
@@ -1,89 +1,79 @@
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "Adafruit_NeoPixel.h"
|
||||
#include "main.h"
|
||||
#include "utils.h"
|
||||
#include "panel.cpp"
|
||||
#include "keyboard_lights.cpp"
|
||||
#include <chrono>
|
||||
#include <cinttypes>
|
||||
#include <driver/adc.h>
|
||||
|
||||
u32 lasts[NUM_NOTES]; // variable to store the value coming from the sensor
|
||||
#define u8 uint8_t
|
||||
#define u16 uint16_t
|
||||
#define u32 uint32_t
|
||||
#define u64 uint64_t
|
||||
#define timeMillis() std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count()
|
||||
#define min(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define let auto
|
||||
#define val const auto
|
||||
|
||||
u64 start_time = 0;
|
||||
|
||||
typedef struct note {
|
||||
char name[4];
|
||||
u16 midi;
|
||||
} Note;
|
||||
|
||||
|
||||
// 61 Notes from C2 to C7
|
||||
val NUM_NOTES = 61;
|
||||
const Note notes[] = {
|
||||
{"C2", 36}, {"C#2", 37}, {"D2", 38}, {"D#2", 39}, {"E2", 40}, {"F2", 41}, {"F#2", 42}, {"G2", 43}, {"G#2", 44}, {"A2", 45}, {"A#2", 46}, {"B2", 47},
|
||||
{"C3", 48}, {"C#3", 49}, {"D3", 50}, {"D#3", 51}, {"E3", 52}, {"F3", 53}, {"F#3", 54}, {"G3", 55}, {"G#3", 56}, {"A3", 57}, {"A#3", 58}, {"B3", 59},
|
||||
{"C4", 60}, {"C#4", 61}, {"D4", 62}, {"D#4", 63}, {"E4", 64}, {"F4", 65}, {"F#4", 66}, {"G4", 67}, {"G#4", 68}, {"A4", 69}, {"A#4", 70}, {"B4", 71},
|
||||
{"C5", 72}, {"C#5", 73}, {"D5", 74}, {"D#5", 75}, {"E5", 76}, {"F5", 77}, {"F#5", 78}, {"G5", 79}, {"G#5", 80}, {"A5", 81}, {"A#5", 82}, {"B5", 83},
|
||||
{"C6", 84}, {"C#6", 85}, {"D6", 86}, {"D#6", 87}, {"E6", 88}, {"F6", 89}, {"F#6", 90}, {"G6", 91}, {"G#6", 92}, {"A6", 93}, {"A#6", 94}, {"B6", 95},
|
||||
{"C7", 96}
|
||||
};
|
||||
|
||||
// LED pins
|
||||
val PIN_LED = 37;
|
||||
let led_state = true;
|
||||
|
||||
// 5 Multiplexers: GPIO pins for each analog multiplexer that handles 12 sensors (1 octave)
|
||||
// Notes are connected in order: Mux #1 (0-11), Mux #2 (12-23), Mux #3 (24-35), Mux #4 (36-47), Mux #5 (48-59)
|
||||
val NUM_MUX = 5;
|
||||
val PINS_PER_MUX = 12;
|
||||
const adc1_channel_t mux_in[] = {ADC1_CHANNEL_1, ADC1_CHANNEL_2, ADC1_CHANNEL_3, ADC1_CHANNEL_4, ADC1_CHANNEL_5};
|
||||
//const int mux_in[] = {A0, A1, A2, A3, A4};
|
||||
|
||||
// Select pins for every multiplexer, each multiplexer has 4 select pins, all sel0 are connected to 14, etc.
|
||||
val NUM_MUX_SEL = 4;
|
||||
const int mux_sel[] = {40, 38, 36, 34};
|
||||
|
||||
// Multisampling: Take multiple samples for each sensor and average them
|
||||
val MULTISAMPLING = 1;
|
||||
|
||||
int lasts[NUM_NOTES]; // variable to store the value coming from the sensor
|
||||
u64 last_hit_times[NUM_NOTES];
|
||||
u32 bounce_delay = 50; // Minimum time between two hits
|
||||
|
||||
let max_sensor = 4096;
|
||||
let max_threshold = 3000;
|
||||
let active_threshold = 400; // Minimum value to be considered as a hit
|
||||
|
||||
let led_refresh_on = false;
|
||||
|
||||
Panel panel;
|
||||
KeyboardLights keyboardLights;
|
||||
val max_sensor = 4096;
|
||||
val max_threshold = 2000;
|
||||
val active_threshold = 100; // Minimum value to be considered as a hit
|
||||
|
||||
void setup()
|
||||
{
|
||||
// Initialize pins
|
||||
pinModeSafe(LED_REFRESH, OUTPUT);
|
||||
for (int pin: MUX_IN) pinModeSafe(pin, INPUT);
|
||||
for (int pin: MUX_SEL_OUT) pinModeSafe(pin, OUTPUT);
|
||||
// Turn on LED
|
||||
pinMode(PIN_LED, OUTPUT);
|
||||
digitalWrite(PIN_LED, led_state);
|
||||
|
||||
// Initialize serial
|
||||
Serial.begin(115200);
|
||||
Serial.printf("Initialized\r\n");
|
||||
|
||||
panel.begin();
|
||||
keyboardLights.begin();
|
||||
}
|
||||
|
||||
u64 fps_last_update = 0;
|
||||
u32 fps_updates = 0;
|
||||
const u32 fps_interval_ms = 1000;
|
||||
|
||||
void countFps(u64 time)
|
||||
{
|
||||
// Report FPS every second
|
||||
fps_updates++;
|
||||
if (time - fps_last_update >= fps_interval_ms)
|
||||
{
|
||||
fps_last_update = time;
|
||||
double fps = 1.0 * fps_updates / fps_interval_ms * 1000;
|
||||
Serial.printf("FPS: %.2f\r\n", fps);
|
||||
fps_updates = 0;
|
||||
// Initialize pin and serial
|
||||
for (int pin: mux_in) pinMode(pin, INPUT);
|
||||
adc1_config_width(ADC_WIDTH_MAX);
|
||||
for (val pin : mux_in) {
|
||||
adcAttachPin(pin);
|
||||
adc1_config_channel_atten(pin, ADC_ATTEN_DB_11);
|
||||
}
|
||||
}
|
||||
// for (val pin: mux_sel) pinMode(pin, OUTPUT);
|
||||
Serial.begin(9600);
|
||||
Serial.println("Initialized");
|
||||
|
||||
void readKeyboard()
|
||||
{
|
||||
u64 time = millis();
|
||||
countFps(time);
|
||||
|
||||
// Toggle LED refresh indicator
|
||||
digitalWrite(LED_REFRESH, led_refresh_on = !led_refresh_on);
|
||||
|
||||
// Loop through each multiplexer state
|
||||
for (int i = 0; i < PINS_PER_MUX; i++)
|
||||
{
|
||||
// Set select pins
|
||||
for (int j = 0; j < NUM_MUX_SEL; j++)
|
||||
{
|
||||
// i >> j is the jth bit of i
|
||||
digitalWrite(MUX_SEL_OUT[j], (i >> j) & 1);
|
||||
}
|
||||
|
||||
// Read input pins from the multiplexer
|
||||
for (int j = 0; j < NUM_MUX; j++)
|
||||
{
|
||||
int note_id = j * PINS_PER_MUX + i;
|
||||
if (note_id >= NUM_NOTES) break;
|
||||
|
||||
// Read the analog input
|
||||
u32 v = analogRead(MUX_IN[j]);
|
||||
if (v != lasts[note_id])
|
||||
{
|
||||
on_sensor_update(note_id, time, lasts[note_id], v);
|
||||
}
|
||||
lasts[note_id] = v;
|
||||
}
|
||||
}
|
||||
// start_time = timeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,40 +81,90 @@ void readKeyboard()
|
||||
*
|
||||
* @param id Sensor index
|
||||
*/
|
||||
void on_sensor_update(int id, u64 time, u32 last, u32 current)
|
||||
void on_sensor_update(int id, u64 time, int last, int current)
|
||||
{
|
||||
// If the last hit is too close, ignore this hit
|
||||
let last_hit_time = last_hit_times[id];
|
||||
if (time - last_hit_time < bounce_delay) return;
|
||||
|
||||
// If the value is above the threshold
|
||||
if (current > active_threshold)
|
||||
// If the last value is larger than the current value, check timeout
|
||||
if (last > current && last > active_threshold)
|
||||
{
|
||||
// If the last value is below the threshold, it's a new hit
|
||||
if (last < active_threshold)
|
||||
u64 elapsed = time - last_hit_times[id];
|
||||
if (elapsed < 150)
|
||||
{
|
||||
// Send MIDI message
|
||||
// /hit <note> <velocity>
|
||||
Serial.printf("/hit %d %d\r\n", notes[id].midi,
|
||||
MIN((last - active_threshold) * 127 / (max_threshold - active_threshold), 127));
|
||||
|
||||
// Lights
|
||||
keyboardLights.hit(id);
|
||||
// If the last hit is too close, ignore this hit
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (last > active_threshold)
|
||||
{
|
||||
// Released
|
||||
|
||||
// The last value is a local maximum,
|
||||
// and we read it as the hit strength of our note. Send midi command to the host.
|
||||
// /hit <note> <velocity>
|
||||
Serial.printf("/hit %d %d\r\n", notes[id].midi,
|
||||
min((last - active_threshold) * 127 / (max_threshold - active_threshold), 127));
|
||||
|
||||
// Update last hit time
|
||||
last_hit_times[id] = time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read sensor value with multisampling
|
||||
*
|
||||
* @param pin Sensor pin
|
||||
* @return Sensor value
|
||||
*/
|
||||
int read_sensor(int pin)
|
||||
{
|
||||
let sum = 0;
|
||||
for (let i = 0; i < MULTISAMPLING; i++) {
|
||||
sum += analogRead(pin);
|
||||
}
|
||||
return (int) round(((float) sum) / ((float) MULTISAMPLING));
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
readKeyboard();
|
||||
// u64 time = timeMillis();
|
||||
// u64 elapsed = time - start_time;
|
||||
|
||||
// for (int i = 0; i < LK_NUM_LIGHTS; i++)
|
||||
// Toggle LED
|
||||
led_state = !led_state;
|
||||
digitalWrite(PIN_LED, led_state);
|
||||
delay(100);
|
||||
|
||||
// // Loop through each multiplexer input
|
||||
for (val mux : mux_in)
|
||||
{
|
||||
val v = read_sensor(mux);
|
||||
Serial.printf("%d ", v);
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
// Serial.printf("%" PRIu64 "=============\r\n", elapsed);
|
||||
|
||||
// Loop through each multiplexer state
|
||||
// for (int i = 0; i < PINS_PER_MUX; i++)
|
||||
// {
|
||||
// lk.setPixelColor(i, Adafruit_NeoPixel::ColorHSV(last_hue + i * hue_interval, 255, brightness));
|
||||
// // Set select pins
|
||||
// for (int j = 0; j < NUM_MUX_SEL; j++)
|
||||
// {
|
||||
// // i >> j is the jth bit of i
|
||||
// digitalWrite(mux_sel[j], (i >> j) & 1);
|
||||
// }
|
||||
//
|
||||
// // Read four input pins from the multiplexer
|
||||
// for (int j = 0; j < NUM_MUX; j++)
|
||||
// {
|
||||
// int note_id = j * PINS_PER_MUX + i;
|
||||
// if (note_id >= NUM_NOTES) break;
|
||||
//
|
||||
// // Read the analog input
|
||||
// int v = read_sensor(mux_in[j]);
|
||||
// if (v != lasts[note_id])
|
||||
// {
|
||||
// // Serial prints are really slow, so don't use them in debug mode
|
||||
// Serial.printf("%s %d\r\n", notes[note_id].name, v);
|
||||
// on_sensor_update(note_id, time, lasts[note_id], v);
|
||||
// }
|
||||
// lasts[note_id] = v;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// Serial.printf("Loop %" PRIu64 "\r\n", elapsed);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Created by Hykilpikonna on 4/13/23.
|
||||
//
|
||||
|
||||
#ifndef FIRMWARE_MAIN_H
|
||||
#define FIRMWARE_MAIN_H
|
||||
|
||||
/**
|
||||
* Called when the sensor value changes
|
||||
*
|
||||
* @param id Sensor index
|
||||
*/
|
||||
void on_sensor_update(int id, u64 time, u32 last, u32 current);
|
||||
|
||||
#endif //FIRMWARE_MAIN_H
|
||||
@@ -1,184 +0,0 @@
|
||||
//
|
||||
// Created by Hykilpikonna on 4/21/23.
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "Adafruit_NeoPixel.h"
|
||||
#include "Encoder.h"
|
||||
|
||||
/**
|
||||
* Class controlling the MIDI panel
|
||||
*/
|
||||
class Panel {
|
||||
private:
|
||||
Adafruit_NeoPixel led_key;
|
||||
Adafruit_NeoPixel led_knob;
|
||||
Adafruit_NeoPixel led_rotary;
|
||||
|
||||
u16 last_hue = 0;
|
||||
u8 brightness = 40;
|
||||
|
||||
bool key_states[P_PINS_PER_MUX]{};
|
||||
bool btn_states[P_PINS_PER_MUX]{};
|
||||
u32 pot_states[P_PINS_PER_MUX]{};
|
||||
|
||||
Encoder *encoders[P_NUM_ROTARY]{};
|
||||
int encoder_states[P_NUM_ROTARY]{};
|
||||
|
||||
TaskHandle_t panelThread{};
|
||||
|
||||
public:
|
||||
Panel() :
|
||||
led_key(4, P_LED_KEY, NEO_GRB + NEO_KHZ800),
|
||||
led_knob(9, P_LED_KNOB, NEO_GRB + NEO_KHZ800),
|
||||
led_rotary(9, P_LED_ROTARY, NEO_GRB + NEO_KHZ800)
|
||||
{}
|
||||
|
||||
void begin()
|
||||
{
|
||||
for (int pin: P_MUX_SEL_OUT) pinModeSafe(pin, OUTPUT);
|
||||
for (int pin: P_ROTARY_A) pinModeSafe(pin, INPUT);
|
||||
for (int pin: P_ROTARY_B) pinModeSafe(pin, INPUT);
|
||||
pinModeSafe(P_BUTTON_MUX_IN, INPUT);
|
||||
pinModeSafe(P_KEY_MUX_IN, INPUT);
|
||||
pinModeSafe(P_KNOB_MUX_IN, INPUT);
|
||||
pinModeSafe(P_LED_KEY, OUTPUT);
|
||||
pinModeSafe(P_LED_KNOB, OUTPUT);
|
||||
pinModeSafe(P_LED_ROTARY, OUTPUT);
|
||||
|
||||
led_key.begin();
|
||||
led_knob.begin();
|
||||
led_rotary.begin();
|
||||
|
||||
// Initialize encoders
|
||||
for (int i = 0; i < P_NUM_ROTARY; i++)
|
||||
{
|
||||
encoders[i] = new Encoder(P_ROTARY_A[i], P_ROTARY_B[i]);
|
||||
}
|
||||
|
||||
xTaskCreate(loopPanel, "loopPanel", 4096, this, 1, &panelThread);
|
||||
}
|
||||
|
||||
private:
|
||||
void readPanel()
|
||||
{
|
||||
const auto hue_interval = 512;
|
||||
last_hue += hue_interval;
|
||||
|
||||
// Read rotary encoders
|
||||
for (int i = 0; i < P_NUM_ROTARY; ++i)
|
||||
{
|
||||
int state = encoders[i]->read();
|
||||
if (encoder_states[i] != state)
|
||||
{
|
||||
encoder_states[i] = state;
|
||||
Serial.printf("Rotary changed - id: %d, value: %d\r\n", i, state);
|
||||
led_rotary.setPixelColor(i, Adafruit_NeoPixel::ColorHSV(last_hue, 255, brightness));
|
||||
led_rotary.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Read buttons
|
||||
for (int i = 0; i < P_PINS_PER_MUX; ++i)
|
||||
{
|
||||
// Set select pins
|
||||
for (int j = 0; j < P_NUM_MUX_SEL; ++j)
|
||||
{
|
||||
// i >> j is the jth bit of i
|
||||
digitalWrite(P_MUX_SEL_OUT[j], (i >> j) & 1);
|
||||
}
|
||||
vTaskDelay(1);
|
||||
|
||||
// Read button
|
||||
int key = !digitalRead(P_KEY_MUX_IN);
|
||||
int btn = !digitalRead(P_BUTTON_MUX_IN);
|
||||
|
||||
// If the state is changed, call button callback
|
||||
if (key_states[i] != key)
|
||||
{
|
||||
key_states[i] = key;
|
||||
onKey(i, key);
|
||||
}
|
||||
|
||||
if (btn_states[i] != btn)
|
||||
{
|
||||
btn_states[i] = btn;
|
||||
onBtn(i, btn);
|
||||
}
|
||||
|
||||
// Read potentiometer
|
||||
int pot = (int) round(analogRead(P_KNOB_MUX_IN) / 16.0);
|
||||
onPotRead(i, pot);
|
||||
|
||||
// If the state is changed, call potentiometer callback
|
||||
if (ABS(pot_states[i] - pot) > 4)
|
||||
{
|
||||
pot_states[i] = pot;
|
||||
onPotChange(i, pot);
|
||||
}
|
||||
}
|
||||
|
||||
delay(10);
|
||||
led_key.show();
|
||||
led_knob.show();
|
||||
led_rotary.show();
|
||||
}
|
||||
|
||||
void onKey(int id, bool state)
|
||||
{
|
||||
// Check if it's one of the larger keys (the first 4)
|
||||
if (id < 4)
|
||||
{
|
||||
if (state)
|
||||
{
|
||||
// Set a random color for the key's LED
|
||||
led_key.setPixelColor(id, Adafruit_NeoPixel::ColorHSV(random(0, 65535), 255, brightness));
|
||||
led_key.show();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear the key's LED
|
||||
led_key.setPixelColor(id, 0);
|
||||
led_key.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Key 5 = clear
|
||||
if (id == 4 && state)
|
||||
{
|
||||
led_key.clear();
|
||||
led_key.show();
|
||||
}
|
||||
|
||||
Serial.printf("Key changed - id: %d, state: %d\r\n", id, state);
|
||||
}
|
||||
|
||||
void onBtn(int id, bool state)
|
||||
{
|
||||
Serial.printf("Button changed - id: %d, state: %d\r\n", id, state);
|
||||
}
|
||||
|
||||
void onPotRead(int id, u8 value)
|
||||
{
|
||||
// Set LED
|
||||
led_knob.setPixelColor(id, Adafruit_NeoPixel::ColorHSV(last_hue, 255, value));
|
||||
led_knob.show();
|
||||
}
|
||||
|
||||
void onPotChange(int id, u8 value)
|
||||
{
|
||||
// Serial.printf("Potentiometer changed - id: %d, value: %d\r\n", id, value);
|
||||
}
|
||||
|
||||
[[noreturn]] static void loopPanel(void* pvParameters)
|
||||
{
|
||||
auto* panel = (Panel*) pvParameters;
|
||||
while (true)
|
||||
{
|
||||
panel->readPanel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#ifndef FIRMWARE_UTILS_H
|
||||
#define FIRMWARE_UTILS_H
|
||||
|
||||
#define u8 uint8_t
|
||||
#define u16 uint16_t
|
||||
#define u32 uint32_t
|
||||
#define u64 uint64_t
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define MAX(a, b) ((a) > (b) ? (a) : (b))
|
||||
#define ABS(a) ((a) < 0 ? -(a) : (a))
|
||||
#define let auto
|
||||
|
||||
#define pinModeSafe(pin, mode) do { if (pin != -1) pinMode(pin, mode); } while (false)
|
||||
|
||||
#endif //FIRMWARE_UTILS_H
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-23
@@ -10,29 +10,7 @@ fn start() -> Result<()> {
|
||||
let midi_out = MidiOutput::new("MIDI Output")?;
|
||||
let mut conn_out = midi_out.create_virtual("Virtual MIDI Output").unwrap();
|
||||
|
||||
// List serial devices
|
||||
let ports = serialport::available_ports()?;
|
||||
|
||||
// If there are no ports available, quit
|
||||
if ports.is_empty() {
|
||||
println!("No serial ports found.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Let the user choose a port
|
||||
println!("Available serial ports:");
|
||||
for (i, p) in ports.iter().enumerate() {
|
||||
println!("{}: {}", i, p.port_name);
|
||||
}
|
||||
println!("Choose a port (range 0-{}): ", ports.len() - 1);
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
let choice = input.trim().parse::<usize>().unwrap();
|
||||
let port = &ports[choice];
|
||||
|
||||
// Open serial port
|
||||
let serial_port = serialport::new(port.port_name.as_str(), 115200)
|
||||
let serial_port = serialport::new("/dev/cu.usbmodem14501", 9600)
|
||||
.timeout(std::time::Duration::from_millis(10))
|
||||
.open()?;
|
||||
let mut reader = BufReader::new(serial_port);
|
||||
|
||||
Reference in New Issue
Block a user