# Payoff: Read the Sweep Off the Picture

*Task 5 of 5 · [Spectrograms](https://gpu.rocks/learn/spectrograms-9ecd2295.md) · GPU.js Learn*

A spectrogram is not only for looking at. This signal is a **chirp** —
a tone sliding steadily upward — and everything needed to characterise it is already in the
picture. For each frame, find the bin carrying the most energy. The sequence of winners is
the chirp's **ridge**, and the slope of that ridge is the sweep rate, in hertz
per second, recovered from nothing but the image.

The GPU shape: one thread per frame, each scanning its own column of 128 bins for the
largest value and returning **the index**, not the value. An argmax along an
axis is a reduction — Reductions climbs the general log₂ ladder for the case where the axis
is huge — but with 128 values per column, one thread walking them is the right call. What
matters more is what it does not need: no atomics, no shared memory, no thread writing to a
cell it does not own. Every thread pulls its own column and writes its own answer.

Where would this come from on a real page? A microphone, and the wiring is short:

```js
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audio = new AudioContext({ sampleRate: 48000 });
await audio.audioWorklet.addModule('tap.js');   // posts Float32Array frames
const tap = new AudioWorkletNode(audio, 'tap');
audio.createMediaStreamSource(stream).connect(tap);

const gpu = new GPU();
const spectrogram = gpu.createKernel(/* the kernel you already wrote */);

tap.port.onmessage = event => {
  const spec = spectrogram(event.data);   // event.data is the ring buffer
  paint(spec, peakOf(spec));
};
```

**That code cannot run here, and this course is not going to pretend otherwise.**
Your code executes inside a Web Worker — which is what lets a runaway kernel be killed
instead of freezing the page — and a Worker has no `AudioContext`, no
`OfflineAudioContext` and no `navigator.mediaDevices`. There is no
microphone to reach and nothing to play back through. The buffers this module builds are the
stand-in, and they buy something a microphone never could: every assertion below is exact.
Every kernel you have written works unchanged the day you paste it onto a page with the
wiring above.

## Goal

**Goal:** find the ridge with a kernel, then turn its slope into a sweep
rate in hertz per second and log it.

## Requirements

- `output: [FRAMES]` — one thread per frame, scanning all `this.constants.bins` bins
- Return the *bin index* of the largest magnitude in that column, not the magnitude itself
- Convert the slope to hertz per second and `console.log` it — bins are `SR / WIN` Hz apart, frames `HOP / SR` seconds apart

## Hint 1 — tracking an argmax

Two locals: the best value seen so far, and the index it came from. Update both
together, and return the *index*. Returning `best` instead of
`bestBin` is the classic slip, and it looks plausible right up until the
numbers turn out to be 0.2 rather than 40.

## Hint 2 — the kernel

```js
let best = 0;
let bestBin = 0;
for (let b = 0; b < this.constants.bins; b++) {
  const v = spec[b][this.thread.x];
  if (v > best) {
    best = v;
    bestBin = b;
  }
}
return bestBin;
```

Note `spec[b][this.thread.x]`: the column belongs to this thread, and
`b` walks down it.

## Hint 3 — the arithmetic

```js
const binsPerFrame = (line[FRAMES - 1] - line[0]) / (FRAMES - 1);
const hzPerSecond = (binsPerFrame * (SR / WIN)) / (HOP / SR);
```

Bins are `4096 / 256` = 16 Hz apart and frames are `64 / 4096` = 15.625 ms
apart, so one bin per frame is 1,024 Hz per second — which is exactly what went in.

## Same idea elsewhere

Ridge tracking is how a radar measures range rate, how a guitar tuner follows a bent
note, how a birdsong classifier segments syllables and how a bearing monitor spots a
harmonic drifting with load. The primitive is `argmax` along an axis:
`torch.argmax(dim=…)`, `cub::DeviceSegmentedReduce` with
`ArgMax`, `simd_max` plus a ballot in Metal. The awkward part on every
platform is the same one you just stepped around — a maximum is easy, but carrying the
*index* of the maximum through a parallel reduction means reducing pairs, not
numbers.

## Starter code

```js
// The spectrogram is already built. What is new is reading it.
const gpu = new GPU({ mode });

const SR = 4096;   // samples per second
const WIN = 256;
const HOP = 64;
const BINS = 128;
const FRAMES = Math.floor((signal.length - WIN) / HOP) + 1;

const spectrogram = gpu.createKernel(function (signal) {
  const frame = this.thread.x;
  const bin = this.thread.y;
  const start = frame * this.constants.hop;
  let re = 0;
  let im = 0;
  for (let t = 0; t < this.constants.win; t++) {
    const w = 0.5 - 0.5 * Math.cos((2 * Math.PI * t) / (this.constants.win - 1));
    const s = signal[start + t] * w;
    const angle = (-2 * Math.PI * bin * t) / this.constants.win;
    re += s * Math.cos(angle);
    im += s * Math.sin(angle);
  }
  return Math.sqrt(re * re + im * im) / this.constants.win;
}, {
  output: [FRAMES, BINS],
  constants: { win: WIN, hop: HOP },
});

const ridge = gpu.createKernel(function (spec) {
  // TODO: scan this frame's column of this.constants.bins bins and return
  // the INDEX of the loudest one.
  return 0;
}, {
  output: [FRAMES],
  constants: { bins: BINS },
});

const spec = spectrogram(signal);
const line = ridge(spec);
console.log('ridge bins:', Array.from(line).join(' '));

// TODO: turn the slope into hertz per second and log it.
// const binsPerFrame = (line[FRAMES - 1] - line[0]) / (FRAMES - 1);
// const hzPerSecond = (binsPerFrame * (SR / WIN)) / (HOP / SR);
// console.log('sweep rate:', hzPerSecond, 'Hz per second');
```

---

Interactive version: https://gpu.rocks/learn/spectrograms-9ecd2295/5

[Previous task](https://gpu.rocks/learn/spectrograms-9ecd2295/4.md)
