Introduction: Raspberry Pi Guitar Tuner

The purpose of this project is to create a guitar tuner by using a Raspberry pi. Why not just buy a guitar tuner? Because buying a guitar tuner does not provide any functional perspective of differences between Frequency and Pitch. The objective of this project is to learn how to detect specific frequencies from each guitar string, and allows the user to interact with the tuner through the software that we have written in Python. The program will show the note that is being played by the user, including whether the note is sharp or flat. Like a store-bought guitar tuner, the user is capable of playing the guitar constantly to check whether their guitar is in tune.

The biggest hurdle preventing hobbyists from building their own has been understanding the difference between the Frequency and Pitch, and how to get software to recognize that difference too. With this aspect we will by using a microphone to measure the physical aspect of sound visa vi the frequency. However in musical terms there is also an aspect of pitch, which is generally a perceived aspect of a sound. Most often, the pitch of a sound played at a certain frequency will be adjusted in order for the pitch to be double, the same as, or half of the reference sound's intended pitch. This is important because most of the time, pitch is proportional to the frequency of a tone, however if the loudness of a tone increases beyond a certain point, then the pitch detection will need to by adjusted to compensate for the higher intensity of the sound.

If you look at the waveform above, you can see a spectrum of the frequencies produced by strumming a string on the guitar. The note pictured was E2, which is 82 Hz. However, the most intense and "loudest" frequency in the sound is around 240 Hz, around three times the fundamental frequency. This is a harmonic of the original frequency. So, a straightforward detection of the frequency won't work, as it will detect the most intense frequency, 240 Hz. To get to the fundamental frequency (the true frequency of the note we played) we have to get a little creative.

Step 1: Parts: What You'll Need

There are not many tools needed for this project. It is as simple as it looks. This instructable assumes you have a Pi that is already set up (Wifi/OS/etc.) AND IS CONNECTED TO A DISPLAY. The display can be an SSH connection over a network OR an HDMI cable connected to a monitor/TV.

This project used a Raspberry Pi 2 using the Jessie OS with Pixel connected through HDMI to a Dell Monitor.

Parts List:

1) 2 bread boards

2) 4 resistors (1k ohms)

3) Raspberry Pi 2 or higher

4) RGB LED

5) CanaKit Ribbon cable and GPIO interface

6) Several connection wires

7) USB Microphone

If you would like to try the 7-Segment addition:

8) SN74LS47N chip

9) 7 resistors (470 ohms)

10) A 9V battery connector

11) 9 volt battery

12) seven segments display

Be warned that the 7-segment part of this instructable is incomplete, and was an experiment in Raspberry Pi GPIO.

Step 2: Building the Circuit

First, we need to build the LED circuit. Notice that the LED that we used have 4 pins in total. Each LED requires one resistor 1k ohms resistor to each pin of the LEDs. Also notice that one out of four pins of the LED are longer than the other three, grounding the longest to the Ribbon cable to the GPIO of the Raspberry Pi by using the colored wires to connect between pin 26 on the GPIO ports and that longest LED pin. The other three pins of the LED should be connected to pins 5, 6, and 16 accordingly as following: “red wire” to pin 5, “blue wire” to pin 6, and “green wire” to pin 16.

Step 3: Code

To get Python running on your Raspberry Pi, you'll need to connect to your Pi either with an HDMI cord to see its display, or use SSH to open a Terminal on the PI.

Once in a Terminal or Command Line environment on the Pi, execute the following lines of code:

  • sudo apt-get install python-numpy
  • sudo apt-get install python-opencv
  • sudo apt-get install python-scipy
  • sudo apt-get install python-pyaudio python3-pyaudio

These commands will install the Numpy and Scipy environments on your Pi which allow you to edit and deploy python code.

All the code you will need to run this project can be found on the project GitHub:

https://github.com/katrinamo/RPiPitch

It contains GPIO test files, which are used to make sure your Raspberry Pi GPIO is working along with a program to test the matlab plotting function. The matlab plotting was mainly used for debugging and developing the pitch detection. It is optional to install. You really only need the packages above to make it work.

The code can also be copy and pasted below:

