M
Published © GPL3+

Ukulele Tuner

Tuner for an Ukulele/Guitar, made using a M5 Core + M5Go bottom

BeginnerShowcase (no instructions)1,713
Ukulele Tuner

Things used in this project

Hardware components

ESP32 Basic Core IoT Development Kit
M5Stack ESP32 Basic Core IoT Development Kit
×1
M5 Go Bottom
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Code

UkuleleTuner

Arduino
#include <M5Stack.h>
#include "arduinoFFT.h"
#include "esp32_digital_led_lib.h"

// Tuner:
#define FREQ_OFFSET -2.0
#define MIN_FREQ 200
#define MAX_FREQ 480

struct Note {
  char chr;
  double freq;
} notes[] = {
  { 'G', 392.0 },
  { 'C', 261.6 },
  { 'E', 329.6 },
  { 'A', 440.0 },
};

// LED
strand_t strand = {.rmtChannel = 0, .gpioNum = 15, .ledType = LED_WS2812B_V3, .brightLimit = 32, .numPixels = 10, .pixels = nullptr, ._stateVars = nullptr};
strand_t * STRANDS [] = { &strand };

// FFT:
#define SIGNAL_LENGTH 1024
#define NUM_PEAKS 8
#define NOISE_THRESHOLD 900
#define SAMPLINGFREQUENCY 10000
#define SAMPLING_TIME_US     ( 1000000UL/SAMPLINGFREQUENCY )
#define ANALOG_SIGNAL_INPUT        M5STACKFIRE_MICROPHONE_PIN
#define M5STACKFIRE_MICROPHONE_PIN 34
#define M5STACKFIRE_SPEAKER_PIN 25 // speaker DAC, only 8 Bit

double adcBuffer[SIGNAL_LENGTH];
double vImag[SIGNAL_LENGTH];

arduinoFFT FFT = arduinoFFT(adcBuffer, vImag, SIGNAL_LENGTH, SAMPLINGFREQUENCY);

double peaks[NUM_PEAKS];
int curPeakIndex = 0;
int lastMaxAmp;
double lastPeak;
double lastNonZeroPeak;
int lastNonZeroPeakMillis;
double lastDrawPeak = -1;
int lastDrawMillis = -1;


void ledBar(int r, int g, int b) {
  for (int i = 0; i < 10; i++) {
    strand.pixels[i] = pixelFromRGBW(r, g, b, 0);
  }
  digitalLeds_drawPixels(STRANDS, 1);
}

void setup()
{
  M5.begin();
  M5.Power.begin();
  M5.Power.setWakeupButton(BUTTON_A_PIN);
  
  dacWrite(M5STACKFIRE_SPEAKER_PIN, 0); // make sure that the speaker is quite
  
  M5.Lcd.begin();
  M5.Lcd.fillScreen( BLACK );

  digitalLeds_initDriver();
  digitalLeds_addStrands(STRANDS, 1);
  ledBar(255, 0, 0);
}

double findPeak() {
  // Use FFT to find current peak
  int n;
  uint32_t nextTime = 0;
  for (n = 1; n < SIGNAL_LENGTH; n++)
  {
    adcBuffer[n] = analogRead( ANALOG_SIGNAL_INPUT );

    // wait for next sample
    while (micros() < nextTime);
    nextTime = micros() + SAMPLING_TIME_US;
  }

  FFT.DCRemoval();
  FFT.Windowing(FFT_WIN_TYP_HANN, FFT_FORWARD);  /* Weigh data */
  FFT.Compute(FFT_FORWARD);
  FFT.ComplexToMagnitude();
  int maxAmplitude = 0;
  for (n = 0; n < SIGNAL_LENGTH; n++) {
    vImag[n] = 0; // clear imaginary part

    // Low & High-pass filter
    int freq = n * SAMPLINGFREQUENCY / SIGNAL_LENGTH;
    if (freq < MIN_FREQ || freq > MAX_FREQ) adcBuffer[n] = 0;

    // Amplitude calculation
    int absVal = abs(adcBuffer[n]);
    if (absVal > maxAmplitude) {
      maxAmplitude = absVal;
    }
  }

  double peak = FFT.MajorPeak();
  lastMaxAmp = maxAmplitude;
  lastPeak = peak;
  if (maxAmplitude < NOISE_THRESHOLD) return 0;  
  return peak + FREQ_OFFSET;
}

