A small command-line ukulele/guitar tuner written in Python.

# Project

When I was on sabbatical in 2013-2014 , a former student of mine sent me a 3D-printed banjo ukulele he had invented. I was new to the ukulele, but it got me hooked really quickly. There’s something lovely and minimal about a four-stringed instrument – compared to the guitar, it’s a lot quicker to figure out and memorize chords. In the summer of 2014, I decided to augment my collection with a wooden concert ukulele, the Kala KA-SCG, and I’ve been strumming enthusiastically, if amateurishly, ever since.

Long ago, I downloaded a free tuner app for my phone, but I actually prefer reading chords and tabs from my laptop because a bunch of the websites I use throw up annoying ads for their mobile versions. Having a tuner on my computer would mean one less device to take into the music room. I know there’s lots of free tuner apps for Mac and PC too, but I was curious how long it would take to write my own. The answer: less than an afternoon.

# Strategy

My general strategy was to obtain a Fast Fourier Transform (FFT) of the audio data to decompose a short snippet of audio into its constituent frequencies, and pick off the one with the largest magnitude. The code also attends to a couple of extra details:

• Because this is a short-time Fourier transform, I apply the Hann window function to the signal before getting the FFT in order to avoid aliasing.

• I overlap frames of audio data in order to get a moving average of the frequency spectrum. This is a compromise between small-but-fast FFT buffers (which provide poor frequency resolution), and lengthy waits between updates.

I found this example code to be a helpful resource when I was getting started. It was probably the first thing that popped up when I googled “Python audio FFT” or something similar. After playing with their code for a few minutes, I soon decided to switch away from the threaded asynchronous model to the simpler blocking I/O model, for which the PyAudio docs came in handy. Finally, this page helpfully provided formulas to convert between frequency, MIDI note number, and note name.

# Results

The program works every bit as well as my phone app does, and I now use it whenever I get started playing. It’s a great testament to the state of Python development in 2016 that you can do practical, real-world audio processing in under 40 lines of code.

This is also available on my github page, but here’s the program since it’s short enough to fit here too:

#! /usr/bin/env python
######################################################################
# tuner.py - a minimal command-line guitar/ukulele tuner in Python.
# Requires numpy and pyaudio.
######################################################################
# Author:  Matt Zucker
# Date:    July 2016
######################################################################

import numpy as np
import pyaudio

######################################################################
# Feel free to play with these numbers. Might want to change NOTE_MIN
# and NOTE_MAX especially for guitar/bass. Probably want to keep
# FRAME_SIZE and FRAMES_PER_FFT to be powers of two.

NOTE_MIN = 60       # C4
NOTE_MAX = 69       # A4
FSAMP = 22050       # Sampling frequency in Hz
FRAME_SIZE = 2048   # How many samples per frame?
FRAMES_PER_FFT = 16 # FFT takes average across how many frames?

######################################################################
# Derived quantities from constants above. Note that as
# SAMPLES_PER_FFT goes up, the frequency step size decreases (so
# resolution increases); however, it will incur more delay to process
# new sounds.

SAMPLES_PER_FFT = FRAME_SIZE*FRAMES_PER_FFT
FREQ_STEP = float(FSAMP)/SAMPLES_PER_FFT

######################################################################
# For printing out notes

NOTE_NAMES = 'C C# D D# E F F# G G# A A# B'.split()

######################################################################
# These three functions are based upon this very useful webpage:
# https://newt.phys.unsw.edu.au/jw/notes.html

def freq_to_number(f): return 69 + 12*np.log2(f/440.0)
def number_to_freq(n): return 440 * 2.0**((n-69)/12.0)
def note_name(n): return NOTE_NAMES[n % 12] + str(n/12 - 1)

######################################################################
# Ok, ready to go now.

# Get min/max index within FFT of notes we care about.
# See docs for numpy.rfftfreq()
def note_to_fftbin(n): return number_to_freq(n)/FREQ_STEP
imin = max(0, int(np.floor(note_to_fftbin(NOTE_MIN-1))))
imax = min(SAMPLES_PER_FFT, int(np.ceil(note_to_fftbin(NOTE_MAX+1))))

# Allocate space to run an FFT.
buf = np.zeros(SAMPLES_PER_FFT, dtype=np.float32)
num_frames = 0

# Initialize audio
stream = pyaudio.PyAudio().open(format=pyaudio.paInt16,
channels=1,
rate=FSAMP,
input=True,
frames_per_buffer=FRAME_SIZE)

stream.start_stream()

# Create Hanning window function
window = 0.5 * (1 - np.cos(np.linspace(0, 2*np.pi, SAMPLES_PER_FFT, False)))

# Print initial text
print 'sampling at', FSAMP, 'Hz with max resolution of', FREQ_STEP, 'Hz'
print

# As long as we are getting data:
while stream.is_active():

# Shift the buffer down and new data in
buf[:-FRAME_SIZE] = buf[FRAME_SIZE:]

# Run the FFT on the windowed buffer
fft = np.fft.rfft(buf * window)

# Get frequency of maximum response in range
freq = (np.abs(fft[imin:imax]).argmax() + imin) * FREQ_STEP

# Get note number and nearest note
n = freq_to_number(freq)
n0 = int(round(n))

# Console output once we have a full buffer
num_frames += 1

if num_frames >= FRAMES_PER_FFT:
print 'freq: {:7.2f} Hz     note: {:>3s} {:+.2f}'.format(
freq, note_name(n0), n-n0)