398be34aaf
- README: rewrite Build section for the hand-written Makefile (make / make DEBUG=1 / run / test / bench). Add a "System dependencies" table with the X11+GL dev packages per distro (apt/dnf/pacman/zypper/apk), since a clone won't build without them and the error is otherwise cryptic. Fix stale ./bin/Debug paths to ./bin/Release. - Makefile: add `make check-deps` — probes for the required X11/GL dev headers and prints the install command for the detected distro on failure (via /etc/os-release). - build_web.sh: auto-discover app modules from src/*.c (drop the hardcoded APP_MODULES list that had to be hand-synced), matching the desktop Makefile. Excludes the desktop platform backends. Remove the stale rspektrum.make reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
256 lines
10 KiB
Markdown
256 lines
10 KiB
Markdown
# rspektrum
|
||
|
||
**rspektrum** is an interactive spectrogram viewer for inspecting radio captures
|
||
and arbitrary audio. It loads a WAV file, computes a short-time Fourier transform
|
||
(STFT), and draws the result as a zoomable, pannable time–frequency image. Its
|
||
distinguishing feature is support for **mLnL annotations** — labelled regions
|
||
(TX frames, assertion outcomes, impairment fires, …) carried *inside* the WAV
|
||
file itself — which it overlays on the measured spectrogram so you can compare
|
||
what a modem *intended* to transmit against what actually hit the air.
|
||
|
||
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). rspektrum runs three
|
||
ways: a native desktop app (C + raylib), a headless command-line renderer, and a
|
||
WebAssembly build in the browser.
|
||
|
||
## [Click for Video Demo](https://nicecrew.tv/w/2w9Y5qvKuDz6mwrzAweryW)
|
||
|
||
|
||
|
||

