String Tuner
By Scott Landry, Dalton Clark, and Young Chang
Overview
The mbed-based guitar/string tuner has two separate modes: a "Tune by ear" mode that outputs a tone to the on-board speaker, and a "Tune by response" mode that shows the user how close a note played is to a selected set of notes
Tune By Ear
This mode will generate a note selected by the user using the joystick to select the note. The note is played by pressing the fire button, and the user can cycle between notes using the up and down controls
Tune By Response
The "Tune by response" option utilizes a microphone attached to a computer to determine how close the current tone or sound is to one of the six notes in standard tuning. The input from the microphone is picked up by a C# application, and a Fast Fourier Transform is applied to the input to determine the frequency of the note that is being played by the user. The frequency is sent across a serial connection to the mbed, and then a graph is displayed in real-time that shows how close the frequency sent across serial is to a particular note (the note displayed is the closest note frequency to the received frequency). The graph will display a blue bar that is either over or under a white line on the uLCD display. This corresponds to the note being either sharper or flatter than the particular note.
Components
A joystick is used to navigate the menus for the program, while the tone selected by the user to play in the "Tune by ear" option is sent on a PwmOut pin to the Class-D amp and on-board speaker. The display for the string tuner uses the uLCD screen. The audio input for the C# component uses a standard computer microphone (for testing purposes, a microphone from a USB webcam was used).
Pinout
Navigation stick | mbed |
---|---|
R | p5 |
D | p6 |
L | p7 |
C | p8 |
U | p9 |
- | gnd |
uLCD cable | mbed |
---|---|
Res | p29 |
Gnd | gnd |
Tx | p27 |
Rx | p26 |
+5V | external (6V) power source |
mbed | Class D Amp | Speaker |
---|---|---|
p21 | IN + | |
gnd | IN -, PWR- | |
External (6V) power source | PWR + | |
OUT + | + | |
OUT - | - |
Code
main.cpp
#include "mbed.h" #include "rtos.h" #include "uLCD_4DGL.h" #include "joyNav.h" #include "menuDraw.h" uLCD_4DGL uLCD(p28,p27,p29); // serial tx, serial rx, reset pin; BusOut myleds(LED1,LED2,LED3,LED4); Nav_Switch myNav( p9, p6, p7, p5, p8); PwmOut speaker(p21); Serial pc(USBTX, USBRX); // tx, rx char *noteDisp[] = {"E2","A2","D3","G3","B3","E4"}; float hertzOut[] ={82.41,110,146.83,196.00,246.94,329.63}; Mutex ulcdMutex; Mutex waveBufferMutex; Mutex micMutex; Mutex spkrMutex; Mutex menuStateMutex; int menuState = 1; int tuneState = 0; int noteState = 0; int optionSelect = 0; bool initialDraw = false; bool isPlaying = false; void tunerDisplay(void const *args) { int menuStateTuner; //thread that controls the uLCD display while(true) { menuStateMutex.lock(); menuStateTuner = menuState; menuStateMutex.unlock(); if(menuState & MENU_INIT) { //main menu if(!initialDraw) { //draw initial state of menu ulcdMutex.lock(); uLCD.cls(); uLCD.color(WHITE); uLCD.locate(2,0); uLCD.printf("STRING TUNER"); uLCD.locate(0,5); //Tune by response is pre-selected uLCD.filled_rectangle(0,40,127,49,WHITE); uLCD.color(BLACK); uLCD.textbackground_color(WHITE); uLCD.printf("> Tune by response"); //switch text colors back uLCD.color(WHITE); uLCD.textbackground_color(BLACK); uLCD.locate(0,7); uLCD.printf(" Tune by ear"); ulcdMutex.unlock(); //set initDraw state to true so it won't draw again initialDraw = true; } } if(menuStateTuner & MENU_RESP) { //response tuning menu if(!initialDraw) { //draw white bar and "initial tuning" text ulcdMutex.lock(); uLCD.cls(); drawBar(&uLCD); uLCD.text_string("Standard tuning",3,0,FONT_7X8, WHITE); ulcdMutex.unlock(); initialDraw = true; } } if(menuStateTuner & MENU_EAR) { //tune by ear menu if(!initialDraw) { //draw selected note ulcdMutex.lock(); uLCD.cls(); drawByEarMenu(&uLCD,noteDisp[(tuneState*6)+noteState]); ulcdMutex.unlock(); initialDraw = true; } } } } void navControl(void const *args) { //thread that controls the navigation control for the tuner int menuStateNav; while(true) { menuStateMutex.lock(); menuStateNav = menuState; menuStateMutex.unlock(); if(myNav.left()) { //go back to main menu if on any other sub-menu if(menuStateNav & MENU_EAR) { if(!tuneState) { //turn off speaker spkrMutex.lock(); speaker = 0; spkrMutex.unlock(); isPlaying = false; menuStateMutex.lock(); menuState = MENU_INIT; menuStateMutex.unlock(); optionSelect = 0; initialDraw = false; } } if(menuStateNav & MENU_RESP) { if(!tuneState) { menuStateMutex.lock(); menuState = MENU_INIT; menuStateMutex.unlock(); initialDraw = false; } } } if(myNav.right()) { } if(myNav.up()) { if(menuStateNav & MENU_INIT) { //switch selected item if on main menu if(optionSelect) { optionSelect &= 0; ulcdMutex.lock(); //draw deselected item uLCD.locate(0,7); uLCD.filled_rectangle(0,(7*8)-1,127,(7*8)+9,BLACK); uLCD.printf(" Tune by ear"); //draw selected item uLCD.locate(0,5); uLCD.filled_rectangle(0,40,127,49,WHITE); uLCD.color(BLACK); uLCD.textbackground_color(WHITE); uLCD.printf("> Tune by response"); //reset text fg/bg color uLCD.textbackground_color(BLACK); uLCD.color(WHITE); ulcdMutex.unlock(); } } if(menuStateNav & MENU_RESP) { } if(menuStateNav & MENU_EAR) { //cycle through notes if on Tune by ear menu noteState = (noteState > 0 ? noteState - 1 : 0); ulcdMutex.lock(); //blank previous note //draw new note redrawNote(&uLCD, noteDisp[(tuneState*6)+noteState]); ulcdMutex.unlock(); } } if(myNav.down()) { if(menuStateNav & MENU_INIT) { if(!optionSelect) { optionSelect |= 1; //draw menu selection ulcdMutex.lock(); //draw deselected item uLCD.locate(0,5); uLCD.filled_rectangle(0,(5*8)-1,127,(5*8)+9,BLACK); uLCD.printf(" Tune by response"); //draw selected item uLCD.locate(0,7); uLCD.filled_rectangle(0,(7*8)-1,127,(7*8)+9,WHITE); uLCD.color(BLACK); uLCD.textbackground_color(WHITE); uLCD.printf("> Tune by ear"); //reset text fg/bg color uLCD.textbackground_color(BLACK); uLCD.color(WHITE); ulcdMutex.unlock(); } } if(menuStateNav & MENU_RESP) { } if(menuStateNav & MENU_EAR) { //cycle through notes noteState = (noteState + 1) % 6; ulcdMutex.lock(); //blank previous note //draw new note redrawNote(&uLCD, noteDisp[(tuneState*6)+noteState]); ulcdMutex.unlock(); } } if(myNav.fire()) { if(menuStateNav & MENU_INIT) { //if on main menu, go to next menu if(!optionSelect) { //go to "Tune by response" screen menuStateMutex.lock(); menuState = MENU_RESP; menuStateMutex.unlock(); initialDraw = false; } else { //go to "Tune by ear" screen menuStateMutex.lock(); menuState = MENU_EAR; menuStateMutex.unlock(); initialDraw = false; } Thread::wait(100); } if(menuStateNav & MENU_EAR) { //play note on speaker if Tune by ear menu spkrMutex.lock(); speaker = (isPlaying ? 0 : 0.001); speaker.period(1/hertzOut[(6*tuneState)+noteState]); isPlaying = !isPlaying; spkrMutex.unlock(); } if(menuStateNav & MENU_RESP) { } } Thread::wait(200); } } /*void speakerPlay(void const *args) { while(true) { //speaker.period(1/f) sets output tone //speaker = v sets volume output //if(menuState & MENU_EAR) { spkrMutex.lock(); speaker = 0.001; speaker.period(1.0/440.0); Thread::wait(1000); //speaker = (isPlaying ? 0.7 : 0); spkrMutex.unlock(); //} } }*/ int main() { Thread threadDisp(tunerDisplay); //initial display thread Thread threadNav(navControl); //navigation control thread //Thread threadspkr(speakerPlay); char fBuff[10] = {0}; float freq = 0; int frTblIndex = 0; int menuStateMain; while(true) { menuStateMutex.lock(); menuStateMain = menuState; menuStateMutex.unlock(); if(menuStateMain & MENU_RESP) { pc.gets(fBuff,4); //stores the received frequency to the buffer freq = atof(fBuff); //atof converts the buffer into an float frTblIndex = getClosestDraw(hertzOut, (float)(freq)); //find closest note to one read in from serial if(freq != 0.0f) { //ignore read-in value of 0 ulcdMutex.lock(); //display result on uLCD interpretInd(&uLCD,frTblIndex, hertzOut, (float)freq, noteDisp); ulcdMutex.unlock(); } } } }
menuDraw.cpp
#include "mbed.h" #include "uLCD_4DGL.h" void drawByEarMenu(uLCD_4DGL *u, char *initNote) { (*u).locate(2,1); (*u).printf("Standard tuning"); (*u).locate(2,2); (*u).printf("Current note:"); (*u).locate(2,3); (*u).text_width(4); (*u).text_height(4); (*u).printf("%s",initNote); (*u).text_width(1); (*u).text_height(1); //draw play/pause icon at bottom (*u).triangle(24,118,24,126,32,122,WHITE); } void redrawNote(uLCD_4DGL *u, char *note) { (*u).locate(2,3); (*u).filled_rectangle(16,24,47,55,BLACK); (*u).text_width(4); (*u).text_height(4); (*u).printf("%s",note); (*u).text_width(1); (*u).text_height(1); } void drawHertz(uLCD_4DGL *u, float hz) { (*u).locate(2,10); (*u).filled_rectangle(0,8*10,127,(8*10)+7,BLACK); (*u).printf("%f",hz*1); } void drawBar(uLCD_4DGL *u) { (*u).locate(0,9); (*u).filled_rectangle(0,8*9,127,(8*9)+7,WHITE); } //void drawCloseBars(uLCD_4DGL *u) { //} //void drawEmpty(uLCD_4DGL *u) { //} int getClosestDraw(float *freqArr, float inFreq) { float diff = abs(inFreq - freqArr[0]); int index = 0; for(int i = 0; i < 6; i++) { if(diff > abs(inFreq - freqArr[i])) { diff = abs(inFreq - freqArr[i]); index = i; } } return index; } /* Bar graph for each note is scaled differently for each note */ void interpretInd(uLCD_4DGL *u,int index, float *freqArr, float freq, char **noteNameArr) { //since the scales may be different for each note, separate them with logic statements float scalePixel = 0; char freqS[4] = {0}; //used to debug by displaying frequency on bar sprintf(freqS,"%f",freq); (*u).filled_rectangle(0,8,127,15,BLACK); //display note name (*u).text_string(noteNameArr[index],0,1,FONT_7X8,WHITE); if(index == 0) { //drawing bars for E //82 Hz if(freq <= 64) { //all empty (*u).filled_rectangle(0,8*4,127,127,BLACK); } else if(freq >= 100) { //draw full bars (*u).filled_rectangle(0,8*4,127,127,BLUE); } else { scalePixel = freq - 64; scalePixel *= (2.5); (*u).filled_rectangle(0,127-(int)(scalePixel),127,127,BLUE); (*u).filled_rectangle(0,8*4,127,126 - (int)(scalePixel),BLACK); } } else if(index == 1) { //drawing bars for A //110Hz if(freq <= 90) { (*u).filled_rectangle(0,8*4,127,127,BLACK); } else if(freq >= 120) { (*u).filled_rectangle(0,8*4,127,127,BLUE); } else { scalePixel = freq - 90; scalePixel *= (2.7); (*u).filled_rectangle(0,127-(int)(scalePixel),127,127,BLUE); (*u).filled_rectangle(0,8*4,127,126 - (int)(scalePixel),BLACK); } } else if(index == 2) { //drawing bars for D //146.83Hz //accurate if(freq <= 118) { (*u).filled_rectangle(0,8*4,127,127,BLACK); } else if(freq >= 158) { (*u).filled_rectangle(0,8*4,127,127,BLUE); } else { scalePixel = freq - 118; scalePixel *= (1.5); (*u).filled_rectangle(0,127-(int)(scalePixel),127,127,BLUE); (*u).filled_rectangle(0,8*4,127,126 - (int)(scalePixel),BLACK); } } else if(index == 3) { //drawing bars for G //196Hz if(freq <= 160) { (*u).filled_rectangle(0,8*4,127,127,BLACK); } else if(freq >= 216) { (*u).filled_rectangle(0,8*4,127,127,BLUE); } else { scalePixel = freq - 160; scalePixel *= (8/4.9); (*u).filled_rectangle(0,127-(int)(scalePixel),127,127,BLUE); (*u).filled_rectangle(0,8*4,127,126 - (int)(scalePixel),BLACK); } } else if(index == 4) { //drawing bars for B //246.94Hz if(freq <= 218) { (*u).filled_rectangle(0,8*4,127,127,BLACK); } else if(freq >= 266) { (*u).filled_rectangle(0,8*4,127,127,BLUE); } else { scalePixel = freq - 218; scalePixel *= (8/4.5); (*u).filled_rectangle(0,127-(int)(scalePixel),127,127,BLUE); (*u).filled_rectangle(0,8*4,127,126 - (int)(scalePixel),BLACK); } } else { //drawing bars for E //329.63Hz if(freq <= 294) { (*u).filled_rectangle(0,8*4,127,127,BLACK); } else if(freq >= 349) { (*u).filled_rectangle(0,8*4,127,127,BLUE); } else { scalePixel = freq - 294; scalePixel *= (1.4); (*u).filled_rectangle(0,127-(int)(scalePixel),127,127,BLUE); (*u).filled_rectangle(0,8*4,127,126 - (int)(scalePixel),BLACK); } } (*u).filled_rectangle(0,8*9,127,(8*9)+7,WHITE); //(*u).text_string(freqS,3,9,FONT_7X8, RED); //display frequency on white line }
menuDraw.h
void drawByEarMenu(uLCD_4DGL *u, char *initNote); void redrawNote(uLCD_4DGL *u, char *note); void drawHertz(uLCD_4DGL *u, float hz); void drawBar(uLCD_4DGL *u); int getClosestDraw(float *freqArr, float inFreq); void interpretInd(uLCD_4DGL *u,int index, float *freqArr, float freq, char **noteNameArr);
joyNav.cpp
#include "mbed.h" #include "joyNav.h" Nav_Switch::Nav_Switch (PinName up,PinName down,PinName left,PinName right,PinName fire): _pins(up, down, left, right, fire) { _pins.mode(PullUp); //needed if pullups not on board or a bare nav switch is used - delete otherwise wait(0.001); //delays just a bit for pullups to pull inputs high } bool Nav_Switch::up() { return !(_pins[0]); } bool Nav_Switch::down() { return !(_pins[1]); } bool Nav_Switch::left() { return !(_pins[2]); } bool Nav_Switch::right() { return !(_pins[3]); } bool Nav_Switch::fire() { return !(_pins[4]); } int Nav_Switch::read() { return _pins.read(); } Nav_Switch::operator int () { return _pins.read(); }
joyNav.h
#include "mbed.h" #define MENU_INIT 1 #define MENU_RESP 2 #define MENU_EAR 4 class Nav_Switch { public: Nav_Switch(PinName up,PinName down,PinName left,PinName right,PinName fire); int read(); //boolean functions to test each switch bool up(); bool down(); bool left(); bool right(); bool fire(); //automatic read on RHS operator int (); //index to any switch array style bool operator[](int index) { return _pins[index]; }; private: BusIn _pins; };
Import programguitarTuner
Guitar Tuner project for ECE 4180
The C# code that sends the frequency over serial:
Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Un4seen.Bass; using Un4seen.BassWasapi; using Un4seen.BassAsio; using System.IO.Ports; namespace ConsoleApplication1 { class Program { static int Main() { int[] freqDump = new int[4096]; int hz; Int32 record; Bass.BASS_Init(-1, 44100, BASSInit.BASS_DEVICE_MONO, IntPtr.Zero); int a; BASS_DEVICEINFO info = new BASS_DEVICEINFO(); for (int n = 0; Bass.BASS_RecordGetDeviceInfo(n, info); n++) { Console.WriteLine(info.ToString()); Console.WriteLine("{0}", n); } if(!Bass.BASS_RecordInit(-1)) { Console.WriteLine("Error initializing recording device"); } if (!Bass.BASS_RecordSetDevice(0)) { Console.WriteLine("Error setting recording device"); } Bass.BASS_RecordSetInput(0, BASSInput.BASS_INPUT_ON, -1); record = Bass.BASS_RecordStart(44100, 1, BASSFlag.BASS_SAMPLE_FLOAT, null, IntPtr.Zero); Console.WriteLine(record); float[] fft = new float[2048]; string hertzS; SerialPort _serialPort = new SerialPort("COM5"); _serialPort.Open(); while (true) { Bass.BASS_ChannelGetData(record, fft, (int)BASSData.BASS_DATA_FFT4096); if (Utils.FFTIndex2Frequency(fft.ToList().IndexOf(fft.Max()), 4096, 44100) > 60 && Utils.FFTIndex2Frequency(fft.ToList().IndexOf(fft.Max()), 4096, 44100) < 350) { hz = Utils.FFTIndex2Frequency(fft.ToList().IndexOf(fft.Max()), 4096, 44100); //Console.WriteLine("{0}", fft.ToList().IndexOf(fft.Max())); hertzS = hz.ToString().PadLeft(3, '0'); Console.WriteLine("{0} Hz",hertzS); _serialPort.WriteLine(hz.ToString().PadLeft(3, '0')); System.Threading.Thread.Sleep(200); } } _serialPort.Close(); Console.ReadKey(); return 0; } } }
Image
Guitar tuner project set up on bread board (not pictured: external microphone and computer used for getting frequency)
Please log in to post comments.