Help with concurrency and merging two concepts together

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
        }
    }
  }

This is where you whack a key.

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;
  }
}

Start with just one LED on keywhack. Then associate a pattern with the keythwack.

Where did the first sketch come from? Have you modified it from what you found? Can you read it at least to see where the MIDI key makes a LED illuminate?

The second sketch is already playing very nice as far as concurrent operation goes. It uses the fastLED macro

EVERY_N_MILLIS(10) {  // ... stuff runs 100 frames per second

}

Through the tiny window it looks like that code could manage a large number of key press animations and leave plenty of time leftover for other logic. Here you would replace the random launch with a controlled by MIDI key lunch.

The loop() continues to run free and get the 100 fps animation done.

Are you looking to do the same effect as it does, or just advice about how to launch and manage N animations, launched by MIDI key?

When I am in the lab I look to see if the second sketch can't be made a little simpler, and to see if my guess is correct.

a7

I am not asking for someone to 'do it for me'. I am very much a novice when it comes to Arduino development. My experience is relatively low, but I can usually learn once I understand a simple example. I have already given it a try, and now I'm here.

Something I don't quite grasp currently is how the main loop works in conjunction with the other functions. Like practically, how can it 'listen' for new MIDI inputs, and process those while also still lighting up the LEDs associated with the currently pressed keys.

Thanks - something I'm a little unclear on here is if the pattern should be called in here or up in the main loop.

    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();

This seems to be where it's currently lighting up the LEDs? I'm not quite getting where I need to put code to call an animation instead of turning on a single LED per key

Original code is here GitHub - ddribin/piano-lights-sw: Displays lights (LEDs) over piano keys I modified it to remove some stuff that did not apply to my case and also to use the velocity from the MIDI data to map it to LED brightness. So, I do have a general grasp of the current code.

Where I get lost is understanding how everything happens between the main loop and the sub functions, and what exactly happens within each main loop cycle.

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;
    }
}

My understanding of this, is that it takes the midi data and updates the keys and velocities arrays to store that the key is being pressed, and with what velocity.

Then back in the main loop, the big for loop turns on corresponding LEDs.

This firework effect would be a good start, and I've played with it enough to understand the parameters and how to control the size (which I'd tie into velocity)

I'm just currently unsure of the general steps i'd need to take to combine this with the midi code. Any help/guidance is welcome. Thank you.

OK, the MIDI stuff is almost like magic, as it relies on (or appears to rely on) callback functions.

So your loop() just does what you write there, but has the opportunty and ability to see if, in the background, any new MIDI events are arrived.

They might just be handled entirely in the background by the callback functions, but those functions can set variables or flags that your loop() code can test.

a7

@freakintoddles I looked closer at the second sketch and hacked it to launch effects on the press of a button.

I still don't understand it - now it looks like t There can be only three cases of the animation running at the same time.

The code runs on the same hardware as the wokwi linked, you could copy/paste it into the sketch pane and run it but you'll have to add the ezButton library for it to compile.

That pushbutton will launch an animation, and the rest of the loop() will make launched animations play out on the strip.

shoot() launches the effect, calls to doSparks() are made every 10 milliseconds, 100 frames per second, to make the animation progress.

I've had too much of one thing, or not enough of another to go further. I may be able to convince someone of the importance of doing more now; don't hold your breath.

But do look at the loop() function I leave for now. It just replaces the random chance of launching an effect at a random point on the strip with "effect on demand" mediated by a pushbutton.

# include <FastLED.h>

# include <ezButton.h>

ezButton myButton(A0);

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);

  myButton.setDebounceTime(20);

  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() {  
 
  myButton.loop();



  if (myButton.isPressed()) {

// this could be the key number instead of a random number
    static byte sh;
    shoot(sh, random(10, NUM_LEDS - 10));
    sh++; if (sh >= 3) sh = 0;
  }

// no - we'll launch our own
//  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, int position) {
 
  if (1) {
    flareShot[nr]=true;
    Serial.print(" nr  ");
    Serial.println(nr);

 
  flarePos[nr] = position;
  Serial.print(" flare position  ");
  Serial.println(flarePos[nr]);

  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
        }
    }
  }

HTH and L8R

OK, she says: there can only be three happening at once, but each might have many many LEDs currently involved being tracked and updated and killed finally.

Seems about right.

a7

The combination of "I want to use concurrency" and "I'm a beginner" is explosive. Even very experienced programmers can struggle with concurrency. The best suggestion I can make is to start with an ESP32 or STM32 (like in your example) and use FreeRTOS. You have a higher likelyhood of being successful than if you try to shoehorn concurrency concepts into a base level Arduino.

That's it, I'm out :slight_smile:

Thanks for the assistance, I'll try this out and report back once I get it working. Cheers

I've never heard of this but I'll check it out. I'm not sure explosive is the right term for what I am asking for here, I'm just a part-time hobbyist doing this in my spare time for fun. I only am really looking to learn enough to accomplish a specific goal not to become an expert here.

It might well be said that the combination of "I want to use freeRTOS" and "I'm a beginner" is explosive.

freeRTOS will make sense one day should you actually ever need it only if you coukd write a program like you are after here without using freeRTOS.

Ironic, no?

a7

I've just noticed that your key pressed to LED effect is handled here

and that means you are acting over and over because a key is pressed, not because it got pressed or got released.

So that won't work to launch an animation, it would keep launching.

You will have to (could, I mean) launch directly from handleNoteOn() or set a flag in that function and use the flag as an indication to launch an animation.

I'm in transit so can't see which if not both would work, or if one method would be all that much better than the other.

a7

Thanks - yeah I was wondering about that. In the current midi/led code and how it works, if I press a key(s) down, the corresponding LED(s) stay lit up (visually) until the key is released. But I am realizing it was never entirely clear to me how that was happening.

I see near the beginning of the loop, FastLED.clear(); is called which clears any data and sets everything to black.

Then in the main for() there, it iterates through keys[] to turn on any leds which are set to 'true' (from handleNoteOn).

handleNoteOff sets the related keys[] value(s) back to false once the note is turned off.

So, technically right now while I am holding keys down, the lights are being turned off and back on again each loop - it's just happening so fast that it's not visible and they appear to just be solid on.

No!

You are changing the data in the LED buffer, but clear() doesn't actually turn off any real LEDs.

All changes to the buffer through whatever means do not effect the LEDs and are therefore not visible until… you publish them with

    FastLED.show();

This is essential, as it affords an opportunity to completely calculate and set all the new LEDs with no visual effect. Set, clear, change colours at your leisure. In any order. Never seen.

Then show the results.

Many of my sketches call the show() method for the pixel library in only one place, the loop() function, and only N times per second, the animation frame rate.

a7

OK gotcha - so clear() alone doesn't do anything until show() is called later. Thx for clearing that up.