void recordCurrentPeak() {
  double peak = findPeak();
  peaks[curPeakIndex] = peak;
  curPeakIndex = (curPeakIndex + 1) % NUM_PEAKS;
}

int sort_asc(const void *cmp1, const void *cmp2)
{
  double a = *((double *)cmp1);
  double b = *((double *)cmp2);
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}

double estimateMedianPeak() {
  // Calculate # of valid samples
  int valid = 0;
  double sortedPeaks[NUM_PEAKS];
  for (int i = 0; i < NUM_PEAKS; i++) {
    if (peaks[i] > 0) {
      sortedPeaks[valid] = peaks[i];
      valid++;
    }
  }
  if (valid <= NUM_PEAKS / 2) {
    return 0; // not enough valid samples
  }
 
  // Sort peak list & pick median
  qsort(sortedPeaks, valid, sizeof(double), sort_asc);
  return sortedPeaks[valid/2];    
}

void render(double peak) { 
  char buf[20];
  M5.lcd.setTextColor(WHITE, BLACK);

  // Header
  /*M5.lcd.setTextSize(1);
  M5.lcd.setTextDatum(TL_DATUM);
  sprintf(buf, "Peak: %0.1f  ", lastPeak);
  M5.lcd.drawString(buf, 90, 5);
  sprintf(buf, "Amp: %d  ", lastMaxAmp);
  M5.lcd.drawString(buf, 180, 5);*/

  // Current freq
  M5.lcd.setTextSize(3);
  M5.lcd.setTextDatum(TC_DATUM);
  sprintf(buf, peak > 0 ? "%0.1f Hz" : "----- Hz", peak);
  M5.lcd.drawString(buf, M5.Lcd.width()/2, 40);
  M5.lcd.drawRect(50, 40-10, M5.lcd.width() - 50*2, 45, WHITE);

  // Individual notes
  M5.lcd.setTextSize(2);
  M5.lcd.setTextDatum(TL_DATUM);
  bool didMatch = false;
  for (int i = 0; i < sizeof(notes)/sizeof(Note); i++) {
    Note note = notes[i];
    int y = 100 + i*30;
    int percent = 0;
    if (peak > 0) {
      percent = min(100, max(0, (int)(50.0 + (peak - note.freq) * 250.0 / 50.0)));
    }
    
    sprintf(buf, "%c %0.1fHz", note.chr, note.freq);
    if (abs(peak - note.freq) < 0.5) {
      didMatch = true;
      M5.lcd.setTextColor(GREEN, BLACK);
    } else {
      M5.lcd.setTextColor(WHITE, BLACK);
    }
    M5.lcd.drawString(buf, 20, y);

    y -= 1;
    int barX = 140;
    int barWidth = M5.lcd.width() - barX - 20;
    M5.lcd.fillRect(barX, y, barWidth/2, 20, BLACK);    
    M5.lcd.fillRect(barX+barWidth/2, y, barWidth/2, 20, BLACK);    
    M5.lcd.drawFastVLine(barX+barWidth/2-1, y, 20, WHITE); // Middle line    
    if (percent > 0 && percent < 100) {
      int barValue = percent * barWidth / 100;
      int cursorWidth = 3;
      M5.lcd.fillRect(barX+barValue-cursorWidth/2, y, cursorWidth, 20, GREEN);    
    }
    M5.lcd.drawRect(barX-1, y-1, barWidth+2, 20+2, WHITE);    
  }

  if (didMatch) { ledBar(0, 255, 0); }
  else { ledBar(255, 0, 0); }
}

void loop(void)
{
  recordCurrentPeak();
  double peak = estimateMedianPeak();
  if (peak > 0) { 
    lastNonZeroPeak = peak;
    lastNonZeroPeakMillis = millis();
  }
  double effectivePeak = peak != 0 ? peak : (millis() - lastNonZeroPeakMillis < 1000 ? lastNonZeroPeak : 0); 
  if (effectivePeak != lastDrawPeak && millis() - lastDrawMillis > 20) {
    lastDrawPeak = effectivePeak;
    render(effectivePeak);
    lastDrawMillis = millis();
  }
  M5.update();
}

Credits

M

M

1 project • 0 followers

Comments