226 lines
8.5 KiB
Markdown
226 lines
8.5 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
|
||
|
||
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
|
||
```
|
||
|
||
Web (WebAssembly) build:
|
||
|
||
```bash
|
||
./build_web.sh # emscripten; emits the WebAssembly bundle
|
||
```
|
||
|
||
> 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.
|
||
|
||
---
|
||
|
||
## Usage (desktop 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. Try the bundled sample:
|
||
|
||
```bash
|
||
./bin/Debug/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.
|