438150080c
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
109 lines
5.4 KiB
Markdown
109 lines
5.4 KiB
Markdown
# 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
|
|
`malloc`s 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`.
|