Quick background - I have working code that lights up LEDs on a strip corresponding to MIDI output from a piano keyboard. Press key(s) = Light up LED(s).
It works great as it is - I want to add more functionality and I am relatively novice and don't know how to approach coding what I want to do.
Goal is, trigger little animations on each key pressed (e.g. little pulse spreading across many LEDs centered around the piano key that was pressed).
The key to this I know will be getting 'concurrency' to work meaning many animations happening at the same time that overlap.
For that part - I found what appears to be a good example here Concurrent Fireworks - Wokwi ESP32, STM32, Arduino Simulator which I'd like to adapt into my current code. It's just too over my head though, and I was hoping for some expert guidance here.
If I could get a little firework popping off above each key played, that would be awesome.
Current MIDI code is as follows.
/*
* Copyright (c) 2022 Dave Dribin
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Arduino.h>
#include <FastLED.h>
#include <MIDI.h>
static const int NUM_LEDS = 64;
static const int START_LED = 0;
// Hardware SPI of ESP8266
#define DATA_PIN D7
#define CLOCK_PIN D5
CRGB leds[NUM_LEDS];
static const int NUM_KEYS = 76;
bool keys[NUM_KEYS];
byte pedal = 0;
byte velocities[NUM_KEYS]; // Declaration of velocities array
MIDI_CREATE_DEFAULT_INSTANCE();
static void handleNoteOn(byte channel, byte note, byte velocity);
static void handleNoteOff(byte channel, byte note, byte velocity);
static void handleControlChange(byte channel, byte number, byte value);
// Integer division with rounding using only integer operations
// https://stackoverflow.com/questions/2422712/rounding-integer-division-instead-of-truncating/2422723#2422723
// https://blog.pkh.me/p/36-figuring-out-round%2C-floor-and-ceil-with-integer-division.html
long div_round_closest(long dividend, long divisor)
{
return (dividend + (divisor / 2)) / divisor;
}
// Custom version of map() that rounds fractional values instead of truncating.
long my_map(long x, long in_min, long in_max, long out_min, long out_max)
{
long numerator = (x - in_min) * (out_max - out_min);
long denominator = (in_max - in_min);
long result = div_round_closest(numerator, denominator) + out_min;
return result;
}
void setup()
{
FastLED.addLeds<SK9822, DATA_PIN, CLOCK_PIN>(leds, NUM_LEDS);
FastLED.setBrightness(30);
MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
MIDI.setHandleControlChange(handleControlChange);
for (int i = 0; i < NUM_KEYS; i++) {
keys[i] = false;
}
}
void loop()
{
MIDI.read();
int saturation = 255;
int brightness = 255;
// First, clear the existing led values
FastLED.clear();
bool anyKeyDown = false;
for (int i = 0; i < NUM_KEYS; i++) {
if (keys[i]) {
int led = my_map(i, 0, NUM_KEYS-1, START_LED, NUM_LEDS-1);
int hue = my_map(i, 0, NUM_KEYS-1, 0, 255);
// Calculate brightness based on velocity
byte velocity = velocities[i];
// Calculate brightness based on velocity
brightness = my_map(velocity, 0, 127, 30, 255);
leds[led] = CHSV(hue, saturation, brightness);
anyKeyDown = true;
}
}
FastLED.show();
}
/// Note A0 MIDI value
static const byte MIN_PIANO_MIDI_NOTE = 28;
/// Note C8 MIDI value
static const byte MAX_PIANO_MIDI_NOTE = 116;
static void handleNoteOn(byte channel, byte note, byte velocity)
{
if ((note >= MIN_PIANO_MIDI_NOTE) && (note <= MAX_PIANO_MIDI_NOTE)) {
keys[note - MIN_PIANO_MIDI_NOTE] = true;
velocities[note - MIN_PIANO_MIDI_NOTE] = velocity;
}
}
static void handleNoteOff(byte channel, byte note, byte velocity)
{
if ((note >= MIN_PIANO_MIDI_NOTE) && (note <= MAX_PIANO_MIDI_NOTE)) {
keys[note - MIN_PIANO_MIDI_NOTE] = false;
}
}
static void handleControlChange(byte channel, byte number, byte value)
{
if (channel == 64) {
pedal = number;
}
}
Working concurrent fireworks code is below, visualized here
#include <FastLED.h>
const int NUM_LEDS=144;
#define DATA_PIN 5
CRGB leds[NUM_LEDS]; // sets up block of memory
const int NUM_SPARKS = 40; // max number (could be NUM_LEDS / 2);
const int maxPos = (NUM_LEDS-1) * 128 ;
int sparkPos[3][NUM_SPARKS] ;
int sparkVel[3][NUM_SPARKS] ;
int sparkHeat[3][NUM_SPARKS];
int flarePos[3];
bool flareShot[3] = {false,false,false} ;
byte nSparks[3];
CRGBPalette16 gPal1;
CRGBPalette16 gPal2;
CRGBPalette16 gPal3;
DEFINE_GRADIENT_PALETTE( testp ) {
0, 0, 0,0,
32, 8, 0, 0,
64, 16,0, 0,
96, 64,32, 0,
128,128,80, 0,
160, 160,100,0,
192, 192, 192,0,
224, 255, 255, 255,
255, 200, 200, 255};
void setup() {
// Serial.begin(115200);
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
FastLED.setBrightness(255);
gPal1 = testp;
gPal2 = CRGBPalette16( CRGB::Black, CRGB::DarkBlue, CRGB::Aqua, CRGB::White);
gPal3 = CRGBPalette16( CRGB::Black, CRGB::DarkGreen, CRGB::Yellow, CRGB::White);
}
/*
* Main Loop
*/
byte nrv;
void loop() {
EVERY_N_MILLIS(10) {
// random16_add_entropy( random());
if (not flareShot[0]) shoot(0);
if (not flareShot[1]) shoot(1);
if (not flareShot[2]) shoot(2);
}
EVERY_N_MILLIS(10) {
if (flareShot[0]) doSparks(0);
if (flareShot[1]) doSparks(1);
if (flareShot[2]) doSparks(2);
}
EVERY_N_MILLIS(10) {
FastLED.show();
// FastLED.clear();
fadeToBlackBy(leds,NUM_LEDS,80);
}
}
void shoot(byte nr) {
if (random(1000) <10) {
flareShot[nr]=true;
flarePos[nr]=random(40,NUM_LEDS-40);
nSparks[nr]=30;
// initialize sparks
for (int x = 0; x < nSparks[nr]; x++) {
sparkPos[nr][x] = flarePos[nr]<<7;
sparkVel[nr][x] = random16(0, 5120)-2560; // velocitie original -1 o 1 now -255 to + 255
word sph = abs(sparkVel[nr][x])<<2;
if (sph>2550) sph=2550; // set heat before scaling velocity to keep them warm heat is 0-500 but then clamped to 255
sparkHeat[nr][x]=sph ;
}
sparkHeat[nr][0] = 5000; // this will be our known spark
}
}
void doSparks(byte nr){
for (int x = 0; x < nSparks[nr]; x++) {
sparkPos[nr][x] = sparkPos[nr][x] + (sparkVel[nr][x]>>6); // adjust speed of sparks here
sparkPos[nr][x]=constrain(sparkPos[nr][x],0,maxPos);
sparkHeat[nr][x]=scale16(sparkHeat[nr][x],128000); // adjust speed of cooldown here
CRGB color;
if (nr==0) color = ColorFromPalette( gPal1 , scale16(sparkHeat[nr][x],6600));
if (nr==1) color = ColorFromPalette( gPal2 , scale16(sparkHeat[nr][x],6600));
if (nr==2) color = ColorFromPalette( gPal3 , scale16(sparkHeat[nr][x],6600));
leds[sparkPos[nr][x]>>7]+=color;
if (sparkHeat[nr][0] < 1) {
flareShot[nr]=false; // this fireworks is done
}
}
}