Piezo MIDI Controller With Scale Selection

Hi, I'm fairly new to coding and I've had some help by AI to put this together, but I'm building a velocity sensitive piezo MIDI controller that I would much appreciate some help with, if possible. Basically, I am using an OLED display, an encoder and LED strip to enable me to cycle through a variety of scales/modes. All is good when changing the scale, all notes in the scale light green while all others are red, but when I change the root note, the LED's do not update. Here's the full code:

#include <Adafruit_GFX.h>
#include <gfxfont.h>
#include <MIDIUSB.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Encoder.h>
#include <Adafruit_NeoPixel.h>

#define SCREEN_WIDTH 128    // OLED display width, in pixels
#define SCREEN_HEIGHT 64    // OLED display height, in pixels
#define OLED_RESET -1       // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define MUX_S0 16
#define MUX_S1 14
#define MUX_S2 15
#define MUX_S3 10

#define BUTTON_UP 4
#define BUTTON_DOWN 5
#define CHANNEL_UP 7
#define CHANNEL_DOWN 8

#define ENCODER_PIN_A A1
#define ENCODER_PIN_B A2
#define ENCODER_BUTTON_PIN A3

#define LED_PIN 9
#define NUM_LEDS 12
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

int octave = 0;
int baseNote = 48;
int maxOctave = 2;

int midiChannel = 1;
const int maxChannel = 16;

const int threshold = 200;
const int maxVelocity = 127;

Encoder enc(ENCODER_PIN_A, ENCODER_PIN_B);
int encoderPos = 0;

enum Scale { 
  MAJOR, MINOR, MINOR_PENTATONIC, MINOR_HARMONIC, WHOLE_TONE, 
  IONIAN, PHRYGIAN, DORIAN, LYDIAN, MIXOLYDIAN, 
  AEOLIAN, LOCRIAN, BLUES, WHOLE_HALF_DIMINISHED, 
  LYDIAN_DOMINANT, NEAPOLITAN_MINOR
};
Scale currentScale = MAJOR;
int rootNote = 48;

bool displayRootNote = false;

void updateDisplay();
void handlePiezo(int channel, int midiNote);
void handleEncoder();
int getNoteForScale(int rootNote, int scale, int noteOffset);
bool isNoteInScale(int rootNote, int scale, int note);
void updateLEDsForScale();
String getNoteName(int midiNote); // New function to get note name

int numPiezos = 1;

void setup() {
  pinMode(BUTTON_UP, INPUT_PULLUP);
  pinMode(BUTTON_DOWN, INPUT_PULLUP);
  pinMode(CHANNEL_UP, INPUT_PULLUP);
  pinMode(CHANNEL_DOWN, INPUT_PULLUP);

  pinMode(MUX_S0, OUTPUT);
  pinMode(MUX_S1, OUTPUT);
  pinMode(MUX_S2, OUTPUT);
  pinMode(MUX_S3, OUTPUT);

  pinMode(ENCODER_BUTTON_PIN, INPUT_PULLUP);

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);
  }
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  updateDisplay();
  strip.begin();
  strip.show();
}

void loop() {
  handleEncoder();

  if (digitalRead(BUTTON_UP) == LOW && octave < maxOctave) {
    octave++;
    delay(200);
    updateDisplay();
  }
  if (digitalRead(BUTTON_DOWN) == LOW && octave > -2) {
    octave--;
    delay(200);
    updateDisplay();
  }

  if (digitalRead(CHANNEL_UP) == LOW && midiChannel < maxChannel) {
    midiChannel++;
    delay(200);
    updateDisplay();
  }
  if (digitalRead(CHANNEL_DOWN) == LOW && midiChannel > 1) {
    midiChannel--;
    delay(200);
    updateDisplay();
  }

  for (int i = 0; i < numPiezos; i++) {
    handlePiezo(i, getNoteForScale(rootNote, currentScale, i) + (octave * 12));
  }

  updateLEDsForScale();
}

