#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();
}
Comments