work / aurascope / step 04

STEP 04 · TAG v0.4-dashboard

Streaming, and a scope to watch it with

// phase 2 · audio into linux · status: done, running on the board · published [date]

The goal of this step

Step 03 ended with the receiver enumerated as ALSA card 0. That proves the kernel found the device. It says nothing about whether audio is actually arriving, or whether what arrives is correct. This step closes that gap — and does it with something I can leave running and glance at, rather than a one-shot command.

The result is aurascope-dash: a small C program on the board that captures from ALSA, computes levels and a spectrum, and serves a live dashboard over HTTP. Two views, which answer two different questions:

Why arecord isn't enough

arecord -d 5 test.wav proves bytes moved. It does not tell you the signal is right, and it can't be watched while you change something at the other end of the link. Most of what I actually wanted to know is temporal: does the level hold steady, does it drop out when I walk away from the transmitter, does the noise floor sit where it should.

The distinction that made this worth building: a dropout lasting 40 ms is completely invisible in a waveform frame — the frame is only about 10 ms wide, so the gap either fills the whole view or has already scrolled past. In a 30-second RMS trace the same dropout is an obvious notch. The instantaneous view tells you what the signal is; the history tells you what it has been doing. Neither substitutes for the other, which is why both are on the page.

Capturing the audio

audio.c opens the PCM device and runs a detached capture thread. The parameters are deliberately unambitious — this is a monitor, not a recorder:

ParameterValueWhy
Rate48000 HzMatches the Auracast/LC3 chain end to end, so nothing resamples.
FormatSND_PCM_FORMAT_S16_LEWhat the UAC device presents; no conversion needed.
Channels2 (stereo)The Auracast side broadcasts one BIS per channel, so left and right arrive over separate streams and are worth watching separately.
Period512 frames≈10.7 ms — fast enough to feel live, large enough that the wakeup rate is not a burden on a Cortex-A9.
FFT_SIZE1024≈47 Hz per bin at 48 kHz.
WAVE_POINTS256Waveform decimated by stride = FFT_SIZE / WAVE_POINTS.
SPEC_BARS64Bins folded down to something a browser can draw legibly.

What gets computed each period

RMS and peak are the two numbers that matter, both in dB relative to full scale, with the + 1e-9f guard that stops digital silence turning into -inf:

rms = sqrtf((float)(sq_sum / (double)n));
rms_db  = 20.0f * log10f(rms  + 1e-9f);
peak_db = 20.0f * log10f(peak + 1e-9f);

Peak alone is too twitchy to read on a moving meter, so there's a separate held value that jumps instantly to a new maximum and then bleeds away at peak_hold_db -= 0.3f per period — about 28 dB per second. Fast enough to follow real changes, slow enough that a transient stays on screen long enough to see.

The spectrum is a 1024-point FFT with a Hann window applied first (fft_hann_window()). The window is not optional: an FFT implicitly assumes the block repeats forever, and a sine that doesn't fit a whole number of cycles into the block produces a discontinuity at the seam. Untapered, that smears energy across every bin — a single clean tone comes out looking like broadband noise. Hann tapers both ends to zero and the tone lands where it belongs.

Getting it to a browser

Everything lands in one audio_snapshot_t guarded by a mutex — the capture thread writes it, the HTTP thread reads a consistent copy:

typedef struct {
    long  ts_ms;
    float rms_db;
    float peak_db;
    float peak_hold_db;
    float wave[WAVE_POINTS];
    float spec[SPEC_BARS];
} audio_snapshot_t;
As committed, that struct is the mono version — one waveform array. The stereo split described below carries a waveform per channel; the spectrum stays single. That change is still local at the time of writing, so the struct above is what is currently in the repo rather than what is currently on the board.