void handlePiezo(int channel, int midiNote) {
  digitalWrite(MUX_S0, (channel & 0x01));
  digitalWrite(MUX_S1, (channel & 0x02) >> 1);
  digitalWrite(MUX_S2, (channel & 0x04) >> 2);
  digitalWrite(MUX_S3, (channel & 0x08) >> 3);

  int piezoValue = analogRead(A0);

  if (piezoValue > threshold) {
    int velocity = map(piezoValue, threshold, 1023, 1, maxVelocity);
    noteOn(midiNote, velocity);
    delay(50);
    noteOff(midiNote);
  }
}

int getNoteForScale(int rootNote, int scale, int noteOffset) {
  // Scale intervals are constant and based on the root note
  static const int major[] = {0, 2, 4, 5, 7, 9, 11};
  static const int minor[] = {0, 2, 3, 5, 7, 8, 10};
  static const int minorPentatonic[] = {0, 2, 3, 5, 7};
  static const int minorHarmonic[] = {0, 2, 3, 5, 7, 8, 11};
  static const int wholeTone[] = {0, 2, 4, 6, 8, 10};
  static const int blues[] = {0, 3, 5, 6, 7, 10};
  static const int wholeHalfDiminished[] = {0, 2, 3, 5, 6, 8, 9, 11};
  static const int lydianDominant[] = {0, 2, 4, 6, 7, 9, 10};
  static const int neapolitanMinor[] = {0, 1, 3, 5, 6, 8, 11};

  static const int modes[7][7] = {
    {0, 2, 4, 5, 7, 9, 11}, // Ionian
    {0, 2, 3, 5, 7, 8, 10}, // Dorian
    {0, 1, 3, 5, 7, 8, 10}, // Phrygian
    {0, 2, 4, 6, 7, 9, 11}, // Lydian
    {0, 2, 4, 5, 7, 9, 10}, // Mixolydian
    {0, 2, 3, 5, 7, 8, 10}, // Aeolian
    {0, 1, 3, 5, 6, 8, 10}  // Locrian
  };

  switch (scale) {
    case MAJOR: return rootNote + major[noteOffset % 7];
    case MINOR: return rootNote + minor[noteOffset % 7];
    case MINOR_PENTATONIC: return rootNote + minorPentatonic[noteOffset % 5];
    case MINOR_HARMONIC: return rootNote + minorHarmonic[noteOffset % 7];
    case WHOLE_TONE: return rootNote + wholeTone[noteOffset % 6];
    case IONIAN: return rootNote + modes[0][noteOffset % 7];
    case DORIAN: return rootNote + modes[1][noteOffset % 7];
    case PHRYGIAN: return rootNote + modes[2][noteOffset % 7];
    case LYDIAN: return rootNote + modes[3][noteOffset % 7];
    case MIXOLYDIAN: return rootNote + modes[4][noteOffset % 7];
    case AEOLIAN: return rootNote + modes[5][noteOffset % 7];
    case LOCRIAN: return rootNote + modes[6][noteOffset % 7];
    case BLUES: return rootNote + blues[noteOffset % 6];
    case WHOLE_HALF_DIMINISHED: return rootNote + wholeHalfDiminished[noteOffset % 8];
    case LYDIAN_DOMINANT: return rootNote + lydianDominant[noteOffset % 7];
    case NEAPOLITAN_MINOR: return rootNote + neapolitanMinor[noteOffset % 7];
    default: return rootNote; // Default to rootNote if scale not recognized
  }
}

bool isNoteInScale(int rootNote, int scale, int note) {
  for (int i = 0; i < 12; i++) {
    if (getNoteForScale(rootNote, scale, i) % 12 == note % 12) {
      return true;
    }
  }
  return false;
}

void noteOn(byte pitch, byte velocity) {
  byte status = 0x90 | ((midiChannel - 1) & 0x0F);
  midiEventPacket_t noteOn = {0x09, status, pitch, velocity};
  MidiUSB.sendMIDI(noteOn);
  MidiUSB.flush();
}

