# Above Nyquist

*Task 2 of 5 · [Sampling & Aliasing](https://gpu.rocks/learn/sampling-and-aliasing-ad14836c.md) · GPU.js Learn*

Here is the part that surprises people. Sample a 200 Hz tone at 256 Hz and you do not
get a bad 200 Hz tone, or a warning, or noise. You get a **perfectly good tone at a
different frequency** — and there is no way to tell from the samples that anything went
wrong, because the samples of the two are *the same numbers*.

The arithmetic is one line. Sampling at `fs` adds a whole turn to the phase every
time `f` moves by `fs`, and a whole turn is invisible. So every frequency
in the family `f, f ± fs, f ± 2fs, …` produces an identical sample sequence, and the
one you actually perceive is the member nearest zero — fold `f` into the
**base band** −fs/2 … +fs/2:

```js
alias = f - Math.round(f / fs) * fs
//  300 Hz at fs = 256 →  +44 Hz     (the picture above)
//  200 Hz at fs = 256 →  −56 Hz     (what you are about to sample)
```

Round to the *nearest* multiple, and keep the sign. A negative alias is not a mistake:
a sine at −56 Hz is a sine at 56 Hz mirrored in time, and it is genuinely what you hear and see.
(It is also why a filmed wagon wheel turns backwards, which task 5 comes back to.) Writing
`fs - f` instead of `f - fs` flips that mirror — and goes negative for the
wrong reason the moment `f` exceeds `fs`.

fs/2 is **Nyquist**, the highest frequency a rate can carry. Exactly *at*
it the rule is degenerate, which is why the safe test is `f >= fs / 2` and not
`f > fs / 2`: a 128 Hz sine sampled at 256 Hz gives you sin(φ), −sin(φ), sin(φ), …
— two samples per period, forever, so you can never recover both amplitude and phase. And if φ
happens to be 0 you get 256 zeros and the tone disappears completely. Watch what the fold does
to the 128 Hz entry in `freqs`.

The second kernel writes **two planes**: `output: [256, 2]` is indexed
`result[plane][i]`, the shape this track carries a complex signal in (plane 0 real,
plane 1 imaginary) once the DFT arrives. Here the planes are simply two ordinary real signals,
side by side, so you can compare them sample for sample.

## Figures

- **two tones, one set of samples — nothing downstream can tell them apart**

## Goal

**Goal:** fold six frequencies into the base band with one kernel, then use a
second kernel to sample `200 Hz` and its alias into two planes and show they are the
same numbers.

## Requirements

- The fold kernel returns `f - Math.round(f / fs) * fs` — one thread per frequency, sign kept
- The sample kernel uses `output: [256, 2]` and picks its frequency with `this.thread.y`
- Feed the folded 200 Hz alias into the second kernel and `console.log` it

## Hint 1 — which multiple to subtract

`f / fs` says how many whole sample rates fit inside `f`.
Rounding it to the nearest integer (not flooring it) is what lands the answer inside
−fs/2 … +fs/2 rather than 0 … fs.

## Hint 2 — two planes, one kernel

With `output: [256, 2]`, `this.thread.y` is 0 or 1 and
`this.thread.x` is the sample index. Pick the frequency with the plane index and
everything else is task 1 again:

```js
const f = pair[this.thread.y];
const t = this.thread.x / this.constants.sampleRate;
return Math.sin(2 * Math.PI * f * t + this.constants.phase);
```

## Same idea elsewhere

Every sampled system on every platform has this hazard and the same fix: filter above
Nyquist *before* you sample, never after. Graphics calls it the same thing —
a texture minified without mipmaps aliases, and MSAA/anisotropic filtering are anti-aliasing in
the literal signal-processing sense. GPU texture units implement that filter in hardware
precisely because doing it afterwards is impossible.

## Starter code

```js
// Two kernels: fold the frequencies, then sample two of them side by side.
const gpu = new GPU({ mode });

// One thread per frequency — pure arithmetic, no data at all.
const fold = gpu.createKernel(function (freqs) {
  const f = freqs[this.thread.x];
  // TODO: subtract the NEAREST whole multiple of the sample rate, keeping the sign.
  return f;
}, {
  output: [6],
  constants: { sampleRate: 256 },
});

// output: [256, 2] → result[plane][i]. Plane 0 samples pair[0], plane 1 pair[1].
const sample = gpu.createKernel(function (pair) {
  // TODO: choose this plane's frequency with this.thread.y
  const f = pair[0];
  const t = this.thread.x / this.constants.sampleRate;
  return Math.sin(2 * Math.PI * f * t + this.constants.phase);
}, {
  output: [256, 2],
  constants: { sampleRate: 256, phase: 0.9 },
});

const folded = fold(freqs);
console.log('folded:', Array.from(folded));

const alias = folded[3];   // freqs[3] is the 200 Hz tone
console.log('200 Hz sampled at 256 Hz comes back as', alias, 'Hz');

const rows = sample([200, alias]);
console.log('plane 0:', rows[0][0], rows[0][1], rows[0][2], rows[0][3]);
console.log('plane 1:', rows[1][0], rows[1][1], rows[1][2], rows[1][3]);
```

---

Interactive version: https://gpu.rocks/learn/sampling-and-aliasing-ad14836c/2

[Previous task](https://gpu.rocks/learn/sampling-and-aliasing-ad14836c/1.md) · [Next task](https://gpu.rocks/learn/sampling-and-aliasing-ad14836c/3.md)
