SomaSonic answers that per frequency band, per time window — from the raw samples, not from a listening session. Upload a narration stem and the bed that plays underneath it, and it returns the map of exactly where the two are fighting.
The workspace. Every pairing keeps its score, and the score carries the same colour ramp the analysis screen uses — so a number and its colour can never disagree between two views.Upload, with the work done before the upload. The browser decodes the file, downmixes to mono, resamples to 16 kHz and converts to Int16 PCM — so what crosses the network is 5.5 MB instead of 33 MB. The progress bar is reporting that local pass, not the transfer.The result. Time runs left to right (0–60 s), frequency bottom to top (312 Hz–8 kHz). Cool means the voice is clear; hot means the bed is within 6 dB of it. Read the bottom row: this bed is fighting the narration almost entirely below 312 Hz — which is a fix you can act on, and exactly what a single loudness number cannot tell you. The vertical breaks are the gaps between spoken lines; silence is never scored, because silence cannot be masked.The same screen on a phone — a real 390 px viewport, not a scaled-down desktop. The map keeps the worst value in each column as it reduces rather than averaging, so a tenth-of-a-second clash survives the squeeze; the time axis just thins its labels to fit.
How it works
Client decimation → NumPy STFT → canvas
Decode in the browser, not on the serverAn OfflineAudioContext downmixes to mono and resamples to 16 kHz Int16 PCM before anything is sent. That is what keeps the backend down to two dependencies — no ffmpeg, no librosa, no codec binaries in the deploy.
Trust the samples, not the metadataThe server reads the byte array with np.frombuffer(..., '<i2') and derives duration by counting samples, so a file lying in its header cannot shift the timeline.
Short-time Fourier transformA Hanning window, N=1024, hop 512 — 31.25 frames per second. A three-minute track becomes 5,624 analysable frames.
Two gates, because a naive score is meaninglessFrames below the energy floor are skipped — silence cannot be masked, and without this gate the quiet gaps dominate the score. Then anything more than 40 dB under a frame's own peak is discarded, so spectral leakage is not read as a clash.
Eight log-spaced bands0–8016 Hz with widening bins (20/20/25/35/60/90/140/123) to match how hearing actually resolves pitch, scored against a named HEADROOM_DB = −6.0 threshold.
Render without melting the browser44,992 stored values reduce to 6,240 canvas cells — one column per pixel, worst value wins — drawn with fillRect in about 25 ms. As DOM elements it would have frozen the render tree.
2backend dependencies — Django 6.1.1 and NumPy 2.5.3
33 MB → 5.5 MBpayload per stem, after client-side decimation
5,624analysed frames from a 3-minute track
229 KBof JSON holding 44,992 spectral values
~25 msto draw the heatmap, 6,240 cells
−6 dBthe headroom hinge the whole reading turns on
Why this problem
It came from a real failure
I run an autonomous pipeline that renders long-form documentary films — generated narration over a generated music bed. Its worst recurring defect was never a crash: it was a finished film where the bed sat on top of the voice, and nothing in the system could see it, because loudness targets are met by a mix that is still unintelligible. A single number for the whole track cannot find a clash that lives in one band for seven seconds.
SomaSonic is that missing measurement built as its own tool: not “is this loud enough”, but where, and for how long, is the bed inside the voice's headroom. The run shown above is real material from that pipeline — a Ngorongoro episode's narration against its own music bed. It scores 48%, with 27.4 seconds of speech contested, almost all of it under 312 Hz.
Academic honesty. SomaSonic is my capstone for HarvardX CS50W (Web Programming with Python and JavaScript), submitted in September 2026 and currently in grading — not a completed credential.
The source repository stays private under CS50's policy on publishing coursework. This page shows the interface and the design decisions; it does not distribute the code. Happy to walk through the implementation live.