# Three Ways to Get It Wrong

*Task 3 of 5 · [Autocorrelation & Pitch](https://gpu.rocks/learn/autocorrelation-b159433f.md) · GPU.js Learn*

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.

## Figures

- **every trap in one picture: the free peak, the right peak, and the tall wrong one**

## Goal

**Goal:** return the *normalised* score at every lag, then report
the first peak that reaches 0.8 — not the biggest one.

## Requirements

- One guarded loop, three accumulators: the dot product, the energy of `signal[i]`, and the energy of `signal[i + lag]`
- Return `dot / Math.sqrt(headEnergy * tailEnergy)`, so lag 0 comes back as exactly 1
- Walk lags from 8 upward and take the FIRST local peak whose score is at least `0.8`
- Log both: `console.log('period:', lag, 'samples')` and `console.log('pitch:', sampleRate / lag, 'Hz')`

## Hint 1 — three accumulators, one pass

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

```js
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:

```js
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.

## Starter code

```js
// Same shape as task 1, three accumulators instead of one.
const gpu = new GPU({ mode });

const correlate = gpu.createKernel(function (signal) {
  const lag = this.thread.x;
  let dot = 0;
  let head = 0;
  let tail = 0;
  for (let i = 0; i < this.constants.n; i++) {
    if (i + lag < this.constants.n) {
      dot += signal[i] * signal[i + lag];
      // TODO: accumulate the two energies as well —
      // head gets signal[i] squared, tail gets signal[i + lag] squared.
    }
  }
  // TODO: return the normalised score, dot / sqrt(head * tail)
  return dot;
}, {
  output: [256],
  constants: { n: 512 },
});

const rho = correlate(signal);
console.log('rho[0] (should be exactly 1):', rho[0]);

// TODO: walk from lag 8 and take the FIRST local peak that reaches 0.8.
// Then log:
//   console.log('period:', period, 'samples');
//   console.log('pitch:', sampleRate / period, 'Hz');
```

---

Interactive version: https://gpu.rocks/learn/autocorrelation-b159433f/3

[Previous task](https://gpu.rocks/learn/autocorrelation-b159433f/2.md) · [Next task](https://gpu.rocks/learn/autocorrelation-b159433f/4.md)