#THIS CODE IS HEAVILY BASED ON: <br># <a href="https://benchodroff.com/2017/02/18/using-a-raspberry-pi-with-a-microphone-to-hear-an-audio-alarm-using-fft-in-python/" rel="nofollow">  https://benchodroff.com/2017/02/18/using-a-raspbe...>
#/usr/bin/env python
import pyaudio
from numpy import zeros,linspace,short,fromstring,hstack,transpose,log2, log
from scipy import fft, signal
from time import sleep
from scipy.signal import hamming, convolve
import matplotlib.pyplot as plt
import RPi.GPIO as GPIO
import sys
#from findfundfreq import *
#Volume Sensitivity, 0.05: Extremely Sensitive, may give false alarms
#             0.1: Probably Ideal volume
#             1: Poorly sensitive, will only go off for relatively loud
SENSITIVITY= 1.0
#Bandwidth for detection (i.e., detect frequencies within this margin of error of the TONE)
BANDWIDTH = 1
# Show the most intense frequency detected (useful for configuration)
frequencyoutput=True #notes in cents
Note_E = 5
Note_A = 0
Note_D = 7
Note_G = 2
Note_B = 10
Note_E4= 5 #holds previous frequency
prevFreq = 0
z1 = 10
z2 = 0
z0 = 0
MIN_FREQUENCY = 60
MAX_FREQUENCY = 500
#Max & Min cent value we care about
MAX_CENT = 11
MIN_CENT = 0
RELATIVE_FREQ = 440.0
if len(sys.argv) > 1:
	if (sys.argv[1] >= 415.0 and sys.argv[1] <= 445.0):
		RELATIVE_FREQ = sys.argv[1]#GPIO set up for the Red Green and Blue colors
GPIO.setmode(GPIO.BCM)
GPIO.setup(5, GPIO.OUT)
GPIO.setup(6, GPIO.OUT)
GPIO.setup(13, GPIO.OUT) #Set up audio sampler - 
NUM_SAMPLES = 2048
SAMPLING_RATE = 48000
pa = pyaudio.PyAudio()
_stream = pa.open(format=pyaudio.paInt16,
                  channels=1, rate=SAMPLING_RATE,
                  input=True,
                  frames_per_buffer=NUM_SAMPLES)
print("Detecting Frequencies. Press CTRL-C to quit.")
go = 10
#while go > 0:
while True:
    while _stream.get_read_available()< NUM_SAMPLES: sleep(0.01)
    audio_data  = fromstring(_stream.read(
         _stream.get_read_available()), dtype=short)[-NUM_SAMPLES:]
    # Each data point is a signed 16 bit number, so we can normalize by dividing 32*1024
    normalized_data = audio_data / 32768.0

    w = hamming(2048)  
    intensity = abs(w*fft(normalized_data))[:NUM_SAMPLES/2]  