void noteOff(byte pitch) {
  byte status = 0x80 | ((midiChannel - 1) & 0x0F);
  midiEventPacket_t noteOff = {0x08, status, pitch, 0};
  MidiUSB.sendMIDI(noteOff);
  MidiUSB.flush();
}

void updateDisplay() {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print(F("Oct: "));
  display.print(octave + 0);

  if (displayRootNote) {
    display.print(F(" Root Note: "));
    display.print(getNoteName(rootNote));  // Show root note as a letter
  } else {
    display.setCursor(0, 16); // Move to next line
    display.print(F("Scl: "));
    switch (currentScale) {
      case MAJOR: display.print(F("Major")); break;
      case MINOR: display.print(F("Minor")); break;
      case MINOR_PENTATONIC: display.print(F("Minor Pent")); break;
      case MINOR_HARMONIC: display.print(F("Harmonic m")); break;
      case WHOLE_TONE: display.print(F("Whole Tone")); break;
      case IONIAN: display.print(F("Ionian")); break;
      case DORIAN: display.print(F("Dorian")); break;
      case PHRYGIAN: display.print(F("Phrygian")); break;
      case LYDIAN: display.print(F("Lydian")); break;
      case MIXOLYDIAN: display.print(F("Mixolydian")); break;
      case AEOLIAN: display.print(F("Aeolian")); break;
      case LOCRIAN: display.print(F("Locrian")); break;
      case BLUES: display.print(F("Blues")); break;
      case WHOLE_HALF_DIMINISHED: display.print(F("Whole Half Dim")); break;
      case LYDIAN_DOMINANT: display.print(F("Lydian Dom")); break;
      case NEAPOLITAN_MINOR: display.print(F("Neapol Min")); break;
    }
  }
 
  display.setCursor(0, 48); // Move to next line
  display.print(F("Ch: "));
  display.print(midiChannel);
  display.display();
}
 
enum EncoderMode {
  CHANGE_ROOT_NOTE,
  CHANGE_SCALE
};
EncoderMode encoderMode = CHANGE_SCALE; 
 
void handleEncoder() {
  int newEncoderPos = enc.read() / 4; 
 
  if (digitalRead(ENCODER_BUTTON_PIN) == LOW) {
    encoderMode = (encoderMode == CHANGE_SCALE) ? CHANGE_ROOT_NOTE : CHANGE_SCALE;
    displayRootNote = (encoderMode == CHANGE_ROOT_NOTE); 
    delay(300); 
    updateDisplay();
  }
 
  if (newEncoderPos != encoderPos) {
    if (encoderMode == CHANGE_ROOT_NOTE) {
      rootNote = (rootNote + (newEncoderPos > encoderPos ? 1 : -1)) % 128;
      if (rootNote < 0) rootNote += 128;
      
      updateLEDsForScale();
 
    } else if (encoderMode == CHANGE_SCALE) {
      currentScale = static_cast<Scale>((currentScale + (newEncoderPos > encoderPos ? 1 : -1) + 16) % 16);

      updateLEDsForScale();
    }
 
    encoderPos = newEncoderPos; 
    updateDisplay();           
  }
}
 
void updateLEDsForScale() {
  for (int i = 0; i < NUM_LEDS; i++) {
    int note = rootNote + i;
    if (isNoteInScale(rootNote, currentScale, note)) {
      strip.setPixelColor(i, strip.Color(0, 255, 0)); // Green for notes in scale
    } else {
      strip.setPixelColor(i, strip.Color(255, 0, 0)); // Red for notes outside scale
    }
  }
  strip.show();
}

String getNoteName(int midiNote) {
  const char* notes[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
  return String(notes[midiNote % 12]);
}
 

Well how about you change the format of the code to a more readable version, without line nrs, no extra whileline in between the lines and properly indented. It is probably something fairly obvious, but i can't read it like this (and i don't really want to)

