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