The transport is Server-Sent Events over a hand-rolled HTTP server, which is two deliberate choices. SSE rather than WebSockets because the data only ever travels one way: SSE is plain HTTP with a text/event-stream content type and newline-delimited frames, needs no handshake or framing library, and the browser reconnects on its own when the board reboots. And hand-rolled rather than pulling in a web server, because adding a package to an embedded image to serve one static page and one event stream is not a trade worth making.

Frames go out at roughly 10 Hz — decimated from the ~94 Hz capture period, since nothing on screen benefits from being redrawn faster than the eye resolves.

The two views

The client is a single index.html drawing to Canvas off the EventSource stream. No framework, no build step, no CDN — it has to work on a board that may have no route to the internet.

Instantaneous: a waveform per channel normalised to ±1, the spectrum on a −90 dB to 0 dB scale, and a vertical level meter spanning −60 dB to 0 dB with the RMS as a gradient fill and the held peak as a red line above it.

The last 30 seconds: a rolling chart holding HISTORY_LEN = 300 samples — at ~10 Hz, exactly 30 seconds — plotting RMS in green against peak in red, on a 0 to −60 dB scale. This is the panel that catches the problems worth catching. A steady pair of lines means the link is healthy; a notch means something dropped.

Why left and right get their own chart

Splitting the waveform into two panels looks like a presentation choice. It isn't — it follows the shape of the transport underneath.

Stereo here is two independent radio streams, not one. The gateway broadcasts a separate Broadcast Isochronous Stream per channel — channel allocation 0x1 and 0x2 — and the receiver syncs to both, which is why it reports the stream bitfield as 0x0003. Left and right therefore travel over the air separately and can fail separately. Averaged into one trace, a channel dropping out shows up as a mild dip that looks like a quiet passage. Drawn side by side, it is unmissable: one panel keeps moving and the other flatlines. That single glance rules out a whole class of fault — and points at one BIS rather than the whole link.
The AuraScope live scope showing two waveform panels side by side, labelled waveform L in cyan and waveform R in orange, both displaying the same dense complex audio signal with bursts of activity that decay to a flat line at the right-hand end. Below them a single spectrum panel shows 64 blue bars, broadly level across the lower and middle bars and dropping sharply across the last few.
Real program material through the whole chain — microphone, LC3, Auracast, USB, ALSA — with the two channels drawn separately.

Packaging it

The whole thing is a normal Yocto recipe rather than something scp'd onto the board by hand, so it survives a reflash and ships with the image. The recipe's own summary is the honest one-line description of the program:

# meta-aurascope/recipes-apps/aurascope-dash/aurascope-dash_1.0.bb

SUMMARY = "Captures ALSA audio, computes RMS/peak/FFT, and serves a \
Canvas + Server-Sent-Events dashboard over a hand-rolled HTTP server."

An init script starts it at boot, so the dashboard is up as soon as the board is:

$ /etc/init.d/aurascope-dash status
$ curl http://<board-ip>:8080/

Result

Audio from the nRF5340 gateway's microphone travels over Auracast, decodes on the receiver, crosses USB into ALSA on the Zynq, and is visible in a browser as a live waveform, spectrum and level trace. That's the wireless-to-Linux path proven end to end, not inferred from an enumeration message.

It also builds the thing the rest of the project needs. Phase 3 is about observing this audio path two different ways — and both of those need somewhere to display their output. That somewhere now exists, with a live data feed already plumbed into it.

Files

PathWhat it does
newrecipes-apps/aurascope-dash/aurascope-dash_1.0.bbThe recipe — builds and installs the daemon
new.../files/aurascope-dash.initStarts it at boot
new.../files/audio.c, audio.hALSA capture thread, RMS/peak/hold, snapshot struct
new.../files/fft.c, fft.h1024-point FFT and Hann window
new.../files/http.c, http.hHand-rolled HTTP server and the SSE feed
new.../files/index.htmlCanvas dashboard — the whole client
new.../files/main.c, MakefileStartup and build
changedrecipes-core/images/aurascope-image.bbAdds aurascope-dash to the image

Code

Commit e1e1bfe · code: aurascope-dash