From 34d15a7f1c5c29fc2f09a8abf6d1e6020833c0dd Mon Sep 17 00:00:00 2001 From: Dejvino Date: Fri, 27 Feb 2026 06:48:19 +0100 Subject: [PATCH] Menu with button support to switch wave tables --- AudioThread.cpp | 28 ++++++++- SharedState.cpp | 28 +++++++-- SharedState.h | 27 ++++++++- UIThread.cpp | 155 +++++++++++++++++++++++++++++++++++++----------- 4 files changed, 196 insertions(+), 42 deletions(-) diff --git a/AudioThread.cpp b/AudioThread.cpp index 71d88bb..65128ed 100644 --- a/AudioThread.cpp +++ b/AudioThread.cpp @@ -45,14 +45,38 @@ void loopAudio() { if (now - lastNoteChangeTime > 500) { lastNoteChangeTime = now; int noteIndex = random(0, SCALES[currentScaleIndex].numNotes); - currentFrequency = SCALES[currentScaleIndex].frequencies[noteIndex]; + + // Calculate frequency based on key, scale, and octave + const float baseFrequency = 261.63f; // C4 + float keyFrequency = baseFrequency * pow(2.0f, currentKeyIndex / 12.0f); + int semitoneOffset = SCALES[currentScaleIndex].semitones[noteIndex]; + currentFrequency = keyFrequency * pow(2.0f, semitoneOffset / 12.0f); + Serial.println("Playing note: " + String(currentFrequency) + " Hz"); } // Generate the sine wave sample + int16_t sample; double phaseIncrement = 2.0 * M_PI * currentFrequency / SAMPLE_RATE; phase = fmod(phase + phaseIncrement, 2.0 * M_PI); - int16_t sample = static_cast(AMPLITUDE * sin(phase)); + + switch (currentWavetableIndex) { + case 0: // Sine + sample = static_cast(AMPLITUDE * sin(phase)); + break; + case 1: // Square + sample = (phase < M_PI) ? AMPLITUDE : -AMPLITUDE; + break; + case 2: // Saw + sample = static_cast(AMPLITUDE * (1.0 - (phase / M_PI))); + break; + case 3: // Triangle + sample = static_cast(AMPLITUDE * (2.0 * fabs(phase / M_PI - 1.0) - 1.0)); + break; + default: + sample = 0; + break; + } // Write the same sample to both left and right channels (mono audio). // This call is blocking and will wait until there is space in the DMA buffer. diff --git a/SharedState.cpp b/SharedState.cpp index c470822..436d41b 100644 --- a/SharedState.cpp +++ b/SharedState.cpp @@ -4,11 +4,29 @@ volatile unsigned long lastLoop0Time = 0; volatile unsigned long lastLoop1Time = 0; volatile bool watchdogActive = false; +UIState currentState = UI_MENU; +volatile int menuSelection = 0; + +const MenuItem MENU_ITEMS[] = { + { "Scale Type", UI_EDIT_SCALE_TYPE }, + { "Scale Key", UI_EDIT_SCALE_KEY }, + { "Wavetable", UI_EDIT_WAVETABLE } +}; +const int NUM_MENU_ITEMS = sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]); + const Scale SCALES[] = { - { "C Major", { 261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25 }, 8 }, - { "C Minor", { 261.63, 293.66, 311.13, 349.23, 392.00, 415.30, 466.16, 523.25 }, 8 }, - { "Pentatonic", { 261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25 }, 8 }, - { "Blues", { 261.63, 311.13, 349.23, 369.99, 392.00, 466.16, 523.25, 587.33 }, 8 } + { "Major", { 0, 2, 4, 5, 7, 9, 11, 12 }, 8 }, + { "Minor", { 0, 2, 3, 5, 7, 8, 10, 12 }, 8 }, + { "Pentatonic", { 0, 2, 4, 7, 9, 12, 14, 16 }, 8 }, + { "Blues", { 0, 3, 5, 6, 7, 10, 12, 14 }, 8 } }; const int NUM_SCALES = sizeof(SCALES) / sizeof(SCALES[0]); -volatile int currentScaleIndex = 0; \ No newline at end of file +volatile int currentScaleIndex = 0; + +const char* KEY_NAMES[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; +const int NUM_KEYS = sizeof(KEY_NAMES) / sizeof(KEY_NAMES[0]); +volatile int currentKeyIndex = 0; // C + +const char* WAVETABLE_NAMES[] = {"Sine", "Square", "Saw", "Triangle"}; +const int NUM_WAVETABLES = sizeof(WAVETABLE_NAMES) / sizeof(WAVETABLE_NAMES[0]); +volatile int currentWavetableIndex = 0; // Sine \ No newline at end of file diff --git a/SharedState.h b/SharedState.h index 833e365..d60ad1e 100644 --- a/SharedState.h +++ b/SharedState.h @@ -7,14 +7,39 @@ extern volatile unsigned long lastLoop0Time; extern volatile unsigned long lastLoop1Time; extern volatile bool watchdogActive; +enum UIState { + UI_MENU, + UI_EDIT_SCALE_TYPE, + UI_EDIT_SCALE_KEY, + UI_EDIT_WAVETABLE +}; + struct Scale { const char* name; - const float frequencies[8]; + const int semitones[8]; int numNotes; }; +struct MenuItem { + const char* label; + UIState editState; +}; + +extern UIState currentState; +extern const MenuItem MENU_ITEMS[]; +extern const int NUM_MENU_ITEMS; +extern volatile int menuSelection; + extern const Scale SCALES[]; extern const int NUM_SCALES; extern volatile int currentScaleIndex; +extern const char* KEY_NAMES[]; +extern const int NUM_KEYS; +extern volatile int currentKeyIndex; + +extern const char* WAVETABLE_NAMES[]; +extern const int NUM_WAVETABLES; +extern volatile int currentWavetableIndex; + #endif // SHAREDSTATE_H \ No newline at end of file diff --git a/UIThread.cpp b/UIThread.cpp index c96eee4..17de1c2 100644 --- a/UIThread.cpp +++ b/UIThread.cpp @@ -17,6 +17,7 @@ // Encoder Pins #define PIN_ENC_CLK 12 #define PIN_ENC_DT 13 +#define PIN_ENC_SW 14 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); @@ -24,6 +25,15 @@ volatile int8_t encoderDelta = 0; static uint8_t prevNextCode = 0; static uint16_t store = 0; +// Button state +static int lastButtonReading = HIGH; +static int currentButtonState = HIGH; +static unsigned long lastDebounceTime = 0; +static bool buttonClick = false; + +void handleInput(); +void drawUI(); + // --- ENCODER INTERRUPT --- // Robust Rotary Encoder reading void readEncoder() { @@ -48,10 +58,11 @@ void setupUI() { Wire.setSCL(PIN_SCL); Wire.begin(); - pinMode(PIN_ENC_CLK, INPUT_PULLUP); - pinMode(PIN_ENC_DT, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(PIN_ENC_CLK), readEncoder, CHANGE); - attachInterrupt(digitalPinToInterrupt(PIN_ENC_DT), readEncoder, CHANGE); + pinMode(PIN_ENC_CLK, INPUT_PULLUP); + pinMode(PIN_ENC_DT, INPUT_PULLUP); + pinMode(PIN_ENC_SW, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(PIN_ENC_CLK), readEncoder, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC_DT), readEncoder, CHANGE); if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 allocation failed")); @@ -59,43 +70,119 @@ void setupUI() { } display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println(F("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")); display.display(); } void handleInput() { - // Handle Encoder Rotation - int rotation = 0; - noInterrupts(); - rotation = encoderDelta; - encoderDelta = 0; - interrupts(); + // Handle Encoder Rotation + int rotation = 0; + noInterrupts(); + rotation = encoderDelta; + encoderDelta = 0; + interrupts(); - if (rotation != 0) { - currentScaleIndex += rotation; - while (currentScaleIndex < 0) currentScaleIndex += NUM_SCALES; - while (currentScaleIndex >= NUM_SCALES) currentScaleIndex -= NUM_SCALES; - } + if (rotation != 0) { + switch (currentState) { + case UI_MENU: + menuSelection += rotation; + while (menuSelection < 0) menuSelection += NUM_MENU_ITEMS; + menuSelection %= NUM_MENU_ITEMS; + break; + case UI_EDIT_SCALE_TYPE: + currentScaleIndex += rotation; + while (currentScaleIndex < 0) currentScaleIndex += NUM_SCALES; + currentScaleIndex %= NUM_SCALES; + break; + case UI_EDIT_SCALE_KEY: + currentKeyIndex += rotation; + while (currentKeyIndex < 0) currentKeyIndex += NUM_KEYS; + currentKeyIndex %= NUM_KEYS; + break; + case UI_EDIT_WAVETABLE: + currentWavetableIndex += rotation; + while (currentWavetableIndex < 0) currentWavetableIndex += NUM_WAVETABLES; + currentWavetableIndex %= NUM_WAVETABLES; + break; + } + } + + // Handle Button Click + int reading = digitalRead(PIN_ENC_SW); + if (reading != lastButtonReading) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > 50) { + if (reading != currentButtonState) { + currentButtonState = reading; + if (currentButtonState == LOW) { + buttonClick = true; + } + } + } + lastButtonReading = reading; + + if (buttonClick) { + buttonClick = false; + if (currentState == UI_MENU) { + currentState = MENU_ITEMS[menuSelection].editState; + } else { + currentState = UI_MENU; + } + } +} + +void drawUI() { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + + if (currentState == UI_MENU) { + for (int i = 0; i < NUM_MENU_ITEMS; i++) { + if (i == menuSelection) { + display.fillRect(0, i * 10, SCREEN_WIDTH, 10, SSD1306_WHITE); + display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); + } else { + display.setTextColor(SSD1306_WHITE); + } + display.setCursor(2, i * 10 + 1); + display.print(MENU_ITEMS[i].label); + display.print(": "); + + // Display current value + switch (MENU_ITEMS[i].editState) { + case UI_EDIT_SCALE_TYPE: display.print(SCALES[currentScaleIndex].name); break; + case UI_EDIT_SCALE_KEY: display.print(KEY_NAMES[currentKeyIndex]); break; + case UI_EDIT_WAVETABLE: display.print(WAVETABLE_NAMES[currentWavetableIndex]); break; + default: break; + } + } + } else { + // In an edit screen + const char* title = MENU_ITEMS[menuSelection].label; + const char* value = ""; + switch (currentState) { + case UI_EDIT_SCALE_TYPE: value = SCALES[currentScaleIndex].name; break; + case UI_EDIT_SCALE_KEY: value = KEY_NAMES[currentKeyIndex]; break; + case UI_EDIT_WAVETABLE: value = WAVETABLE_NAMES[currentWavetableIndex]; break; + default: break; + } + display.println(title); + display.drawLine(0, 10, SCREEN_WIDTH, 10, SSD1306_WHITE); + display.setCursor(10, 25); + display.setTextSize(2); + display.print(value); + display.setTextSize(1); + display.setCursor(0, 50); + display.println(F("(Press to confirm)")); + } + + display.display(); } void loopUI() { - // sleep to avoid thread starvation - delay(10); - handleInput(); - // The loop on core 0 is responsible for updating the UI. - static unsigned long lastUpdate = 0; - if (millis() - lastUpdate < 100) return; - lastUpdate = millis(); - - display.clearDisplay(); - display.setCursor(0, 0); - display.setTextSize(1); - display.println(F("Current Scale:")); - display.setTextSize(2); - display.println(SCALES[currentScaleIndex].name); - display.display(); + drawUI(); + delay(20); // Prevent excessive screen refresh } \ No newline at end of file