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
|
# rspektrum
|
||||||
|
|
||||||
An interactive spectrogram viewer for inspecting **mLink** radio captures (and
|
**rspektrum** is an interactive spectrogram viewer for inspecting radio captures
|
||||||
any other audio). It computes an STFT of a WAV file, draws it as a zoomable,
|
and arbitrary audio. It loads a WAV file, computes a short-time Fourier transform
|
||||||
pannable spectrogram, and can overlay **mLnL annotations** — labelled regions
|
(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
|
(TX frames, assertion outcomes, impairment fires, …) carried *inside* the WAV
|
||||||
file itself. You can box a time/frequency region, hear it back through a
|
file itself — which it overlays on the measured spectrogram so you can compare
|
||||||
bandpass filter, and export either the picture (PNG) or the isolated audio
|
what a modem *intended* to transmit against what actually hit the air.
|
||||||
(WAV). It runs as a native desktop app (C + raylib), as a headless CLI renderer,
|
|
||||||
and as a WebAssembly build in the browser.
|
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
|
## What it's for
|
||||||
|
|
||||||
The primary use case is reviewing captures from the mLink stack: a WAV recording
|
The primary use case is reviewing captures from the **mLink** radio stack: a WAV
|
||||||
of an over-the-air signal, with an embedded `mLnL` chunk describing what the
|
recording of an over-the-air signal with an embedded `mLnL` chunk describing what
|
||||||
modem/daemon believed it was transmitting at each instant. rspektrum renders
|
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
|
those annotations on top of the measured spectrogram, frame by frame, so intent
|
||||||
frame, what was *intended* versus what actually hit the air. See
|
and reality sit side by side.
|
||||||
[`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.
|
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
|
## Features
|
||||||
|
|
||||||
- **STFT spectrogram** with selectable colormaps, adjustable dB floor / dynamic
|
- **STFT spectrogram** — selectable colormaps, adjustable dB floor / dynamic
|
||||||
range, and absolute (dBFS) or relative amplitude scaling.
|
range, absolute (dBFS) or relative amplitude scaling.
|
||||||
- **mLnL annotation overlay** — labelled boxes drawn from the WAV's embedded
|
- **mLnL annotation overlay** — labelled boxes from the WAV's embedded annotation
|
||||||
annotation chunk; hover a box (or its region on the waveform scope) for a
|
chunk; hover a box (or its region on the scope) for per-frame detail (sequence,
|
||||||
tooltip with per-frame detail (sequence, channel, rate, scheduling offset…).
|
channel, rate, scheduling offset…).
|
||||||
- **Zoom & pan** the time/frequency view (mouse wheel + Alt/middle-drag).
|
- **Zoom & pan** the time/frequency view.
|
||||||
- **Region selection**: box a time **and** frequency range with the mouse.
|
- **Region selection** — box a time *and* frequency range with the mouse.
|
||||||
- **Filtered playback**: play just the selected region, band-limited to the
|
- **Filtered playback** — play just the selected region, band-limited to the
|
||||||
selected frequency box (FFT bandpass — "what you hear is what you'd export").
|
selected frequency box via an FFT bandpass. What you hear is what you'd export.
|
||||||
- **Waveform scope** (toggle) showing the time-domain signal under the spectrum.
|
- **Waveform scope** — toggleable time-domain view beneath the spectrum.
|
||||||
- **Marker / ruler tool** and a **spectrum slice (PSD)** readout.
|
- **Marker / ruler** and a **spectrum slice (PSD)** readout.
|
||||||
- **Export**: save the view as a **PNG**, or the selected region as a **WAV**.
|
- **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
|
- **Headless render mode** — produce an annotated PNG from the CLI with no
|
||||||
no window, no GL, and no X server — pure CPU, runs anywhere (see below).
|
window, no GL, and no X server. Pure CPU; runs in CI, containers, or over SSH.
|
||||||
- **Broad input support**: WAV directly (8/16-bit PCM, 32-bit float; stereo
|
- **Broad input** — WAV directly (8/16-bit PCM, 32-bit float; stereo downmixed to
|
||||||
downmixed to mono); other formats transparently transcoded via `ffmpeg` if it
|
mono); other formats transcoded via `ffmpeg` if it's on `PATH`. Drag-and-drop.
|
||||||
is on `PATH`. Drag-and-drop loading.
|
- **Cross-platform** — Linux/desktop, Windows, and a WebAssembly build.
|
||||||
- **Cross-platform**: Linux/desktop, Windows, and a WebAssembly build.
|
|
||||||
|
---
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
The build is driven by a checked-in Makefile (premake5 is **not** required to
|
The build is driven by a checked-in Makefile. `premake5` is **not** required to
|
||||||
build — only to regenerate the makefiles):
|
build — only to regenerate the makefiles.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make -f rspektrum.make config=debug_x64 # -> bin/Debug/rspektrum
|
make -f rspektrum.make config=debug_x64 # -> bin/Debug/rspektrum
|
||||||
make -f rspektrum.make config=release_x64 # -> bin/Release/rspektrum
|
make -f rspektrum.make config=release_x64 # -> bin/Release/rspektrum
|
||||||
```
|
```
|
||||||
|
|
||||||
The web build:
|
Web (WebAssembly) build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build_web.sh # emscripten; emits the WebAssembly bundle
|
./build_web.sh # emscripten; emits the WebAssembly bundle
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: the release build enables `-O2`, which turns on extra warnings
|
> The release build enables `-O2`, which turns on extra warnings
|
||||||
> (`-Wformat-truncation`) that the debug build doesn't — build release before
|
> (`-Wformat-truncation`) that the debug build doesn't. Build release before
|
||||||
> declaring a change clean.
|
> declaring a change clean.
|
||||||
|
|
||||||
## Running (GUI)
|
---
|
||||||
|
|
||||||
|
## Usage (desktop GUI)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./bin/Debug/rspektrum [input.wav]
|
./bin/Debug/rspektrum [input.wav]
|
||||||
```
|
```
|
||||||
|
|
||||||
Load a file by passing it on the command line, dragging a `.wav` onto the window,
|
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
|
### Controls
|
||||||
|
|
||||||
@@ -90,15 +107,17 @@ or pressing **O** for the file browser.
|
|||||||
| **F1** | About / help |
|
| **F1** | About / help |
|
||||||
| **Esc** | Clear selection / close dialog |
|
| **Esc** | Clear selection / close dialog |
|
||||||
|
|
||||||
Most controls are also available as buttons in the left sidebar (colormap,
|
Most controls are also available as buttons in the left sidebar (colormap, floor,
|
||||||
floor, dynamic range, annotation opacity, grid, etc.).
|
dynamic range, annotation opacity, grid, …).
|
||||||
|
|
||||||
## Headless rendering (CLI)
|
---
|
||||||
|
|
||||||
|
## Usage (headless render)
|
||||||
|
|
||||||
`--render` writes the spectrogram straight to a PNG **with no window, no GL
|
`--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
|
context, and no X server**. It computes the STFT, colorizes the bitmap, bakes the
|
||||||
the annotation overlay onto it, and exports, all on the CPU. This runs anywhere
|
annotation overlay onto it, and exports — all on the CPU — so it runs anywhere
|
||||||
(CI, a bare SSH session, a container with no display), not just under Xvfb:
|
(CI, a bare SSH session, a container with no display):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./bin/Debug/rspektrum --render OUT.png INPUT.wav [options]
|
./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
|
screenshot of the UI), so it carries no sidebar/scope chrome — just the
|
||||||
time–frequency image with the annotation overlay.
|
time–frequency image with the annotation overlay.
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
| Flag | Effect |
|
| Flag | Effect |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `-r, --render OUT.png` | Render to `OUT.png` and exit (no window/GL/X) |
|
| `-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
|
Annotation boxes are drawn **outline + label only** (no translucent fill): mLnL
|
||||||
captures contain many overlapping full-band boxes whose fills would alpha-stack
|
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
|
to opaque and bury the signal, so the outline marks each region while the
|
||||||
spectrogram reads through. `--annotation-opacity` controls outline/label
|
spectrogram reads through.
|
||||||
strength. Filter to just the kinds you care about with `--annotation-kinds`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# everything, brighter overlay
|
# 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
|
--annotation-kinds=tx_frame,assertion_failed
|
||||||
```
|
```
|
||||||
|
|
||||||
Kinds: `tx_frame`, `tx_burst`, `control`, `channel_up`, `channel_down`,
|
Annotation kinds: `tx_frame`, `tx_burst`, `control`, `channel_up`,
|
||||||
`assertion_passed`, `assertion_failed`, `impairment_fire`, `gain_change`,
|
`channel_down`, `assertion_passed`, `assertion_failed`, `impairment_fire`,
|
||||||
`unknown`. `mlnl_samples.wav` is an in-repo WAV that carries an embedded `mLnL`
|
`gain_change`, `unknown`.
|
||||||
chunk.
|
|
||||||
|
|
||||||
> The hover tooltip (sched offset, per-frame detail) only appears with a live
|
> The hover tooltip only appears with a live mouse over a box, so it cannot show
|
||||||
> mouse over a box, so it cannot show up in a static `--render`. To verify
|
> up in a static `--render`. To verify tooltip behaviour you need a real (or
|
||||||
> tooltip behaviour you need a real (or virtual) display driving the GUI — see
|
> virtual) display driving the GUI — see below.
|
||||||
> 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
|
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
|
[`AGENTS.md`](AGENTS.md); the working reference implementation is
|
||||||
[`shot_input.sh`](shot_input.sh).
|
[`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`
|
Prerequisites (Debian/Ubuntu): `sudo apt-get install xvfb imagemagick xdotool`
|
||||||
(plus `libgl1-mesa-dri` and `LIBGL_ALWAYS_SOFTWARE=1` if GL fails / frames are
|
(plus `libgl1-mesa-dri` and `LIBGL_ALWAYS_SOFTWARE=1` if GL fails / frames are
|
||||||
black). To exercise UI paths, synthesize input with `xdotool` against
|
black). Synthesize input with `xdotool` against `DISPLAY=:99` to exercise UI
|
||||||
`DISPLAY=:99` — e.g. move the mouse over an annotation box and re-grab to capture
|
paths.
|
||||||
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
|
## Technical notes
|
||||||
|
|
||||||
- **STFT**: Hann-windowed, 2048-point FFT with 50% overlap by default;
|
- **STFT** — Hann-windowed, 2048-point FFT with 50% overlap by default;
|
||||||
frequency resolution `sampleRate / fftSize` Hz per bin. Amplitude shown in dB.
|
frequency resolution `sampleRate / fftSize` Hz per bin. Amplitude in dB.
|
||||||
- **Axes**: X = time (s), Y = frequency (Hz, scaled to the file's Nyquist),
|
- **Axes** — X = time (s), Y = frequency (Hz, scaled to the file's Nyquist),
|
||||||
colour = amplitude.
|
colour = amplitude.
|
||||||
- **Playback / WAV export** share the same processing path: the selected time
|
- **Playback / WAV export** share one processing path: the selected time span,
|
||||||
span, FFT-bandpassed to the selected frequency box, peak-normalised.
|
FFT-bandpassed to the selected frequency box, peak-normalised.
|
||||||
- **mLnL parsing**: walks the WAV's RIFF chunks for the four-CC `mLnL` chunk
|
- **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
|
(UTF-8 JSON Lines); unknown chunks are skipped, so annotated files stay
|
||||||
standards-compliant audio everywhere else.
|
standards-compliant audio everywhere else.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Source layout
|
## Source layout
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -207,4 +213,5 @@ src/
|
|||||||
```
|
```
|
||||||
|
|
||||||
See [`raylib_for_desktop_applications.md`](raylib_for_desktop_applications.md)
|
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