Task 2 of 5
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.
200 Hz and its alias into two planes and show they are the
same numbers.f - Math.round(f / fs) * fs — one thread per frequency, sign keptoutput: [256, 2] and picks its frequency with this.thread.yconsole.log itf / 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.
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);This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.