Please help check my code for audio filter with ILI9341 and encoder

Hi all, Im brand new to coding and have been working on this sketch for a Teensy 4.1, a ILI9341 TFT screen, and rotary encoder. It seems to work ok, but I would love someone (who knows what they're doing) to look at it before I add more functions. The audio filtering seems to operate as expected. The 7 variables are saved to eeprom and are recalled when the Teensy is rebooted. The screen turns off after a short delay to reduce burn-in and is turned back on when the encoder is turned. Any suggestions would be greatly appreciated!

#include "OpenAudio_ArduinoLibrary.h"
#include "AudioStream_F32.h"
#include "TD_filterIIR.h"
#include <Audio.h>
#include <ILI9341_t3.h>
#include <Encoder.h>
#include <Bounce.h>
#include <EEPROM.h>
#include "font_Arial.h"

AudioInputI2S_F32 input;
filterIIR hp1;
filterIIR eq1;
AudioOutputI2S_F32  output;

AudioConnection_F32 patchCord1(input, 0, hp1, 0);
AudioConnection_F32 patchCord2(hp1, 0, eq1, 0);
AudioConnection_F32 patchCord3(eq1, 0, output, 0);
AudioConnection_F32 patchCord4(eq1, 0, output, 1);
AudioControlSGTL5000 codec;

#define TFT_DC 9
#define TFT_CS 10
ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC);

#define ENCODER_A_PIN   5
#define ENCODER_B_PIN   6
#define ENCODER_BTN_PIN A0

Encoder encoder(ENCODER_A_PIN, ENCODER_B_PIN);
Bounce encoderButton = Bounce(ENCODER_BTN_PIN, 10);
uint32_t lastEncoderReadTime = 0;
elapsedMillis timeSinceLastPress;
int32_t oldEncoderPosition = -999;

unsigned long lastInteractionTime = 0;
const unsigned long screenTimeout = 30000; // 30 seconds
const int backlightPin = A1; // Pin 7 casued screen to flicker

void turnScreenOn() {
  digitalWrite(backlightPin, HIGH); // Turns the backlight on
  tft.setRotation(3); // Ensure the orientation is set correctly after turning on
}


void turnScreenOff() {
  digitalWrite(backlightPin, LOW); // Turns the backlight off
}


// EEPROM addresses for each setting
const int EEPROM_ADDR_HP1 = 0;
const int EEPROM_ADDR_EQ1 = EEPROM_ADDR_HP1 + sizeof(float); // Start after HP1
const int EEPROM_ADDR_EQ1_Q = EEPROM_ADDR_EQ1 + sizeof(float);
const int EEPROM_ADDR_EQ1_GAIN = EEPROM_ADDR_EQ1_Q + sizeof(float);
const int EEPROM_ADDR_EQ2 = EEPROM_ADDR_EQ1_GAIN + sizeof(float);
const int EEPROM_ADDR_EQ2_Q = EEPROM_ADDR_EQ2 + sizeof(float);
const int EEPROM_ADDR_EQ2_GAIN = EEPROM_ADDR_EQ2_Q + sizeof(float);





float hp1_lowcut = 120;

float eq1_lowf = 277;
float eq1_lowq = 2.0;
float eq1_lowgain = -8.0;

float eq2_lowf = 2750;
float eq2_lowq = 5.5;
float eq2_lowgain = -4.0;


void saveSettingsToEEPROM() {
  EEPROM.put(EEPROM_ADDR_HP1, hp1_lowcut);
  EEPROM.put(EEPROM_ADDR_EQ1, eq1_lowf);
  EEPROM.put(EEPROM_ADDR_EQ1_Q, eq1_lowq);
  EEPROM.put(EEPROM_ADDR_EQ1_GAIN, eq1_lowgain);
  EEPROM.put(EEPROM_ADDR_EQ2, eq2_lowf);
  EEPROM.put(EEPROM_ADDR_EQ2_Q, eq2_lowq);
  EEPROM.put(EEPROM_ADDR_EQ2_GAIN, eq2_lowgain);
}

