Skip to content
Vince.
All case studies

Web Audio · DSP

Tuner

Real-time pitch detection from the microphone

A chromatic instrument tuner that listens through the microphone and reports the note and its deviation in cents. The pitch detection is a from-scratch implementation of the YIN algorithm — no library — and it is accurate to under a cent on synthetic signals.

  • Web Audio API
  • TypeScript
  • Canvas

In short

  • YIN autocorrelation with cumulative mean normalisation, implemented from the 2002 paper
  • Parabolic interpolation recovers sub-sample periods: 4 cents of rounding error becomes under 0.5
  • Correctly tracks a fundamental that is quieter than its harmonics, and one that is missing entirely
  • 33 unit tests drive the detector with synthetic tones in Node — no browser, no microphone, no flake

Why not just take the biggest FFT peak

The obvious approach to pitch detection is to run an FFT and find the tallest peak. It works on a flute and falls apart on a guitar. On a plucked low string the second and third harmonics are routinely louder than the fundamental, so the tallest peak sits an octave or a twelfth above the note actually being played — and a tuner that jumps between E2 and E3 while you turn the peg is useless.

The deeper problem is that pitch is not the same thing as spectral energy. A note whose fundamental has been filtered out entirely is still heard at that pitch; this is the missing fundamental, and it is why a small phone speaker can play a bass line it cannot physically reproduce. Any detector that reasons about where the energy is will get that case wrong.

How YIN works

YIN works in the time domain and asks a different question: at what delay does the signal most resemble itself? It computes the squared difference between the signal and a copy of itself shifted by a lag, for every lag in the search range. A periodic signal is nearly identical to itself one period later, so that curve dips sharply at the period.

The step that makes it reliable is the cumulative mean normalisation. The raw difference function always has its global minimum at zero lag and drifts downward at long lags, which biases a naive search toward wrong answers. Dividing by the running mean of the curve so far flattens that trend, and turns a single absolute threshold into a decision that means the same thing across the whole lag range.

The last refinement is arithmetic rather than signal processing. At 44.1 kHz the period of A4 is 100.2 samples, so picking the nearest whole sample reports 441 Hz — about 4 cents sharp, which is visible on a tuner display. Fitting a parabola through the minimum and its two neighbours recovers the fractional part and takes the error below half a cent.

Making it behave on real input

A correct detector still makes a bad tuner if you wire it straight to the display. Microphone input between notes is noise, and noise produces a stream of unrelated estimates that make the needle twitch continuously.

Two things fix it. Readings below a clarity floor are discarded outright, so silence cannot move the display. Accepted readings are then blended in the log-frequency domain, where a fixed smoothing weight corresponds to a fixed number of cents regardless of register — the same responsiveness at E2 as at E5. A jump of more than a fifth re-seeds the filter instead of gliding, because a leap that large is a new note or an octave error, never a slide.

Building the detector to run on a plain array of samples rather than on an AudioContext is what makes all of this testable. The suite feeds it synthetic tones, harmonic stacks with a deliberately weak fundamental, added noise, and a DC offset, then asserts the answer in cents — in Node, in about a fifth of a second.

What I would do differently at scale

The analysis runs on the main thread inside a requestAnimationFrame loop. That is fine for one 2048-sample buffer per frame, and it keeps the demo simple to read, but the correct home for it is an AudioWorklet: guaranteed cadence on the audio thread, immune to layout and garbage collection.

The difference function is also the naive O(n·τ) double loop. YIN can be computed via autocorrelation using two FFTs, which matters if the window grows or several detectors run at once.

Source

  • src/lib/audio/yin.ts
  • src/lib/audio/yin.test.ts