28 May 2019
As a term project, we designed and implemented a guitar (or any instrument) tuner device using the FRDM-KL25Z board, a microphone and an OLED display. The tuner reads the sound from the microphone, breaks down the signal into its frequency components, displays the frequency with the most amplitude (the base frequency), and also displays the closest musical note to that frequency, indicating whether the instrument should be tuned up (higher pitch) or tuned down (lower pitch) to match that closest musical note. An analog microphone is used to capture the input audio signal, the FRDM-KL25Z board acts as the digital signal processor, and a 128x64 OLED screen displays the user interface.
By using Discrete Fourier Transform (DFT), one can decompose a sampled discrete digital signal into a coefficient matrix which represent the required “weights” to reproduce the sampled signal; provided that the sampling frequency F_s is equal or greater than the bandwidth B of the input signal (due to Nyquist–Shannon sampling theorem). A typical guitar can produce sounds ranging from the note C2 (65.41 Hz) to F4 (349.23 Hz). To round things off, a sampling frequency of 1000 \text{ Hz} will suffice to correctly analyse sounds up to B4 (493 Hz). The incoming sound signal x[t] will be transformed into the frequency domain X[jw]. Then the magnitudes of these complex coefficients will be calculated to produce |X[jw]|. Since the guitar sound contains many harmonics, the frequency with the highest magnitude will be extracted, and compared with the closest musical note.
The analog output of the microphone is connected to the board (via
the PTC2
port) and the OLED screen is connected to the
board to enable I2C (Inter-Integrated Circuit) communications which
allows us to display the characters on the screen.
Board Pins | OLED Display | Microphone |
---|---|---|
PTC2 | - | Out |
PTC8 | SCL | - |
PTC9 | SDA | - |
The code itself is at Appendix A; this section describes the behaviour.
We will be using the mbed
environment to program the
board. The library mbed-dsp
contains functions
arm_cfft_f32()
and arm_cmplx_mag_f32()
to
calculate the Fourier transform and magnitudes of complex numbers
respectively. The library FastAnalogIn
is used to sample
the input pin at the sampling frequency and read the input signal as an
array of 512 elements. The library Adafruit_GFX
enables us
to draw shapes and text on the OLED screen.
The array float samples[FFT_SIZE*2]
holds the samples
read at each loop (note that the size is FFT_SIZE*2
because
we need two elements for each complex coefficient). Then the function
arm_cfft_f32()
is called with this array, overwriting it
with the complex coefficients a, b. The
function arm_cmplx_mag_f32()
processes this array to
calculate the magnitudes of these complex coefficients, putting them in
an array of type float magnitudes[FFT_SIZE]
. The index of
the maximum element in this array corresponds to the base frequency of
the input audio signal. The closest musical note to this frequency is
calculated from a lookup table and the difference is stored to be
displayed. To eliminate possible noise, these values are displayed once
every 1024 loop: this trivially reduces response time, but greatly
increases the overall accuracy of the system.
The screen shows to the user the current frequency of the signal in the first row, the second row shows the closest musical note, and how far away it is. Figure 4 shows an example scenario: the input signals frequency is 433 Hz, the closest musical note to this frequency is A4, which is 440 Hz so the user has to tighten the string to increase the frequency by 7 Hz.
There exists two minor differences between the final product and the proposal. The FRDM-KL25Z board uses the Cortex-M0+ architecture, which imposes a hardware limit on the biggest FFT size (512 bins): since we are using a sampling rate of 1000 Hz this means a resolution of 1000/512 = 1.95312. As a result the sensitivity is about \pm 1 Hz.
Also, the user interface is visually different than the proposed one
because of the limitations of the library Adafruit_GFX
and
the size of the OLED screen (128x64). Nevertheless the relevant
information is legibly displayed on the screen.
All together, each group member spent about 12 hours for a total of 24. Group member Miraç L. Gülgönül was tasked with the audio processing tasks: reading the signal from the pin with the sampling rate, calculating the DTFT(Discrete-Time Fourier Transform) of the signal and finding the corresponding frequency with the biggest amplitude. Group member S. Çağatay Çelebi was tasked with interfacing with the OLED screen using the libraries: displaying the frequency, the musical note and the distance from the musical note.
This process was informative in various aspects such as collaborative
coding and division of labor. Also, the coding environment provided by
the mbed
platform was very different from coding in
8051-Assembly
. The code was easier to read, we could do
debugging and we could divide the code to components more easily.
Obviously each paradigm has its advantages: Assembly
allows
for more compact and bare metal coding whereas C++
provides
more abstraction and ease of coding: however at the modern age we
believe that programmer time is more important than processing time.
/* Instrument tuner using the FRDM-KL25Z Board. Written for EEE212 Term Project.
* Miraç L. Gülgönül - S. Çağatay Çelebi
* 20 May 2019
*/
#include "mbed.h"
#include "NVIC_set_all_priorities.h"
#include <ctype.h>
#include "arm_math.h"
#include "arm_const_structs.h"
#include "FastAnalogIn.h"
#include "TextLCD.h"
#include <algorithm>
#include <string>
#include "Adafruit_GFX.h"
#include "Adafruit_GFX_Config.h"
#include "Adafruit_SSD1306.h"
#include "glcdfont.h"
using namespace std;
(PTC9, PTC8);
I2C comms
(comms, PTC3, 0x78, 64, 128);
Adafruit_SSD1306_I2c oled
(PTC2); // INPUT
FastAnalogIn Audio
// Dummy ISR for disabling NMI on PTA4.
// More info at https://mbed.org/questions/1387/How-can-I-access-the-FTFA_FOPT-register-/
extern "C" void NMI_Handler()
{
(PTA4);
DigitalIn test}
// A guitar is at max from C2 to F4: which gives 65.41Hz to 349.23Hz
// Supported highest freq is 493 Hz, B4.
const int SAMPLE_RATE_HZ = 1000;
const int WINDOW_SIZE = 1024; // WAS 1024
const int NOTE_SIZE = 39;
float max_amp = 0;
int max_idx = 0;
int max_idxs[WINDOW_SIZE];
int window_idx = 0;
const int FFT_SIZE = 512; // Size of the FFT.
const char * const notes[NOTE_SIZE] = {
"A1", "A#1", "B1", "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2",
"A2", "A#2", "B2", "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3",
"A3", "A#3", "B3", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4",
"A4", "A#4", "B4",
};
const int note_freqs[NOTE_SIZE] = {
55, 58, 62, 65, 69, 73, 78, 82, 87, 92, 98, 103, 110, 116, 123,
131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 246, 261,
277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 493
};
int differences[39];
int min_diff = 1000;
int min_diff_idx = 10;
= notes[min_diff_idx];
string closest_note
const static arm_cfft_instance_f32 *S;
;
Ticker samplingTimerfloat samples[FFT_SIZE*2];
float magnitudes[FFT_SIZE];
int sampleCounter = 0;
////////////////////////////////////////////////////////////////////////////////
// SAMPLING FUNCTIONS
////////////////////////////////////////////////////////////////////////////////
void samplingCallback()
{
// Read from the ADC and store the sample data
[sampleCounter] = (1023 * Audio) - 511.0f;
samples// Complex FFT functions require a coefficient for the imaginary part of the input.
// Since we only have real data, set this coefficient to zero.
[sampleCounter+1] = 0.0;
samples// Update sample buffer position and stop after the buffer is filled
+= 2;
sampleCounter if (sampleCounter >= FFT_SIZE*2) {
.detach();
samplingTimer}
}
void samplingBegin()
{
// Reset sample buffer position and start callback at necessary rate.
= 0;
sampleCounter .attach_us(&samplingCallback, 1000000/SAMPLE_RATE_HZ);
samplingTimer}
bool samplingIsDone()
{
return sampleCounter >= FFT_SIZE*2;
}
////////////////////////////////////////////////////////////////////////////////
// MAIN FUNCTION
////////////////////////////////////////////////////////////////////////////////
int main()
{
(1);
NVIC_set_all_irq_priorities(UART0_IRQn, 0);
NVIC_SetPriority.setTextSize(3);
oled// Begin sampling audio
();
samplingBegin
= & arm_cfft_sR_f32_len512;
S
while(1) {
// Calculate FFT if a full sample is available.
if (samplingIsDone()) {
// Does a Hanning on samples:
for (int i = 0; i < FFT_SIZE*2; i++) {
if (i % 2 == 0) {
[i] = samples[i] * (1-cos(2*3.141592653589793/(FFT_SIZE)));
samples} else {
[i] = 0.0;
samples}
}
// Run FFT on sample data.
(S, samples, 0, 1);
arm_cfft_f32// Calculate magnitude of complex numbers output by the FFT.
(samples, magnitudes, FFT_SIZE);
arm_cmplx_mag_f32
// Restart audio sampling.
();
samplingBegin}
= 0;
max_amp = 0;
max_idx
for(int i = 1; i < FFT_SIZE; i++) {
if(magnitudes[i] > max_amp && i < 256) {
= magnitudes[i];
max_amp = i;
max_idx }
}
[window_idx] = max_idx;
max_idxs
if(window_idx == WINDOW_SIZE - 1) {
= 0;
window_idx // sorts the array
(max_idxs, max_idxs+100);
sort
// finds the most repeated element
int repeat = max_idxs[0];
int mode = repeat;
int mode_freq = mode;
int count = 1;
int count_mode = 1;
for(int i = 1; i < WINDOW_SIZE; i++) {
if(max_idxs[i] == repeat) {
++count;
} else {
if(count > count_mode) {
= count;
count_mode = repeat;
mode }
= 1;
count = max_idxs[i];
repeat }
}
= static_cast<int>(floor(mode*1.953125)); // for FFT_SIZE = 512, Fs = 1000.
mode_freq
for(int i = 0; i < NOTE_SIZE; i++) {
[i] = mode_freq - note_freqs[i];
differences}
= 1000;
min_diff = 10;
min_diff_idx
for(int i = 0; i < NOTE_SIZE; i++) {
if(abs(differences[i]) < min_diff) {
= differences[i];
min_diff = i;
min_diff_idx }
}
= notes[min_diff_idx];
closest_note char mode_freq_str[10];
char closest_note_char[10];
char min_diff_char[10];
(closest_note_char, "%s", closest_note);
sprintf(mode_freq_str, "%d", mode_freq);
sprintf(min_diff_char, "%d", -1*min_diff);
sprintf
.clearDisplay();
oled.setTextCursor(0,0);
oled
for(int i = 0; i < 3 ; i++) {
.writeChar(mode_freq_str[i]);
oled}
.writeChar('\n');
oled
for(int i = 0; i < 3 ; i++) {
.writeChar(closest_note_char[i]);
oled}
.writeChar(' ');
oled.writeChar(' ');
oled
for(int i = 0; i < 2 ; i++) {
.writeChar(min_diff_char[i]);
oled}
.display();
oled
} else {
= window_idx + 1;
window_idx }
}
}