Task 2 of 5

Above Nyquist

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:

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.

two tones, one set of samples — nothing downstream can tell them apart
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

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:

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.

All tasks in Sampling & Aliasing

  1. One Thread, One Sample
  2. Above Nyquist
  3. What Happened Between the Samples?
  4. The Other Axis Is Discrete Too
  5. Payoff: Keep One Sample in Four

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