95be6f6c22
Replaces the stale Unity-port README (wrong controls, premake build, missing features). Documents the mLink/mLnL focus, real keybindings, make-based build, headless --render mode, and the Xvfb GUI-driving primer. Consolidates the old SPECTROGRAM_README.md into README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
195 lines
8.1 KiB
Markdown
195 lines
8.1 KiB
Markdown
# rspektrum
|
||
|
||
An interactive spectrogram viewer for inspecting **mLink** radio captures (and
|
||
any other audio). It computes an STFT of a WAV file, draws it as a zoomable,
|
||
pannable spectrogram, and can overlay **mLnL annotations** — labelled regions
|
||
(TX frames, assertion outcomes, impairment fires, …) carried *inside* the WAV
|
||
file itself. You can box a time/frequency region, hear it back through a
|
||
bandpass filter, and export either the picture (PNG) or the isolated audio
|
||
(WAV). It runs as a native desktop app (C + raylib), as a headless CLI renderer,
|
||
and as a WebAssembly build in the browser.
|
||
|
||
## What it's for
|
||
|
||
The primary use case is reviewing captures from the mLink stack: a WAV recording
|
||
of an over-the-air signal, with an embedded `mLnL` chunk describing what the
|
||
modem/daemon believed it was transmitting at each instant. rspektrum renders
|
||
those annotations on top of the measured spectrogram so you can see, frame by
|
||
frame, what was *intended* versus what actually hit the air. See
|
||
[`mlnl_chunk_spec.md`](mlnl_chunk_spec.md) for the annotation format. It also
|
||
works as a general-purpose spectrogram tool for plain WAVs with no annotations.
|
||
|
||
## Features
|
||
|
||
- **STFT spectrogram** with selectable colormaps, adjustable dB floor / dynamic
|
||
range, and absolute (dBFS) or relative amplitude scaling.
|
||
- **mLnL annotation overlay** — labelled boxes drawn from the WAV's embedded
|
||
annotation chunk; hover a box (or its region on the waveform scope) for a
|
||
tooltip with per-frame detail (sequence, channel, rate, scheduling offset…).
|
||
- **Zoom & pan** the time/frequency view (mouse wheel + Alt/middle-drag).
|
||
- **Region selection**: box a time **and** frequency range with the mouse.
|
||
- **Filtered playback**: play just the selected region, band-limited to the
|
||
selected frequency box (FFT bandpass — "what you hear is what you'd export").
|
||
- **Waveform scope** (toggle) showing the time-domain signal under the spectrum.
|
||
- **Marker / ruler tool** and a **spectrum slice (PSD)** readout.
|
||
- **Export**: save the view as a **PNG**, or the selected region as a **WAV**.
|
||
- **Headless render mode**: produce an annotated PNG from the command line with
|
||
no window (see below).
|
||
- **Broad input support**: WAV directly (8/16-bit PCM, 32-bit float; stereo
|
||
downmixed to mono); other formats transparently transcoded via `ffmpeg` if it
|
||
is on `PATH`. Drag-and-drop loading.
|
||
- **Cross-platform**: Linux/desktop, Windows, and a WebAssembly build.
|
||
|
||
## Building
|
||
|
||
The build is driven by a checked-in Makefile (premake5 is **not** required to
|
||
build — only to regenerate the makefiles):
|
||
|
||
```bash
|
||
make -f rspektrum.make config=debug_x64 # -> bin/Debug/rspektrum
|
||
make -f rspektrum.make config=release_x64 # -> bin/Release/rspektrum
|
||
```
|
||
|
||
The web build:
|
||
|
||
```bash
|
||
./build_web.sh # emscripten; emits the WebAssembly bundle
|
||
```
|
||
|
||
> Note: the release build enables `-O2`, which turns on extra warnings
|
||
> (`-Wformat-truncation`) that the debug build doesn't — build release before
|
||
> declaring a change clean.
|
||
|
||
## Running (GUI)
|
||
|
||
```bash
|
||
./bin/Debug/rspektrum [input.wav]
|
||
```
|
||
|
||
Load a file by passing it on the command line, dragging a `.wav` onto the window,
|
||
or pressing **O** for the file browser.
|
||
|
||
### Controls
|
||
|
||
| Input | Action |
|
||
|-------|--------|
|
||
| **O** | Open file browser |
|
||
| **Mouse wheel** | Zoom time/frequency |
|
||
| **Alt+drag** / **middle-drag** | Pan the view |
|
||
| **LMB drag** | Select a time + frequency region |
|
||
| **Space** | Play / stop the selected region |
|
||
| **Hover an annotation** | Tooltip with that frame's mLnL detail |
|
||
| **P** | Show / hide the waveform scope |
|
||
| **M** | Marker / ruler tool |
|
||
| **S** | Spectrum slice (PSD) |
|
||
| **E** | Export PNG |
|
||
| **W** | Export selection as WAV |
|
||
| **Home** | Reset view (fit all) |
|
||
| **End** | Zoom to start |
|
||
| **F11** | Toggle fullscreen |
|
||
| **F1** | About / help |
|
||
| **Esc** | Clear selection / close dialog |
|
||
|
||
Most controls are also available as buttons in the left sidebar (colormap,
|
||
floor, dynamic range, annotation opacity, grid, etc.).
|
||
|
||
## Headless rendering (CLI)
|
||
|
||
rspektrum can render an annotated spectrogram to a PNG without opening a window —
|
||
useful for batch capture and for agents reviewing test output:
|
||
|
||
```bash
|
||
./bin/Debug/rspektrum --render OUT.png INPUT.wav [options]
|
||
```
|
||
|
||
Options:
|
||
|
||
| Flag | Effect |
|
||
|------|--------|
|
||
| `-r, --render OUT.png` | Render to `OUT.png` and exit (no window) |
|
||
| `-a, --annotations` | Force the annotation overlay **on** (solid, for review) |
|
||
| `--no-annotations` | Force the overlay off |
|
||
| `--annotation-opacity=V` | Resting overlay alpha `0..1` (default `0.06`, faint) |
|
||
| `--pane` | Capture only the spectrogram pane (no sidebar/scope) |
|
||
| `--width N` / `--height N` | Output size (default `1280×800`) |
|
||
| `-h, --help` | Usage |
|
||
|
||
Default annotation opacity is intentionally faint (the overlay is meant to stay
|
||
out of the way until hovered), so for a screenshot you actually want to *review*,
|
||
pass `--annotations`. Example against the bundled sample:
|
||
|
||
```bash
|
||
./bin/Debug/rspektrum --render /tmp/review.png mlnl_samples.wav --annotations
|
||
```
|
||
|
||
`mlnl_samples.wav` is an in-repo WAV that carries an embedded `mLnL` chunk.
|
||
|
||
> The hover tooltip (sched offset, per-frame detail) only appears with a live
|
||
> mouse over a box, so it cannot show up in a static `--render`. To verify
|
||
> tooltip behaviour you need a real (or virtual) display driving the GUI — see
|
||
> below.
|
||
|
||
## Driving the GUI headlessly (for agents / CI)
|
||
|
||
The app can be run, screenshotted, and clicked on a virtual X display with no
|
||
monitor or GPU (Mesa software GL under Xvfb). The accumulated playbook lives in
|
||
[`AGENTS.md`](AGENTS.md); the working reference implementation is
|
||
[`shot_input.sh`](shot_input.sh).
|
||
|
||
The loop in one breath:
|
||
|
||
```bash
|
||
Xvfb :99 -screen 0 1280x800x24 >/tmp/xvfb.log 2>&1 & # 1. fake screen
|
||
DISPLAY=:99 ./bin/Debug/rspektrum mlnl_samples.wav \
|
||
>/tmp/app.log 2>&1 & # 2. run on it
|
||
sleep 2 # 3. reach a steady frame
|
||
DISPLAY=:99 import -window root /tmp/shot.png # 4. grab the frame
|
||
```
|
||
|
||
Prerequisites (Debian/Ubuntu): `sudo apt-get install xvfb imagemagick xdotool`
|
||
(plus `libgl1-mesa-dri` and `LIBGL_ALWAYS_SOFTWARE=1` if GL fails / frames are
|
||
black). To exercise UI paths, synthesize input with `xdotool` against
|
||
`DISPLAY=:99` — e.g. move the mouse over an annotation box and re-grab to capture
|
||
the hover tooltip:
|
||
|
||
```bash
|
||
DISPLAY=:99 xdotool mousemove 640 400 # hover a box (coords from the spectrogram)
|
||
DISPLAY=:99 import -window root /tmp/hover.png
|
||
DISPLAY=:99 xdotool key space # play the selection, etc.
|
||
```
|
||
|
||
There is no window manager, so the window sits at `0,0` and fills the Xvfb
|
||
screen — match the screen size to the window and a root grab equals the app's
|
||
frame. Always capture stdout/stderr to a log; it's your only view of `TraceLog`
|
||
output and crashes. `shot_input.sh` wraps all of this (start → settle →
|
||
screenshot → optional input → diff); read `AGENTS.md` for the traps (input focus,
|
||
ImageMagick v6 vs v7 command names, software-GL timing).
|
||
|
||
## Technical notes
|
||
|
||
- **STFT**: Hann-windowed, 2048-point FFT with 50% overlap by default;
|
||
frequency resolution `sampleRate / fftSize` Hz per bin. Amplitude shown in dB.
|
||
- **Axes**: X = time (s), Y = frequency (Hz, scaled to the file's Nyquist),
|
||
colour = amplitude.
|
||
- **Playback / WAV export** share the same processing path: the selected time
|
||
span, FFT-bandpassed to the selected frequency box, peak-normalised.
|
||
- **mLnL parsing**: walks the WAV's RIFF chunks for the four-CC `mLnL` chunk
|
||
(UTF-8 JSON Lines); unknown chunks are skipped, so annotated files remain
|
||
standards-compliant audio everywhere else.
|
||
|
||
## Source layout
|
||
|
||
```
|
||
src/
|
||
spectrogram.c # entry point, main loop, CLI args, headless render
|
||
stft.c / fft.c # STFT + FFT
|
||
render.c # spectrogram, annotations, tooltips, scope
|
||
ui.c # sidebar, file browser, buttons
|
||
audio.c # WAV load (ffmpeg fallback), bandpass, playback, WAV export
|
||
mlnl.c / mlnl.h # mLnL annotation chunk parser
|
||
platform_*.c # per-OS shims (linux / win32 / web)
|
||
```
|
||
|
||
See [`raylib_for_desktop_applications.md`](raylib_for_desktop_applications.md)
|
||
for the performance/idle-CPU lessons behind the desktop build.
|