docs: rewrite README as a fresh project overview + usage guide
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,73 +1,90 @@
|
||||
# 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
|
||||
**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. 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
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** 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, no GL, and no X server — pure CPU, runs anywhere (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.
|
||||
- **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):
|
||||
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:
|
||||
Web (WebAssembly) 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
|
||||
> 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)
|
||||
---
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
@@ -90,15 +107,17 @@ or pressing **O** for the file browser.
|
||||
| **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.).
|
||||
Most controls are also available as buttons in the left sidebar (colormap, floor,
|
||||
dynamic range, annotation opacity, grid, …).
|
||||
|
||||
## Headless rendering (CLI)
|
||||
---
|
||||
|
||||
## 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. This runs anywhere
|
||||
(CI, a bare SSH session, a container with no display), not just under Xvfb:
|
||||
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]
|
||||
@@ -108,8 +127,6 @@ 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.
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `-r, --render OUT.png` | Render to `OUT.png` and exit (no window/GL/X) |
|
||||
@@ -123,8 +140,7 @@ Options:
|
||||
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. `--annotation-opacity` controls outline/label
|
||||
strength. Filter to just the kinds you care about with `--annotation-kinds`:
|
||||
spectrogram reads through.
|
||||
|
||||
```bash
|
||||
# everything, brighter overlay
|
||||
@@ -135,20 +151,20 @@ strength. Filter to just the kinds you care about with `--annotation-kinds`:
|
||||
--annotation-kinds=tx_frame,assertion_failed
|
||||
```
|
||||
|
||||
Kinds: `tx_frame`, `tx_burst`, `control`, `channel_up`, `channel_down`,
|
||||
`assertion_passed`, `assertion_failed`, `impairment_fire`, `gain_change`,
|
||||
`unknown`. `mlnl_samples.wav` is an in-repo WAV that carries an embedded `mLnL`
|
||||
chunk.
|
||||
Annotation kinds: `tx_frame`, `tx_burst`, `control`, `channel_up`,
|
||||
`channel_down`, `assertion_passed`, `assertion_failed`, `impairment_fire`,
|
||||
`gain_change`, `unknown`.
|
||||
|
||||
> 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.
|
||||
> 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 (for agents / CI)
|
||||
---
|
||||
|
||||
## 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 accumulated playbook lives in
|
||||
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).
|
||||
|
||||
@@ -164,35 +180,25 @@ 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:
|
||||
black). Synthesize input with `xdotool` against `DISPLAY=:99` to exercise UI
|
||||
paths.
|
||||
|
||||
```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),
|
||||
- **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 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
|
||||
- **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
|
||||
|
||||
```
|
||||
@@ -207,4 +213,5 @@ src/
|
||||
```
|
||||
|
||||
See [`raylib_for_desktop_applications.md`](raylib_for_desktop_applications.md)
|
||||
for the performance/idle-CPU lessons behind the desktop build.
|
||||
for the performance / idle-CPU lessons behind the desktop build, and
|
||||
[`AGENTS.md`](AGENTS.md) for the headless-testing playbook.
|
||||
|
||||
Reference in New Issue
Block a user