feat: true no-X headless --render (CPU spectrogram bitmap + overlay)

Rewrite --render to compute the spectrogram and write a PNG entirely on the
CPU, with no window, no GL context, and no X server. Previously it opened a
hidden GL window and grabbed LoadImageFromScreen(), which still required an X
server (Xvfb); the output was a UI screenshot rather than the spectrogram data.

The new path (RunHeadlessRender) loads the WAV, computes the STFT, colorizes
the bitmap at native STFT resolution, bakes the mLnL annotation overlay onto
it, and exports — all CPU-only. render.c gains a GL-free colorize
(BuildSpectrogramImageCPU), a CPU font loader (LoadFontCPU), and a CPU overlay
drawer (DrawAnnotationsToImage).

Annotations draw outline + label only: mLnL captures contain many overlapping
full-band boxes whose translucent fills alpha-stack to opaque and bury the
signal. The outline marks each region while the spectrogram reads through; a
dark backing strip keeps labels legible. Note: MeasureTextEx/ImageText* bail
when font.texture.id == 0, so the CPU font sets a sentinel non-zero id (the
draw path reads glyph images, never the texture).

Render options: --annotation-opacity (overlay strength), --annotation-kinds
(comma-separated kind filter), --width (resize; default native). Removed the
obsolete --pane/--height window options and the screenshot workaround.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 22:37:38 -07:00
parent 95be6f6c22
commit fb7bc5486e
5 changed files with 378 additions and 120 deletions
+15
View File
@@ -76,6 +76,21 @@ Trigger it from a temp keybinding or a `--shoot-after N frames` flag. This bypas
ImageMagick entirely and is the exact framebuffer — preferable when you can edit code
(which, in a refactor task, you already are).
**C. A true no-display render path (if the app has one).** The strongest option when
you only need to verify *output* (not the live UI): a CLI mode that computes the frame
and writes a PNG entirely on the CPU, never calling `InitWindow` — so it needs **no
Xvfb, no GL, no X server at all**. raylib's `Image` API (`GenImageColor`, `ImageDraw*`,
`ImageDrawTextEx`, `ExportImage`) is pure CPU; only `Texture*`/`Draw*`/`BeginDrawing`
need a GL context. So a headless path can reuse the real pixel/colorize/annotation code
and skip the window. (One trap: `MeasureTextEx`/`ImageText*` short-circuit to zero when
`font.texture.id == 0`, so a CPU-loaded font — `LoadFontData` + `GenImageFontAtlas`, no
upload — must set a sentinel non-zero `texture.id` to draw text.)
> **This repo** has exactly that: `rspektrum --render OUT.png INPUT.wav` (see
> `RunHeadlessRender` in `spectrogram.c`). Use it to check spectrogram/annotation output
> with no display. The Xvfb loop below is still needed to exercise the *interactive* GUI
> (clicks, drags, hover tooltips, the live sidebar) — things the static render can't show.
Then **look at it**: open the PNG with your image-reading tool. Vision catches "the HUD
vanished" or "text is now black-on-black" that a pixel count won't explain.