Maker.io main logo

Ukulele Tuner

25

2022-02-07 | By M5Stack

License: General Public License Arduino

* Thanks for the source code and project information provided by @M

 

Story

Since I always have an M5 Stack around, and my phone Ukulele Tuner app was too slow to open, I decided to quickly build my own instead.

It's based on the Microphone FFT example and uses an FFT with a Low & High-pass filter to detect the median frequency peak over a short time window, and displays the delta to the common Ukulele notes.

 

Code

Copy Code
#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();
}
制造商零件编号 K001
ESP32 BASIC CORE IOT DEV KIT
M5Stack Technology Co., Ltd.
制造商零件编号 K001-V26
ESP32 BASIC CORE IOT V2.6 DEVKIT
M5Stack Technology Co., Ltd.
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.