Task 4 of 5
The numbers in a spectrogram span orders of magnitude. In this one the loudest cell
is about 0.126 and the median cell about 0.00014 — a factor of
nearly 900. Hand that straight to a colour ramp and the median cell gets a tenth of one
percent of the range: you get a black rectangle with a few bright streaks, and the obvious
conclusion — "my kernel is broken" — is wrong. Your kernel is fine. Your scaling is
wrong.
The fix is what ears already do: work in decibels.
db = 20 · log₁₀(mag / peak) puts the loudest cell at 0 and every halving 6 dB
below it, so a 60 dB display range covers a factor of 1,000 in one legible ramp and
everything quieter clamps to black. That single transform is the difference between a
picture and a black rectangle.
Then colour. Loudness rides on value — the part that has to be monotone, or the picture lies about which cell is louder — and the hue sweep is there for legibility, which is the case Colour Spaces argues at length. The ramp below is written for you: black → indigo → magenta → orange → cream. What is yours is the two lines above it, and the canvas is 256×256, so each spectrogram cell paints a 4×2 block.
db = 20 * Math.log10(mag / peak + 1e-9)this.constants.range decibels onto 0…1 and clamp: Math.max(0, Math.min(1, 1 + db / this.constants.range))256×256render(paint.canvas) callMath.log10 is on gpu.js's whitelist, so write it directly — no need
for Math.log(x) / Math.LN10. Math.log is the natural
logarithm and gives answers 2.3 times too far down the scale.
const db = 20 * Math.log10(mag / peak + 1e-9);
const v = Math.max(0, Math.min(1, 1 + db / this.constants.range));log₁₀(0) is -Infinity, and -Infinity / 60
stays infinite: one silent cell would paint NaN, which lands on screen as
whatever the driver felt like. The tiny floor costs nothing (it is 180 dB down) and
makes the silent case land on plain black instead.
librosa.amplitude_to_db, every DAW meter, every analyser you have ever looked
at — and the wider lesson generalises well past sound: a GPU can compute a perfectly correct
answer and a bad transfer function will still show you nothing. HDR tone mapping, log-scale
depth buffers and gamma correction are the same move made for the same reason. On any
platform this is a fragment shader reading a storage texture; the arithmetic does not
change.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.