Task 5 of 5

Payoff: Keep One Sample in Four

Everything at once. Take the 256-sample second you built in task 1 — components at 5, 12 and 40 Hz — and throw away three samples in four. What you have left is 64 samples of the same second, which is a signal at 64 Hz. Nyquist has just dropped from 128 Hz to 32 Hz, and the 40 Hz component is now on the wrong side of it.

You can say exactly what happens, in advance, with the fold from task 2 — applied to the new rate:

5 Hz  → fold(5, 64)  =   5 Hz      survives
12 Hz  → fold(12, 64) =  12 Hz      survives
40 Hz  → fold(40, 64) = −24 Hz      comes back as 24 Hz, mirrored

Not "roughly", not "some artefacts": the decimated samples are identical, number for number, to a signal synthesised from 5, 12 and −24 Hz at 64 Hz. Prove it — plane 0 keeps every fourth sample, plane 1 synthesises the prediction, and the two planes agree.

This is the wagon wheel. A 24 frames-per-second camera pointed at a wheel turning 30 times a second folds it to 30 − 24 = 6 rev/s forwards; at 20 rev/s it folds to 20 − 24 = −4 rev/s, and the wheel appears to roll backwards. Same arithmetic, and the negative sign is doing real work.

And this is why every resampler filters before it decimates: once the 40 Hz component is sitting on top of the 24 Hz band there is no undoing it — the two are the same numbers, and no amount of cleverness downstream can separate them. Remove it while it is still distinguishable, which is what "Convolution & Filters" is for.

Goal: decimate signal by 4 into plane 0, synthesise the folded prediction into plane 1, and show the two are the same signal.

Requirements

Hint 1 — the new sample rate

Keeping one sample in factor divides the rate by the same number:

const newRate = this.constants.sampleRate / this.constants.factor;   // 64

Everything in plane 1 — the fold and the time — uses newRate, never 256.

Hint 2 — the fold, again

Straight from task 2, with the new rate:

const f = tones[c][1];
const a = f - Math.round(f / newRate) * newRate;

Keep the sign. Math.abs here turns −24 Hz into a different signal.

Hint 3 — branching between the planes

Compute the decimated sample, then overwrite it when this thread is in plane 1:

let out = signal[this.thread.x * this.constants.factor];
if (this.thread.y === 1) {
  // … the loop over this.constants.parts …
  out = s;
}
return out;

Same idea elsewhere

Decimation with a pre-filter is the bottom half of every mipmap, every audio sample-rate converter and every image pyramid: CUDA's NPP and Metal Performance Shaders ship it, and a GPU that generates mipmaps without filtering produces exactly the shimmering this task predicts. It is also why "Measuring Speed Honestly" matters here — the filter is the expensive part, and skipping it is the tempting, wrong optimisation.

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.