int menuSelected = 0;
int oldMenuSelected = -1;
bool isValueSelected = false;
String menuItems[] = {"Low cut", "EQ1 Freq", "EQ1 Q", "EQ1 Gain", "EQ2 Freq", "EQ2 Q", "EQ2 Gain"};

void loadSettingsFromEEPROM() {
  EEPROM.get(EEPROM_ADDR_HP1, hp1_lowcut);
  EEPROM.get(EEPROM_ADDR_EQ1, eq1_lowf);
  EEPROM.get(EEPROM_ADDR_EQ1_Q, eq1_lowq);
  EEPROM.get(EEPROM_ADDR_EQ1_GAIN, eq1_lowgain);
  EEPROM.get(EEPROM_ADDR_EQ2, eq2_lowf);
  EEPROM.get(EEPROM_ADDR_EQ2_Q, eq2_lowq);
  EEPROM.get(EEPROM_ADDR_EQ2_GAIN, eq2_lowgain);
  
  // Apply the loaded settings to the filters
  hp1.setBiQuadEq(0,'H',hp1_lowcut,1,0);
  eq1.setBiQuadEq(0,'P',eq1_lowf,eq1_lowq,eq1_lowgain);
  eq1.setBiQuadEq(1,'P',eq2_lowf,eq2_lowq,eq2_lowgain);
}



void setup() {
  AudioMemory(5); 
  AudioMemory_F32(12);

  codec.enable();
  codec.inputSelect(AUDIO_INPUT_LINEIN); 
  codec.adcHighPassFilterDisable();
  codec.lineInLevel(2);
  codec.volume(0.7);


  tft.begin();
  tft.setRotation(3); // Set the rotation before turning the screen on
  tft.fillScreen(ILI9341_BLACK);
  tft.setFont(Arial_14);
  
  pinMode(backlightPin, OUTPUT);
  turnScreenOff(); // Start with the screen off
  delay(100);
  tft.begin();
  turnScreenOn(); // Turn the screen on after initialization
  lastInteractionTime = millis(); // Reset the interaction timer


loadSettingsFromEEPROM();




}

void loop() {
  handleEncoder();
  handleEncoderButton();
  checkScreenTimeout();
}

void handleEncoder() {
  int32_t newPos = encoder.read() / 4;  // Dividing by the encoder resolution
  if (newPos != oldEncoderPosition) {
    lastInteractionTime = millis();  // Update last interaction time on any encoder move
    turnScreenOn();  // Ensure the screen is on when the encoder is moved

    int acceleration = 1;

    // Determine the acceleration based on the difference
    int diff = abs(newPos - oldEncoderPosition);
    if (diff > 2) acceleration = 3; // Adjust this as per your needs

    if (isValueSelected) {
      adjustValue((newPos > oldEncoderPosition ? 1 : -1) * acceleration);
    } else {
      if (newPos > oldEncoderPosition) {
        menuSelected = (menuSelected + 1) % 7;
      } else {
        menuSelected = (menuSelected - 1 + 7) % 7;
      }
      drawMenu();
    }
    oldEncoderPosition = newPos;
  }
}



void checkScreenTimeout() {
  if (millis() - lastInteractionTime > screenTimeout) {
    turnScreenOff();
  }
}



void handleEncoderButton() {
  encoderButton.update();
  if (encoderButton.fallingEdge() && timeSinceLastPress > 200) {
    if (isValueSelected) {
      // Save settings to EEPROM only when transitioning from selected value to menu navigation
      saveSettingsToEEPROM();
    }
    isValueSelected = !isValueSelected;
    drawMenu();
    timeSinceLastPress = 0; // reset the timer
  }
}