chatGPT is good at being inaccurate.

By line 7 it shows... and you want humans to fix a machine's 200 more lines of mess?

Hey, thanks for your response. Sorry about that! I see what you mean and have cleaned it up. Must have copied it from 'Pastebin', hence the numbers. Yea, I think is an easy fix... Probably just one of these two sections need tweaking, 'updateLEDsForScale' or the 'handleEncoder' logic.

Firstly, I was the one who included the library's, so that was a human error. Secondly, I have not employed chatGPT to do all of this for me and I do not expect humans to clear up all the mess. I am merely asking for some pointers (just one human will do) on what might be the issue. It's likely to be between the brackets of just a few lines of code. Don't worry, I'm aware people get a bit aggy on here with newbies making novice mistakes, so all good. But do think it over before you lay into someone next time. Thanks

Yeah OK thanks for that. So i didn't find anything obvious. As far as i can tell if the scale is updated, so should the rootnote be.

It is a little confusing that you use the same variable name for the 'global' rootNote variable as for the local ones, but still

  if (newEncoderPos != encoderPos) {
    if (encoderMode == CHANGE_ROOT_NOTE) {
      rootNote = (rootNote + (newEncoderPos > encoderPos ? 1 : -1)) % 128;
      if (rootNote < 0) rootNote += 128;
      
      updateLEDsForScale();
 
    } else if (encoderMode == CHANGE_SCALE) {
      currentScale = static_cast<Scale>((currentScale + (newEncoderPos > encoderPos ? 1 : -1) + 16) % 16);

      updateLEDsForScale();
    }
 
    encoderPos = newEncoderPos; 
    updateDisplay();           
  }

in both cases the LEDS should be updated from here. The display is updated correctly ?
Oh and what board are you using ?

Oh yea, I see what you mean about the same variables. The display updates as it should and when debugging, the adjusted root notes list correctly in the serial monitor. I am using a Pro Micro. I think the board should be able to handle it: "Sketch uses 22220 bytes (77%) of program storage space. Maximum is 28672 bytes.
Global variables use 853 bytes (33%) of dynamic memory, leaving 1707 bytes for local variables."

Yeah that should be fine, the 12 notes will declare another 36 bytes on the heap but that should not be an issue.

I have renamed the global variable for clarity

Ah now i see.

void updateLEDsForScale() {
  for (int i = 0; i < NUM_LEDS; i++) {
    int note = rootNoteGlobal + i;  // here you adjust the note using the new rote note value, but that means that
           // regardless of the rootnote, the pattern is only defined by the scale as a consequence
    if (isNoteInScale(rootNoteGlobal, currentScale, note)) {
      strip.setPixelColor(i, strip.Color(0, 255, 0)); // Green for notes in scale
    } else {
      strip.setPixelColor(i, strip.Color(255, 0, 0)); // Red for notes outside scale
    }
  }
  strip.show();
}

You see what i mean ?

I think you need to do something like

void updateLEDsForScale() {
  for (int i = 0; i < NUM_LEDS; i++) {
    int note = rootNoteGlobal + i;
    int led = note % NUM_LEDS;
    if (isNoteInScale(rootNoteGlobal, currentScale, note)) {
      strip.setPixelColor(led, strip.Color(0, 255, 0)); // Green for notes in scale
    } else {
      strip.setPixelColor(led, strip.Color(255, 0, 0)); // Red for notes outside scale
    }
  }
  strip.show();
}

and it should shift the root-note along the ledstrip.

Yes! thats it! Awesome, thank you so much:) Really, I can't thank you enough – thats made my day/week... Its very unlikely I would have figured that out on my own, or with chatGPT.

Great. Was thinking you could probably light up the root-note with a slightly different color as well, but i am sure you'll figure that one out.

Yea, I thought bout that... I'll probably change it to yellow. Can finally move onto the build! Thanks for your help.