Task 3 of 5

Three Ways to Get It Wrong

Task 1 worked. It worked on a clean synthetic tone, with a search range chosen to dodge two traps you were never told about, and it would fall over on the signal below. Here is the full charge sheet.

One: lag 0 always wins. Every signal correlates perfectly with an unshifted copy of itself, so r[0] is the maximum of every autocorrelation ever computed. Worse, it is not a spike but a shoulder — nearby lags are nearly as good, because a smooth signal barely changes in one sample. That is why the search starts at 8.

Two: long lags are handicapped. The overlap shrinks as the lag grows — lag 40 sums 472 products, lag 200 only 312 — so raw sums fall away with lag whether or not the signal does. You cannot compare two lags until you divide that out. The honest fix is the one Template Matching already made for a different reason: normalise. Divide by the geometric mean of the two overlapping slices' energies and every lag comes back on the same −1…+1 scale, with lag 0 exactly 1.

Three: and now the octave error. Normalise honestly and a new door opens. A signal that repeats every 40 samples also repeats every 80, and 120, and 160 — every multiple is a real peak, and they all score close to 1. The voice below has one common quirk: every other period is slightly quieter (creaky voice does this, and so do plenty of instruments). The waveform still repeats every 40 samples and you would still hear 204.8 Hz — but a copy shifted 80 samples matches fractionally better, so the tallest peak is at 80 and the naive answer is an octave too low. The fix is not a better maximum. It is to stop taking the maximum: walk up from the shortest lag and take the first peak that clears a threshold.

every trap in one picture: the free peak, the right peak, and the tall wrong one
Goal: return the normalised score at every lag, then report the first peak that reaches 0.8 — not the biggest one.

Requirements

Hint 1 — three accumulators, one pass

Read each sample once and use it three times — the same fusion Reductions makes for mean and RMS:

const a = signal[i];
const b = signal[i + lag];
dot += a * b;
head += a * a;
tail += b * b;
Hint 2 — what counts as a peak

A local peak is a value at least as large as both its neighbours, and here it also has to clear the threshold:

for (let lag = 8; lag < 255; lag++) {
  const peak = rho[lag] > rho[lag - 1]
    && rho[lag] >= rho[lag + 1];
  if (peak && rho[lag] >= 0.8) {
    period = lag;
    break;
  }
}

The break is the whole idea. Drop it and you are back to taking the largest.

Hint 3 — how to tell you got it wrong

If your answer is 80 samples / 102.4 Hz, the code is finding the tallest peak rather than the first one. If it is 40 samples / 204.8 Hz, you have it — and you have also written a pitch detector that beats the obvious one.

Same idea elsewhere

Normalising a similarity score before comparing it is a rule that outlives GPUs: it is why cv::matchTemplate ships TM_CCOEFF_NORMED alongside the raw version, why cosine similarity beats a dot product for embeddings, and why every production pitch tracker (YIN, and the "cumulative mean normalised difference" at its heart) is a normalisation trick bolted onto exactly the kernel you just wrote.

All tasks in Autocorrelation & Pitch

  1. Compare a Signal With Itself
  2. From Lag to Pitch
  3. Three Ways to Get It Wrong
  4. The Note That Isn't There
  5. Payoff: The Same Curve, via the FFT

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.