| /* |
| File/Sketch Name: MusicalNoteDetector |
|
| Version No.: v1.0 Created 7 June, 2020 |
|
| Original Author: Clyde A. Lettsome, PhD, PE, MEM |
|
| Description: This code/sketch displays the approximate frequency as well as the musical note played on an electronic keyboard or piano app. For this project, the analog output from the |
| sound module detector is sent to the A0 analog input of the Arduino Uno. The analog signal is sampled and quantized (digitized). Autocorrelation, weighting and tuning code is used to |
| find fundamental frequency using the first 3 periods. The approximate fundamental frequency is then compared to frequencies in octaves 3, 4, and 5 range to determine the closest musical |
| note frequency. Finally the guessed note for the closest frequency is printed to the screen. |
|
| License: This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License (GPL) version 3, or any later |
| version of your choice, as published by the Free Software Foundation. |
|
| Notes: Copyright (c) 2020 by C. A. Lettsome Services, LLC |
| For more information visit https://clydelettsome.com/blog/2020/06/07/my-weekend-project-musical-note-detector-using-an-arduino/ |
|
| */ |
| #define SAMPLES 128 //Max 128 for Arduino Uno. |
| #define SAMPLING_FREQUENCY 2048 //Fs = Based on Nyquist, must be 2 times the highest expected frequency. |
| #define OFFSETSAMPLES 40 //used for calabrating purposes |
| #define TUNER -3 //Adjust until C3 is 130.50 |
|
| float samplingPeriod; |
| unsigned long microSeconds; |
|
| int X[SAMPLES]; //create vector of size SAMPLES to hold real values |
| float autoCorr[SAMPLES]; //create vector of size SAMPLES to hold imaginary values |
| float storedNoteFreq[12] = {130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185, 196, 207.65, 220, 233.08, 246.94}; |
|
| int sumOffSet = 0; |
| int offSet[OFFSETSAMPLES]; //create offset vector |
| int avgOffSet; //create offset vector |
|
| int i, k, periodEnd, periodBegin, period, adjuster, noteLocation, octaveRange; |
| float maxValue, minValue; |
| long sum; |
| int thresh = 0; |
| int numOfCycles = 0; |
| float signalFrequency, signalFrequency2, signalFrequency3, signalFrequencyGuess, total; |
| byte state_machine = 0; |
| int samplesPerPeriod = 0; |
|
|
| void setup() |
| { |
| Serial.begin(115200); //115200 Baud rate for the Serial Monitor |
| } |
|
| void loop() |
| { |
| //***************************************************************** |
| //Calabration Section |
| //***************************************************************** |
| Serial.println("Calabrating. Please do not play any notes during calabration."); |
| for (i = 0; i < OFFSETSAMPLES; i++) |
| { |
| offSet[i] = analogRead(0); //Reads the value from analog pin 0 (A0), quantize it and save it as a real term. |
| //Serial.println(offSet[i]); //use this to adjust the sound detection module to approximately half or 512 when no sound is played. |
| sumOffSet = sumOffSet + offSet[i]; |
| } |
| samplesPerPeriod = 0; |
| maxValue = 0; |
|
| //***************************************************************** |
| //Prepare to accept input from A0 |
| //***************************************************************** |
| avgOffSet = round(sumOffSet / OFFSETSAMPLES); |
| Serial.println("Counting down."); |
| delay(1000); //pause for 1 seconds |
| Serial.println("3"); |
| delay(1000); //pause for 1 seconds |
| Serial.println("2"); |
| delay(1000); //pause for 1 |
| Serial.println("1"); |
| delay(1000); //pause for 1 seconds |
| Serial.println("Play your note!"); |
| delay(250); //pause for 1/4 second for reaction time |
|
| //***************************************************************** |
| //Collect SAMPLES samples from A0 with sample period of samplingPeriod |
| //***************************************************************** |
| samplingPeriod = 1.0 / SAMPLING_FREQUENCY; //Period in microseconds |
| for (i = 0; i < SAMPLES; i++) |
| { |
| microSeconds = micros(); //Returns the number of microseconds since the Arduino board began running the current script. |
| X[i] = analogRead(0); //Reads the value from analog pin 0 (A0), quantize it and save it as a real term. |
|
| /*remaining wait time between samples if necessary in seconds */ |
| while (micros() < (microSeconds + (samplingPeriod * 1000000))) |
| { |
| //do nothing just wait |
| } |
| } |
|
| //***************************************************************** |
| //Autocorrelation Function |
| //***************************************************************** |
|
| for (i = 0; i < SAMPLES; i++) //i=delay |
| { |
| sum = 0; |
| for (k = 0; k < SAMPLES - i; k++) //Match signal with delayed signal |
| { |
| sum = sum + (((X[k]) - avgOffSet) * ((X[k + i]) - avgOffSet)); //X[k] is the signal and X[k+i] is the delayed version |
| } |
| autoCorr[i] = sum / SAMPLES; |
|
| // First Peak Detect State Machine |
| if (state_machine==0 && i == 0) |
| { |
| thresh = autoCorr[i] * 0.5; |
| state_machine = 1; |
| } |
| else if (state_machine == 1 && i>0 && thresh < autoCorr[i] && (autoCorr[i]-autoCorr[i-1])>0) //state_machine=1, find 1 period for using first cycle |
| { |
| maxValue = autoCorr[i]; |
|
| } |
| else if (state_machine == 1&& i>0 && thresh < autoCorr[i-1] && maxValue == autoCorr[i-1] && (autoCorr[i]-autoCorr[i-1])<=0) |
| { |
| periodBegin = i-1; |
| state_machine = 2; |
| numOfCycles = 1; |
| samplesPerPeriod = (periodBegin - 0); |
| period = samplesPerPeriod; |
| adjuster = TUNER+(50.04 * exp(-0.102 * samplesPerPeriod)); |
| signalFrequency = ((SAMPLING_FREQUENCY) / (samplesPerPeriod))-adjuster; // f = fs/N |
| } |
| else if (state_machine == 2 && i>0 && thresh < autoCorr[i] && (autoCorr[i]-autoCorr[i-1])>0) //state_machine=2, find 2 periods for 1st and 2nd cycle |
| { |
| maxValue = autoCorr[i]; |
| } |
| else if (state_machine == 2&& i>0 && thresh < autoCorr[i-1] && maxValue == autoCorr[i-1] && (autoCorr[i]-autoCorr[i-1])<=0) |
| { |
| periodEnd = i-1; |
| state_machine = 3; |
| numOfCycles = 2; |
| samplesPerPeriod = (periodEnd - 0); |
| signalFrequency2 = ((numOfCycles*SAMPLING_FREQUENCY) / (samplesPerPeriod))-adjuster; // f = (2*fs)/(2*N) |
| maxValue = 0; |
| } |
| else if (state_machine == 3 && i>0 && thresh < autoCorr[i] && (autoCorr[i]-autoCorr[i-1])>0) //state_machine=3, find 3 periods for 1st, 2nd and 3rd cycle |
| { |
| maxValue = autoCorr[i]; |
| } |
| else if (state_machine == 3&& i>0 && thresh < autoCorr[i-1] && maxValue == autoCorr[i-1] && (autoCorr[i]-autoCorr[i-1])<=0) |
| { |
| periodEnd = i-1; |
| state_machine = 4; |
| numOfCycles = 3; |
| samplesPerPeriod = (periodEnd - 0); |
| signalFrequency3 = ((numOfCycles*SAMPLING_FREQUENCY) / (samplesPerPeriod))-adjuster; // f = (3*fs)/(3*N) |
| } |
| } |
|
| //***************************************************************** |
| //Result Analysis |
| //***************************************************************** |
| if (samplesPerPeriod == 0) |
| { |
| Serial.println("Hmm..... I am not sure. Are you trying to trick me?"); |
| } |
| else |
| { |
| //prepare the weighting function |
| total = 0; |
| if (signalFrequency !=0) |
| { |
| total = 1; |
| } |
| if(signalFrequency2 !=0) |
| { |
| total = total + 2; |
| } |
| if (signalFrequency3 !=0) |
| { |
| total = total + 3; |
| } |
|
| //calculate the frequency using the weighting function |
| signalFrequencyGuess = ((1/total) * signalFrequency) + ((2/total) * signalFrequency2) + ((3/total) * signalFrequency3); //find a weighted frequency |
| Serial.print("The note you played is approximately "); |
| Serial.print(signalFrequencyGuess); //Print the frequency guess. |
| Serial.println(" Hz."); |
|
| //find octave range based on the guess |
| octaveRange=3; |
| while (!(signalFrequencyGuess >= storedNoteFreq[0]-7 && signalFrequencyGuess <= storedNoteFreq[11]+7 )) |
| { |
| for(i = 0; i < 12; i++) |
| { |
| storedNoteFreq[i] = 2 * storedNoteFreq[i]; |
| } |
| octaveRange++; |
| } |
|
| //Find the closest note |
| minValue = 10000000; |
| noteLocation = 0; |
| for (i = 0; i < 12; i++) |
| { |
| if(minValue> abs(signalFrequencyGuess-storedNoteFreq[i])) |
| { |
| minValue = abs(signalFrequencyGuess-storedNoteFreq[i]); |
| noteLocation = i; |
| } |
| } |
|
| //Print the note |
| Serial.print("I think you played "); |
| if(noteLocation==0) |
| { |
| Serial.print("C"); |
| } |
| else if(noteLocation==1) |
| { |
| Serial.print("C#"); |
| } |
| else if(noteLocation==2) |
| { |
| Serial.print("D"); |
| } |
| else if(noteLocation==3) |
| { |
| Serial.print("D#"); |
| } |
| else if(noteLocation==4) |
| { |
| Serial.print("E"); |
| } |
| else if(noteLocation==5) |
| { |
| Serial.print("F"); |
| } |
| else if(noteLocation==6) |
| { |
| Serial.print("F#"); |
| } |
| else if(noteLocation==7) |
| { |
| Serial.print("G"); |
| } |
| else if(noteLocation==8) |
| { |
| Serial.print("G#"); |
| } |
| else if(noteLocation==9) |
| { |
| Serial.print("A"); |
| } |
| else if(noteLocation==10) |
| { |
| Serial.print("A#"); |
| } |
| else if(noteLocation==11) |
| { |
| Serial.print("B"); |
| } |
| Serial.println(octaveRange); |
| } |
| //***************************************************************** |
| //Stop here. Hit reset button on Arduino to restart |
| //***************************************************************** |
| while (1); |
| } |