Task 2 of 5

One Number Is Not Enough

Task 1's answer has a hole in it. Take a pure tone at bin 8 and correlate it against a cosine at bin 8: you get 128, the whole amplitude. Now delay that identical tone by 8 samples out of 256 — nothing about the sound has changed — and the same correlation reads 0. The tone is right there and the number says it is absent, because a delay of 8 samples is a quarter turn at bin 8, and a cosine and a sine of the same frequency are orthogonal.

The fix is to measure against both: a cosine and a sine. Two numbers per bin, and the pair rotates as the signal shifts while its length — √(re² + im²) — stays put. That pair is what "complex" means here. It is arithmetic on pairs of floats, not a mysterious new number system: re is what the cosine measured, im is what the sine measured (with a minus sign, from the standard e^(−2πi·ki/n)), and nothing else about it needs believing.

The convention this whole track uses. gpu.js has no complex type, so a complex result is two planes of floats: output: [n, 2], read as result[p][i] — plane p = 0 is the real part, plane p = 1 the imaginary part, i the bin. Since output: [w, h] is indexed [y][x], this.thread.y is the plane and this.thread.x is the bin. Here n is 1 — one bin, two planes.

the delay swings the pair around; the radius is the answer you wanted
Goal: compute both parts of bin 8 with one kernel — output: [1, 2], plane 0 the cosine correlation, plane 1 the sine correlation with a minus sign — and log the real part, the imaginary part and the magnitude for both tone and shiftedTone.

Requirements

Hint 1 — one kernel, two jobs

Every thread runs the same body, so the plane has to be a branch inside it. this.thread.y is 0 for the real plane and 1 for the imaginary one:

if (this.thread.y === 0) acc += signal[i] * Math.cos(angle);
else acc -= signal[i] * Math.sin(angle);
Hint 2 — why the minus sign

The forward transform correlates against e^(−2πi·ki/n), and e^(−iθ) = cos(θ) − i·sin(θ). The minus rides on the sine, which is why plane 1 subtracts. Getting that sign wrong mirrors the entire spectrum, so it is worth fixing here where there is only one bin to look at.

Hint 3 — reading the pair back

With output: [1, 2] the result is two rows of one value:

const out = bin8(tone);
const re = out[0][0];
const im = out[1][0];
console.log('magnitude:', Math.sqrt(re * re + im * im));

Same idea elsewhere

Interleaved or planar complex data is a decision every signal library makes: cuFFT and rocFFT let you choose between cufftComplex (interleaved re/im pairs) and planar layouts, VkFFT does the same, and WebGPU compute shaders usually reach for a vec2<f32>. Planes suit gpu.js because an output axis is free; the arithmetic underneath is identical whichever way the bytes are arranged.

All tasks in The DFT, Honestly

  1. One Bin, One Thread
  2. One Number Is Not Enough
  3. Every Bin, One Thread Each
  4. Magnitude, Phase and the Mirror
  5. There and Back Again

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