|
||
|
||
|
||
|
||
---
|
||
|
||
## What it's for
|
||
|
||
The primary use case is reviewing captures from the **mLink** radio 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, frame by frame, so intent
|
||
and reality sit side by side.
|
||
|
||
It also works as a general-purpose spectrogram tool for plain WAVs with no
|
||
annotations. See [`mlnl_chunk_spec.md`](mlnl_chunk_spec.md) for the annotation
|
||
format.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
- **STFT spectrogram** — selectable colormaps, adjustable dB floor / dynamic
|
||
range, absolute (dBFS) or relative amplitude scaling.
|
||
- **mLnL annotation overlay** — labelled boxes from the WAV's embedded annotation
|
||
chunk; hover a box (or its region on the scope) for per-frame detail (sequence,
|
||
channel, rate, scheduling offset…).
|
||
- **Zoom & pan** the time/frequency view.
|
||
- **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 via an FFT bandpass. What you hear is what you'd export.
|
||
- **Waveform scope** — toggleable time-domain view beneath the spectrum.
|
||
- **Marker / ruler** 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 CLI with no
|
||
window, no GL, and no X server. Pure CPU; runs in CI, containers, or over SSH.
|
||
- **Broad input** — WAV directly (8/16-bit PCM, 32-bit float; stereo downmixed to
|
||
mono); other formats transcoded via `ffmpeg` if it's on `PATH`. Drag-and-drop.
|
||
- **Cross-platform** — Linux/desktop, Windows, and a WebAssembly build.
|
||
|
||
---
|
||
|
||
## Building
|
||
|
||
You need only **`make` and a C compiler** (`gcc` or `clang`) plus the X11/OpenGL
|
||
**development** headers (see below). raylib is vendored in this repo and compiled
|
||
from source — there is no separate raylib install step, no `premake`, no network
|
||
access required. A plain clone builds:
|
||
|
||
```bash
|
||
make # release -> bin/Release/rspektrum (-O3 -ffast-math, AVX2/FMA)
|
||
make DEBUG=1 # debug -> bin/Debug/rspektrum (-g, no optimization)
|
||
make run # build + launch
|
||
make test # build + run the DSP correctness tests
|
||
make bench # FFT benchmark over mlnl_samples.wav
|
||
make clean
|
||
```
|
||
|
||
Useful overrides: `make CC=clang`, or `make ARCH=-march=native` to tune for your
|
||
own CPU (the default `-march=x86-64-v3` targets any ~2013+ x86-64 chip; drop it
|
||
with `make ARCH=` for an older CPU).
|
||
|
||
### System dependencies
|
||
|
||
The compiler needs the X11 and OpenGL **dev** headers (the runtime libs are
|
||
already present on any desktop; only the `-dev`/`-devel` packages are usually
|
||
missing). The X11 extension libraries (Xrandr, Xinerama, Xcursor, Xi) are opened
|
||
at runtime via `dlopen`, but their **headers** are still required to compile.
|
||
|
||
If `make` stops with an error like `fatal error: X11/Xlib.h: No such file` or
|
||
`GL/gl.h: No such file`, install the dev packages for your distribution:
|
||
|
||
| Distro | Command |
|
||
|--------|---------|
|
||
| **Debian / Ubuntu / Mint** | `sudo apt install build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev` |
|
||
| **Fedora / RHEL / Rocky** | `sudo dnf install gcc make libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel mesa-libGL-devel` |
|
||
| **Arch / Manjaro** | `sudo pacman -S base-devel libx11 libxrandr libxinerama libxcursor libxi mesa` |
|
||
| **openSUSE** | `sudo zypper install gcc make libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel Mesa-libGL-devel` |
|
||
| **Alpine** | `sudo apk add build-base libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev mesa-dev` |
|
||
|
||
On Debian/Ubuntu the single metapackage `xorg-dev` pulls in all of the X11 `-dev`
|
||
packages above, if you'd rather not list them.
|
||
|
||
Run `make check-deps` to probe for the required headers before building — it
|
||
prints the install hint for your platform if anything is missing.
|
||
|
||
### Web (WebAssembly) build
|
||
|
||
```bash
|
||
./build_web.sh # emscripten; emits the WebAssembly bundle to bin/web/
|
||
```
|
||
|
||
---
|
||
|
||
## Usage (desktop GUI)
|
||
|
||
```bash
|
||
./bin/Release/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. Try the bundled sample:
|
||
|
||
```bash
|
||
./bin/Release/rspektrum mlnl_samples.wav # in-repo WAV with an embedded mLnL chunk
|
||
```
|
||
|
||
### 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, …).
|
||
|
||
---
|
||
|
||
## Usage (headless render)
|
||
|
||
`--render` writes the spectrogram straight to a PNG **with no window, no GL
|
||
context, and no X server**. It computes the STFT, colorizes the bitmap, bakes the
|
||
annotation overlay onto it, and exports — all on the CPU — so it runs anywhere
|
||
(CI, a bare SSH session, a container with no display):
|
||
|
||
```bash
|
||
./bin/Debug/rspektrum --render OUT.png INPUT.wav [options]
|
||
```
|
||
|
||
The output is the **real spectrogram bitmap** at native STFT resolution (not a
|
||
screenshot of the UI), so it carries no sidebar/scope chrome — just the
|
||
time–frequency image with the annotation overlay.
|
||
|
||
| Flag | Effect |
|
||
|------|--------|
|
||
| `-r, --render OUT.png` | Render to `OUT.png` and exit (no window/GL/X) |
|
||
| `-a, --annotations` | Force the annotation overlay **on** |
|
||
| `--no-annotations` | Force the overlay off |
|
||
| `--annotation-opacity=V` | Overlay strength `0..1` (default `0.5`) |
|
||
| `--annotation-kinds=LIST` | Comma-separated kinds to draw (default: all) |
|
||
| `--width N` | Resize output to `N` px wide (default: native STFT size) |
|
||
| `-h, --help` | Usage |
|
||
|
||
Annotation boxes are drawn **outline + label only** (no translucent fill): mLnL
|
||
captures contain many overlapping full-band boxes whose fills would alpha-stack
|
||
to opaque and bury the signal, so the outline marks each region while the
|
||
spectrogram reads through.
|
||
|
||
```bash
|
||
# everything, brighter overlay
|
||
./bin/Debug/rspektrum --render /tmp/all.png mlnl_samples.wav --annotation-opacity=0.7
|
||
|
||
# only on-air frames and failed assertions
|
||
./bin/Debug/rspektrum --render /tmp/tx.png mlnl_samples.wav \
|
||
--annotation-kinds=tx_frame,assertion_failed
|
||
```
|
||
|
||
Annotation kinds: `tx_frame`, `tx_burst`, `control`, `channel_up`,
|
||
`channel_down`, `assertion_passed`, `assertion_failed`, `impairment_fire`,
|
||
`gain_change`, `unknown`.
|
||
|
||
> The hover tooltip 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 (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 full 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). Synthesize input with `xdotool` against `DISPLAY=:99` to exercise UI
|
||
paths.
|
||
|
||
---
|
||
|
||
## Technical notes
|
||
|
||
- **STFT** — Hann-windowed, 2048-point FFT with 50% overlap by default;
|
||
frequency resolution `sampleRate / fftSize` Hz per bin. Amplitude in dB.
|
||
- **Axes** — X = time (s), Y = frequency (Hz, scaled to the file's Nyquist),
|
||
colour = amplitude.
|
||
- **Playback / WAV export** share one 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 stay
|
||
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, and
|
||
[`AGENTS.md`](AGENTS.md) for the headless-testing playbook.
|