if frequencyoutput:
        which = intensity[1:].argmax()+1
        # use quadratic interpolation around the max
        adjfreq = 1
	if which != len(intensity)-1:
            y0,y1,y2 = log(intensity[which-1:which+2:])
            x1 = (y2 - y0) * .5 / (2 * y1 - y2 - y0)
	    # find the frequency and output:w it
            thefreq = (which+x1)*SAMPLING_RATE/NUM_SAMPLES
	    if thefreq < MIN_FREQUENCY or thefreq > MAX_FREQUENCY:
            	adjfreq = -9999
   	    else:
           	 thefreq = which*SAMPLING_RATE/NUM_SAMPLES
	    	 if thefreq > MIN_FREQUENCY:
            		adjfreq = thefreq
  	    #adjfreq = 140    
	#print("Candidate Freq:  ", candidate_freq, which )
	#sys.stdout.write("Frequency: %d  \r" % (adjfreq))
	#sys.stdout.flush()
	#cents conversion
	if (adjfreq != -9999):
		#print "RAW FREQ:", adjfreq
		adjfreq = 1200 *log2(RELATIVE_FREQ/adjfreq)/100
		adjfreq = adjfreq % 12
		#print adjfreq
		#Case statements
		if abs(adjfreq - Note_E4 ) < 1:
			
			#In Tune E
			if abs(adjfreq - Note_E4) < 0.1  :
				print("You played an E!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH) #GREEN
			#Sharp E
			elif (adjfreq - Note_E4) <  0  :
				print("You are sharp E!")
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
				GPIO.output(13, GPIO.LOW) 
			#Flat E
			elif (adjfreq - Note_E4) > 0  :
				print("You are flat E!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW)
		elif abs(adjfreq - Note_E ) < 1:
				
			#In Tune E
			if abs(adjfreq - Note_E) < 0.1  :
				print("You played an E2!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH)
			#Sharp E
			elif (adjfreq - Note_E) < 0  :
				print("You are sharp E2!")
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
				GPIO.output(13, GPIO.LOW) 
			#Flat E
			elif (adjfreq - Note_E) > 0  :
				print("You are flat E2!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW)
		elif abs(adjfreq - Note_B ) < 1:
			
			#In Tune B
			if abs(adjfreq - Note_B) < 0.1  :
				print("You played a B!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH)
			#Sharp B
			elif (adjfreq - Note_B) < 0  :
				print("You are sharp (B)!")
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
				GPIO.output(13, GPIO.LOW) 
			#Flat B
			elif (adjfreq - Note_B)  >0  :
				print("You are flat (B)!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW)
		elif abs(adjfreq - Note_G ) < 1:
			
			#In Tune g
			if abs(adjfreq - Note_G) < 0.1  :
				print("You played a G!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH) #GREEN
			#Sharp G
			elif (adjfreq - Note_G) < 0  :
				print("You are sharp (G)!")
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
				GPIO.output(13, GPIO.LOW) 
			#Flat G
			elif (adjfreq - Note_G) > 0  :
				print("You are flat (G)!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW)
		
		elif abs(adjfreq - Note_D ) < 1:
	
			GPIO.output(5, GPIO.LOW)
			GPIO.output(6, GPIO.LOW)
			GPIO.output(13, GPIO.HIGH)
			
			#In Tune D
			if abs(adjfreq - Note_D) < 0.1  :
				print("You played a D!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH) #GREEN
			#Sharp D
			elif (adjfreq - Note_D) < 0  :
				print("You are sharp (D)!")
				GPIO.output(13, GPIO.LOW)
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
			#Flat D
			elif (adjfreq - Note_D) > 0  :
				print("You are flat (D)!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW) 
		elif abs(adjfreq - Note_A ) < 1:
			
			#In tune A
			if abs(adjfreq - Note_A) < 0.2  :
				print("You played an A!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH) #GREEN
			#Sharp A
			elif (adjfreq - Note_A) < 0  :
				print("You are sharp A!")
				GPIO.output(13, GPIO.LOW)
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
			#Flat A
			elif (adjfreq - Note_A)  > 0  :
				print("You are flat A!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW)
		elif abs(adjfreq - 12 ) < 1:
			
			#In tune A
			if abs(adjfreq - 12) < 0.2  :
				print("You played an A!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH) #GREEN
			#Sharp A
			elif (adjfreq - 12) < 0  :
				print("You are sharp A!")
				GPIO.output(13, GPIO.LOW)
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
			#Flat A
			elif (adjfreq - 12)  > 0  :
				print("You are flat A!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW)
  	#all off
	else:
		GPIO.output(5, GPIO.LOW) 
		GPIO.output(6, GPIO.LOW)
		GPIO.output(13, GPIO.LOW)
	#sys.stdout.write("Cent: %s  \r" % adjfreq)
	#sys.stdout.flush()
		
	sleep(0.01)</p>

Step 4: In Depth Explanation of Code

To run the code, simply run "python freqDetect.py" in the terminal after navigating to the directory your code is in. To stop the program, use Ctrl-C.

The code also has an option of changing the default A that you can tune to. When tuning, often musicians use the A of 440 Hz to tune. However, different styles of music may define a different tuning A. As a result, our program allows you to tune with respect to any A in the range of 415.0 Hz to 445.0 Hz. To specify a tuning A, run:

python freqDetect.py <some frequency of A>

If you aren't interested in the details of the code, feel free to skip to the next step! This is all the instruction you need to get it running.

If it is quiet in the room, you'll see a lot of debugging information along with "Detecting Frequencies. Press CTRL-C to quit." Then, sing or play an E, A, G, D, or B! You should see a stream of either "You played a __ !" or "You are __ !" The LED should turn Green for in tune, Red for sharp, and Blue for flat.

Frequency Detection:

while True:
    while _stream.get_read_available()< NUM_SAMPLES: sleep(0.01)
    audio_data  = fromstring(_stream.read(
         _stream.get_read_available()), dtype=short)[-NUM_SAMPLES:]
    # Each data point is a signed 16 bit number, so we can normalize by dividing 32*1024
    normalized_data = audio_data / 32768.0

    w = hamming(2048)  
    intensity = abs(w*fft(normalized_data))[:NUM_SAMPLES/2]  

if frequencyoutput:
        which = intensity[1:].argmax()+1
        # use quadratic interpolation around the max
        adjfreq = 1
	if which != len(intensity)-1:
            y0,y1,y2 = log(intensity[which-1:which+2:])
            x1 = (y2 - y0) * .5 / (2 * y1 - y2 - y0)
	    # find the frequency and output:w it
            thefreq = (which+x1)*SAMPLING_RATE/NUM_SAMPLES
	    if thefreq < MIN_FREQUENCY or thefreq > MAX_FREQUENCY:
            	adjfreq = -9999
   	    else:
           	 thefreq = which*SAMPLING_RATE/NUM_SAMPLES
	    	 if thefreq > MIN_FREQUENCY:
            		adjfreq = thefreq

This section is a modified version of that found in the original source.

This is the main part of frequency detection. Using PyAudio, we can sample a stream of audio that is being captured by the USB microphone. This is assigned to audio_data, which is a series of 16-bit numbers. However, 16-bit numbers are hard to work with, so it is normalized to floats (essentially, numbers with decimal places).

We then use the Fast Fourier Transform (fft)to transform the data to the sample space. This is then convolved with a Hamming window function to isolate the samples we have (2048).

Once we have the data in sample space, we look for the maximum value across all the samples and the use interpolation to calculate the frequency in Hertz. Then, this value is checked to see if it is above our threshold for notes we want to detect (50 Hz in this case). If it is, that value is passed along to the next section. Otherwise, we output an impossible value to act as a flag.

Cent Conversion:

#cents conversion
	if (adjfreq != -9999):
		adjfreq = 1200 *log2(RELATIVE_FREQ/adjfreq)/100
		adjfreq = adjfreq % 12<br>

If the value makes sense (isn't -9999 Hz) we convert the frequency to cents. Last section, we detected the frequency at the maximum point of the data. If you were thinking, "Wait a sec, doesn't that mean we might get a harmonic as our frequency which was specifically mentioned in step one?", you are absolutely right. That's where cents come in.

Cents are a unit of measurement that linearize the notes with respect to a tuning frequency, in our case whatever frequency of A we have chosen. Everything is then measured in some integer number of steps from the tuning frequency RELATIVE_FREQ. However, this doesn't totally solve our problem, as a low E2 would still be lost. The low E2 would be 29, whereas the high E4 is a 5.

To solve this, we take the modulo of this step distance with respect to an octave (12 notes). This gives us a pure note, regardless of it's distance from A. Since we have linearized the data, all the notes can be represented by the one integer. All octaves of E can be reduced to the distance, in half steps, away an E is from A (which happens to be 5: E, F, F#, G, G#, A). Thinking about the harmonics, they are all the fundamental times some integer. Thus, multiplying by some integer won't change the distance, as the remainder produced by the modulo is the same. Neat!

This cent conversion is then passed to the case statements.

Case Statements:

<p>#Case statements<br>		if abs(adjfreq - Note_E4 ) < 1:
			
			#In Tune E
			if abs(adjfreq - Note_E4) < 0.1  :
				print("You played an E!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.LOW)
				GPIO.output(13, GPIO.HIGH) #GREEN
			#Sharp E
			elif (adjfreq - Note_E4) <  0  :
				print("You are sharp E!")
				GPIO.output(5, GPIO.HIGH) #RED
				GPIO.output(6, GPIO.LOW) 
				GPIO.output(13, GPIO.LOW) 
			#Flat E
			elif (adjfreq - Note_E4) > 0  :
				print("You are flat E!")
				GPIO.output(5, GPIO.LOW)
				GPIO.output(6, GPIO.HIGH) #BLUE
				GPIO.output(13, GPIO.LOW)</p>

Now the cent value is compared to it's distance from the ideal cent value. If it's within 0.1 from the ideal note, we consider it in tune. The GPIO pin connected to the green pin on the LED is set to high.

Otherwise, if the cent value is less than the ideal, and thus closer to A, we say it is sharp. The GPIO pin connected to the Red pin on the LED is set to high and everything else is low so that we don't get a combination of RGB.

Finally, if the value is greater than the ideal distance, and farther from A, it is flat. The GPIO pin connected to the blue leg of the LED is set to high and the LED should turn blue.

If you want to be more or less forgiving about being in tune, adjust the 0.1 in each case statement to whatever sensitivity suits your needs.

Step 5: Reference Materials & Demos

Provided here are:

  • Reference Sheets and Pin layouts for the 14-pin Chip and 7 Segment Display.
  • Documents explaining how to determine to Frequency and Pitch algorithms in our code.
  • A link to the project log: https://storify.com/Fouriers_Cat

Also included are Demo Videos of our Code and Circuit Working.

Step 6: (FUTURE IMPLEMENTATION): Created the Circuit for Seven Segment Display

This section is an incomplete implementation. The issue we are having is that the Pi cannot provide the 5V to the 7-seg, and using a 9V battery involves using a voltage divider that adds complexity to the circuit. PROCEED AT YOUR OWN RISK AND REMEMBER, ALWAYS RESPECT CURRENT LIMITS.

Before you get into trying to plug your seven segment display into the circuit. You must make sure that every wire that is being fed into the seven segment display is first fed through a resistor (470 Ohms), so the seven segment display does not overload. Also you need a driver chip (SN74LS47N) so that your inputs from the code will be translated into hardware correctly. Both the chip and the seven segment display will be powered by the 5V source (battery) and will be grounded based on the attached document for the hardware. When you are plugging in the wires from the chip to the seven segment display make sure to notice the letters on the data sheet for the chip are matching up with the letters on the diagram of the seven segment display. The result of plugging these two devices together correctly should allow for the hardware to follow the output function of the circuit. Make sure to plug the input from the programmable logic into the driver chip or there will be no output from the display. The inputs should be plugged into the chip as the code dictates.