void adjustValue(int dir) {
  switch(menuSelected) {
    case 0:
      hp1_lowcut += 10 * dir;
      hp1_lowcut = constrain(hp1_lowcut, 20.0f, 210.0f);
      hp1.setBiQuadEq(0,'H',hp1_lowcut,1,0);  // No gain for high-pass filter
      break;
    case 1:
      eq1_lowf += 10 * dir;
      eq1_lowf = constrain(eq1_lowf, 20.0f, 20000.0f);
      eq1.setBiQuadEq(0,'P',eq1_lowf,eq1_lowq,eq1_lowgain);
      break;
    case 2:
      eq1_lowq += 0.1 * dir;
      eq1_lowq = constrain(eq1_lowq, 0.1f, 10.0f);
      eq1.setBiQuadEq(0,'P',eq1_lowf,eq1_lowq,eq1_lowgain);
      break;
    case 3:
      eq1_lowgain += 1 * dir;
      eq1_lowgain = constrain(eq1_lowgain, -24.0f, 24.0f);
      eq1.setBiQuadEq(0,'P',eq1_lowf,eq1_lowq,eq1_lowgain);
      break;
    case 4:
      eq2_lowf += 10 * dir;
      eq2_lowf = constrain(eq2_lowf, 20.0f, 20000.0f);
      eq1.setBiQuadEq(1,'P',eq2_lowf,eq2_lowq,eq2_lowgain);
      break;
    case 5:
      eq2_lowq += 0.1 * dir;
      eq2_lowq = constrain(eq2_lowq, 0.1f, 10.0f);
      eq1.setBiQuadEq(1,'P',eq2_lowf,eq2_lowq,eq2_lowgain);
      break;
    case 6:
      eq2_lowgain += 1 * dir;
      eq2_lowgain = constrain(eq2_lowgain, -24.0f, 24.0f);
      eq1.setBiQuadEq(1,'P',eq2_lowf,eq2_lowq,eq2_lowgain);
      break;
  }
  drawMenu();
}

void drawMenu() {
  // If no previous menu was selected, draw all menu items
  if (oldMenuSelected == -1) {
    for (int i = 0; i < 7; i++) {
      tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
      tft.setCursor(10, i * 30 + 10);
      tft.println(menuItems[i]);
      printMenuItemValue(i);
    }
  } else {
    // Redraw the old menu item in regular style
    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
    tft.setCursor(10, oldMenuSelected * 30 + 10);
    tft.println(menuItems[oldMenuSelected]);
    printMenuItemValue(oldMenuSelected);
  }

  // Draw the current menu item in selected style
  if (isValueSelected) {
    tft.setTextColor(ILI9341_RED, ILI9341_RED);
  } else {
    tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
  }
  tft.setCursor(10, menuSelected * 30 + 10);
  tft.println(menuItems[menuSelected]);
  printMenuItemValue(menuSelected);

  oldMenuSelected = menuSelected;
}

void printMenuItemValue(int menuItem) {
  float value = 0;
  switch(menuItem) {
    case 0: value = hp1_lowcut; break;
    case 1: value = eq1_lowf; break;
    case 2: value = eq1_lowq; break;
    case 3: value = eq1_lowgain; break;
    case 4: value = eq2_lowf; break;
    case 5: value = eq2_lowq; break;
    case 6: value = eq2_lowgain; break;
  }

  int x = 200; 
  int y = menuItem * 30 + 10;
  int width = 50;
  int height = 28;

  // Clear the area
  tft.fillRect(x, y, width, height, ILI9341_BLACK);

  if (menuItem == menuSelected && isValueSelected) {
    tft.setTextColor(ILI9341_RED, ILI9341_BLACK);  // Change text color for highlighted value
  } else {
    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  }

  tft.setCursor(x, y);

  switch (menuItem) {
    case 0:
    case 1:
    case 3:
      tft.print(value, 0);
      break;
    case 2:
      tft.print(value, 1);
      break;
    case 4:
    case 6:
       tft.print(value, 0);
      break;
    case 5:
    tft.print(value, 1);
      break;
  }

  // Reset the text color for other uses
  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
}

Something odd that is happening is that when the Teensy is powered on the highlighted menu item is "EQ1 Freq" instead of "Low Cut", and I cannot figure out why!

My guess is that the statement following the print I added (commented) is executing upon startup, changing the default option to 1.

I can see you are not using Serial.print. You can verify my hypothesis using the onboard LED or via the LCD display…

