Files
rspektrum/known_bugs.md
tyler 438150080c fix: check STFT allocations; reject files too large for the web build
Every malloc/calloc in stft.c was written through unchecked. On desktop
that never bites — Linux overcommits and swaps — but WebAssembly has a
hard 32-bit address-space ceiling, so on a long capture the allocations
genuinely fail and the code wrote through NULL into low memory. That is
why it surfaced as corrupted font glyphs rather than a crash, and only
after loading a large file.

The apparent hang had the same root: ComputeSegment returned void, so a
failed allocation was invisible and the loop ground through all remaining
segments failing identically. The cursor readout showing "-" for the level
was the tell — the segments were there but their spectra were NULL.

ComputeSegment now returns a bool, ComputeSTFTIncremental stops at the
first failure instead of churning, and both halves of a segment's spectra
are freed together (reassignment reads them in lockstep, so half a segment
is worse than none). ComputeSTFTInit, CopySTFT and the scratch buffers are
checked too.

Segments left NULL are already skipped by every consumer — that is how the
progressive fill renders partial results — so a file that nearly fits
degrades to a truncated spectrogram rather than corrupting.

The web load path treats exhaustion as fatal and says so: a multi-hour
capture needs ~11 GB of spectra (478k segments x 1025 bins x two spectra x
12 bytes for the 5.7-hour case) against a 4 GB ceiling browsers cap below
in practice, so there is nothing useful to fall back to. Better to explain
that than present a spectrogram full of holes.

Corrects the known_bugs.md entry, which blamed ALLOW_MEMORY_GROWTH
invalidating cached pointers. That theory fit the symptom but was wrong;
the allocations were simply failing. Also notes what making large files
actually work would take — 16-bit magnitudes, dropping the derivative
spectrum when synchrosqueezing is off, or streaming segments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN
2026-08-12 20:32:17 -07:00

5.4 KiB

Known bugs & rough edges

Behaviour that is unspecified, awkward, or known-imperfect — as distinct from outright breakage. Each entry says what happens, why, and what a real fix would need to decide.


Large WAVs in the browser: too large to analyse

Status: diagnosed and handled. The file is rejected with a message instead of corrupting or hanging; it still cannot be opened in the browser.

A multi-hour capture needs far more memory than a 32-bit WebAssembly page can address. v23 (5.7 h at 12 kHz) works out to ~478k STFT segments x 1025 bins x two spectra x 12 bytes — roughly 11 GB of spectra alone, against a hard wasm32 ceiling of 4 GB that browsers cap below in practice.

The old symptom was not a memory-growth bug, as first assumed. The per-segment mallocs in ComputeSegment were simply failing and their results written through unchecked. On desktop that never bites (Linux overcommits and swaps), but in wasm the failure is real, so the code wrote through NULL into low memory — which is why it surfaced as corrupted font glyphs rather than a crash. The apparent "hang" was the loop grinding through every remaining segment, each failing the same way, since ComputeSegment returned void and nothing noticed.

Every allocation in stft.c is now checked. ComputeSegment reports failure, ComputeSTFTIncremental stops at the first one rather than churning, and the web load path frees the partial result and explains that the file is too large. A file that nearly fits should degrade to a truncated spectrogram — NULL segments are already skipped everywhere, which is how the progressive fill draws partial results — though that path is reasoned rather than tested.

Making large files actually work in the browser needs a different data layout: storing magnitudes as 16-bit, dropping the derivative spectrum unless synchrosqueezing is on, or streaming segments rather than holding them all.

Testing note: always serve the web build with serve_web.py, never python3 -m http.server. Browsers cache .wasm hard enough that a plain reload runs a stale module, which makes rebuilds look like no-ops and has already burned significant time chasing bugs that were fixed.


Playhead vs. a selection edited mid-playback

Status: partially addressed; underlying semantics still undefined.

Playback hands a snapshot of the selected region to the audio device — the samples are copied, bandpassed, and normalised up front, so the sound coming out of the speakers is fixed the moment Space is pressed. The selection box, however, stays live and editable while that audio plays.

Previously the playhead marker was drawn against the live app.sel, so moving or resizing the selection during playback made the marker jump, run off the end, or scale to a region that had nothing to do with what was audible. The playhead is now measured against playSelStart / playSelEnd / playDuration, captured at PlaySelectedRegion() time, so it tracks the audio that is actually playing.

What remains undefined is the product question, not the drawing math:

  • If the user drags the selection somewhere else mid-playback, should the audio follow (restart / re-seek against the new region), or should playback keep going with the old buffer and the marker stay where it is (current behaviour)?
  • Should editing the selection during playback simply stop playback?
  • Should the playhead remain visible when the region it refers to is scrolled off-screen, or has been replaced by a selection elsewhere in the file?

Current behaviour is the conservative reading: the sound wins. The marker always describes real audio, and a mid-playback edit is treated as staging the next thing to play rather than modifying the current one. That is defensible but was never explicitly chosen, and the UI gives no feedback that the box on screen and the audio in flight have diverged.

Related: a sub-threshold click inside an existing selection deliberately does not clear it (hoverInsideSelection in spectrogram.c), because silently clearing changes what Space would play. A click on empty space still resets to full range.

Touches: audio.c (PlaySelectedRegion), spectrogram.c (playhead advance, scope cursor), render.c (DrawPlayhead), spectrogram_types.h (playSelStart / playSelEnd / playDuration).


Long-file zoom sharpness lags the zoom gesture

Status: working as designed, but reads as a bug.

ComputeSkipFactor() (stft.c) strides the initial STFT pass for long files — every 8th segment past 10 minutes — so the overview loads promptly. The missing segments are filled at full resolution afterwards: the visible range first, then a background sweep of the whole file.

The practical effect is that a hard zoom into a 30-minute file can look blocky for a moment before the foreground fill catches up and it sharpens. The fill is gated on view.end - view.start <= 0.25f, so it only runs once reasonably zoomed in. If a view stays blocky indefinitely, the fill is not reaching that range and that is a real bug worth chasing.


Load time on long files is unbounded and unreported

A 30-minute 48 kHz file spends a long time in Processing… before the UI is usable, and the percentage indicator advances non-linearly (the strided overview completes fast, the high-res fill does not). There is no cancel. Headless/scripted runs must wait this out; see AGENTS.md.