Instrument tuners seem simple—show a note and how sharp or flat it is. But accurate pitch detection in real-time is surprisingly tricky. Here's how I built one that actually works.
The Problem with FFT
The obvious approach is using FFT (Fast Fourier Transform) to find the dominant frequency. Most audio visualizers use this—the Web Audio API even has a built-in getFrequencyData() method for it. But for tuning instruments, FFT has a fundamental problem: resolution.
FFT gives you frequency "bins" whose width depends on your sample rate and buffer size. At 44.1kHz with a 2048-sample buffer, each bin is about 21Hz wide. That's nowhere near the precision needed for tuning—the difference between A440 and A441 is musically significant, but they'd land in the same bin.
Autocorrelation
The better approach is autocorrelation—comparing a signal with time-shifted copies of itself to find the period of repetition. When a signal matches its shifted copy closely, you've found the period. Frequency is just 1/period.
function autoCorrelate(buffer, sampleRate) {
// Check if there's enough signal
let rms = 0
for (let i = 0; i < buffer.length; i++) {
rms += buffer[i] * buffer[i]
}
rms = Math.sqrt(rms / buffer.length)
if (rms < 0.01) return -1 // Too quiet
// Autocorrelation: find the period
const c = new Array(buffer.length).fill(0)
for (let i = 0; i < buffer.length; i++) {
for (let j = 0; j < buffer.length - i; j++) {
c[i] += buffer[j] * buffer[j + i]
}
}
// Find the first peak after the initial falloff
let d = 0
while (c[d] > c[d + 1]) d++
let maxval = -1, maxpos = -1
for (let i = d; i < buffer.length; i++) {
if (c[i] > maxval) {
maxval = c[i]
maxpos = i
}
}
return sampleRate / maxpos
}This gives us sub-Hz precision—more than enough for the ±50 cent display musicians expect.
Converting Frequency to Notes
Once we have a frequency, converting to a musical note is straightforward math. Western music uses 12-tone equal temperament, where each semitone is exactly 2^(1/12) times the previous one. Working backwards from A4 = 440Hz:
const A4 = 440
const C0 = A4 * Math.pow(2, -4.75) // ~16.35 Hz
function noteFromPitch(frequency) {
const noteNum = 12 * Math.log2(frequency / C0)
const noteIndex = Math.round(noteNum) % 12
const octave = Math.floor(Math.round(noteNum) / 12)
const cents = Math.round((noteNum - Math.round(noteNum)) * 100)
return {
note: NOTES[noteIndex],
octave,
cents
}
}The cents value tells us how many hundredths of a semitone we're off from the nearest note. Musicians typically consider ±5 cents to be "in tune."
Smoothing the Jitter
Raw pitch detection jitters. Even a perfectly steady tone will show slight variations frame-to-frame due to noise and algorithm limitations. The fix is simple: maintain a history of recent readings and average them.
// Keep last N frequency readings
frequencyHistory.push(detectedFreq)
if (frequencyHistory.length > smoothingAmount) {
frequencyHistory.shift()
}
const avgFreq = frequencyHistory.reduce((a, b) => a + b, 0)
/ frequencyHistory.lengthI exposed this as a "Smoothing" knob. Lower values give faster response (good for quick tuning), higher values give steadier readings (better for checking sustained notes).
The Reference Pitch
Not everyone tunes to A440. Orchestras often tune higher (A442-A444) for a brighter sound. Baroque ensembles tune lower (A415 or A432). The A4 Reference knob lets you adjust this, and the "Play A4" button gives you an audible reference at whatever frequency you've set.
The Hardware Feel
Like the other projects in this series, I wanted the tuner to feel like a physical device. The same draggable knobs, the enclosure with subtle borders, the minimal typography. A tuner you'd actually want on your music stand.
The cents display uses a visual indicator that moves left (flat) or right (sharp) of center—the same UX pattern used in hardware tuners for decades. Some things don't need reinventing.
Try It
The Chromatic Tuner is live. Grab your guitar, click Start, and tune up. Works in any browser with microphone access.