Thank you for the reply! That section of code is in a pervious sketch that didn't have the eeprom and display time out and I didn't have that issue then. I used serial print to show the menu selected in that section. (Im not sure if Im doing this correctly).

loadSettingsFromEEPROM();

drawMenu();
Serial.print("before here");
Serial.println(menuSelected);

}

void loop() {

  handleEncoder();

  handleEncoderButton();
  
  checkScreenTimeout();


}
    
void handleEncoder() {
  int32_t newPos = encoder.read() / 4;  // Dividing by the encoder resolution
  if (newPos != oldEncoderPosition) {
    lastInteractionTime = millis();  // Update last interaction time on any encoder move
    turnScreenOn();  // Ensure the screen is on when the encoder is moved

    int acceleration = 1;

    // Determine the acceleration based on the difference
    int diff = abs(newPos - oldEncoderPosition);
    if (diff > 2) acceleration = 4; // Adjust this as per your needs

    if (isValueSelected) {
      adjustValue((newPos > oldEncoderPosition ? -1 : 1) * acceleration);
    } else {
      if (newPos > oldEncoderPosition) {
        menuSelected = (menuSelected + 1) % 7;
        Serial.print("here");
        Serial.println(menuSelected);
      } else {
        menuSelected = (menuSelected - 1 + 7) % 7;
      }
      drawMenu();
    }
    oldEncoderPosition = newPos;
  }

  Serial.print("after here");
        Serial.println(menuSelected);
}

And the serial monitor prints this about a million times a second:

I added a delay in the loop and when I first power on the Teensy the first menu item (low cut) is highlighted, and then after about a second it scrolls down to the second menu item! So i assume something is going on in the handle encoder section...

void loop() {

   static bool firstLoop = true;
  if (firstLoop) {
    delay(1000);  // Delay for 100 milliseconds (adjust value as needed)
    firstLoop = false;
  }

  handleEncoder();

  handleEncoderButton();
  

  checkScreenTimeout();

}

The Encoder constructor initializes the position as 0, which is the value returned by read() if no activity is detected by the encoder: https://github.com/PaulStoffregen/Encoder/blob/master/Encoder.h

Your sketch initialized oldEncoderPosition as -999. In the first close encounter, newPos will be different and the program interprets that the encoder shaft moved.

To prevent this, change this

to
int32_t oldEncoderPosition = 0;

That should fix the issue.

I know the old position is initialized as -999 in the examples. Why? Beats me.
(I’m working off my cellphone, cannot test)

1 Like

Thank you! That solved the issue. I really appreciate the help.
Does everything else look ok? Especially the handling of the eeprom?

Thanks again

EEPEROM handling looks good

Two notes:

  1. You are setting default values for your parameters

and then you overwrite them by getting the values from EEPROM. That is Ok. I guess you used those values on the first run. If you do load this sketch in a new board, you will start with default values of 0, unless you store them first in EEPROM. Depending on your specific use (perhaps like distributing the sketch), you could store a flag on some EEPROM location that tells if the default values should be stored first during setup() Probably this is not necessary.

  1. EEPROM.put() stores values “no matter what”, while EEPROM.update() stores values only if they are different to what is already stored on EEPROM.

update() saves a few microseconds (which you will not notice) and also EEPROM store cycles (which are limited to around 100,000 and you will not notice either).

Thank you mancera1979. So after everything is finished I could just comment out the "float hp1... etc." values and it should be ok? Or should I switch to using EEPROM.update?

Those float variables still need to be declared so the compiler recognizes them as such.

You can leave them as they are as well as the EEPROM, which allows you to save values from one session to the next.

1 Like

Thank you, I'll just leave it alone then!
I would have never guessed that the hardest part of this project is trying to get a menu and submenu system going that navigates via the encoder and displays properly on the tft screen! Ive been working on it all week and I can't seem to make it work.
I plan on adding a second channel, a compressor and reverb, that will each have their own variables but will basically be repeated twice between the two channels. So instead of having 50 items on the main menu, id like to have channel 1 volume, reverb, compressor, eq menu items that go into a sub menu, and the same repeated for channel 2. What would be the best way to implement this?
Thank you!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.