Files
rspektrum/src/mlnl.h
T
tyler ac262505c1 feat: headless PNG render, mLnL annotations, and per-frame sched offset
Commit the accumulated working-tree changes as one snapshot.

- Headless render: `--render OUT.png INPUT.wav` draws the spectrogram
  (full window, or `--pane` for the spectrogram pane only) to a PNG
  with no visible window. Options: `--annotations`/`--no-annotations`,
  `--annotation-opacity`, `--width`/`--height`.
- mLnL annotations: parse the optional `mLnL` RIFF chunk (schema v2)
  and render tx_frame/assertion/control overlays, a timeline lane, and
  a waveform-scope echo, with hover tooltips on the spectrogram,
  timeline, and scope.
- sched_offset_ms: parse the per-frame intent->air latency and surface
  it in the hover tooltips (boxes stay air-anchored upstream).
- Supporting: build wiring (rspektrum.make), shared types/headers,
  web-build and capture-script tweaks, and removal of the old
  synchrosqueezing LaTeX doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 12:19:37 -07:00

99 lines
3.8 KiB
C

// mlnl.h - mLnL annotation chunk parser (schema v2).
// Walks a WAV file's RIFF chunks looking for the optional `mLnL` chunk
// (UTF-8 JSON Lines), parses each line into an MlnlEvent. Every event is a
// self-contained time range [t_start, t_end] (instantaneous events have
// t_start == t_end). See mlnl_chunk_spec.md.
#ifndef MLNL_H
#define MLNL_H
#include <stdbool.h>
typedef enum {
MLNL_KIND_UNKNOWN = 0,
MLNL_KIND_CHANNEL_UP,
MLNL_KIND_CHANNEL_DOWN,
MLNL_KIND_CONTROL,
MLNL_KIND_TX_FRAME, // v2 primary annotation: one daemon-sourced on-air frame
MLNL_KIND_TX_BURST, // deprecated, energy-detected guess; superseded by tx_frame
MLNL_KIND_ASSERTION_PASSED,
MLNL_KIND_ASSERTION_FAILED,
MLNL_KIND_IMPAIRMENT_FIRE,
MLNL_KIND_GAIN_CHANGE,
} MlnlKind;
typedef struct {
// Required.
double t_start, t_end;
MlnlKind kind;
char kindStr[32]; // original "kind" string (for unknown kinds + display)
// Frequency annotation (resolved from f_lo/f_hi OR f_center/f_bw).
double f_lo_hz, f_hi_hz;
bool has_freq;
// Optional human/visual hints.
char note[96];
bool has_note;
unsigned char colorR, colorG, colorB; // parsed from "#RRGGBB"
bool has_color;
// Kind-specific fields. The has_* flag says whether the producer set it.
unsigned int node;
bool has_node;
char command[64];
bool has_command;
char name[128]; // assertion name
bool has_name;
char reason[96]; // assertion_failed reason
bool has_reason;
double peak, rms, papr_db; // tx_burst
bool has_stats;
double slack_s; // assertion_passed
bool has_slack;
unsigned int id; // tx_burst: monotonic per-wav identifier
bool has_id;
char label[32]; // tx_burst: optional frame tag (SEED/BULK/ACK/...)
bool has_label;
// tx_frame fields (schema v2 primary annotation). `node`/`has_node` above
// carry the transmitting node_id (attribution + color).
int seq, nFrames; // 0-based index + total frames in the PTT ("seq of n")
bool has_seqn; // set once `n` is present
char frame[32]; // mLink frame name (PRESENCE/SEED, BULK, BULK/RTS, ...)
bool has_frame;
char ch[16]; // daemon channel (EMERGENCY / ANNOUNCE_0|1|2 / BULK)
bool has_ch;
char rate[8]; // LDPC rate of a bulk OFDM frame ("1/2".."5/6"); else empty
bool has_rate;
// intent->air latency of this burst: air_time - modem render/intent time.
// The producer already anchors t_start/t_end on the ACTUAL on-air edge, so
// this is a profiling metric ("modem wants vs. actually does"), not a
// plotting offset. Coarse/block-quantized. See mlnl_chunk_spec.md §4.2.
double sched_offset_ms;
bool has_sched_offset;
} MlnlEvent;
// Up to this many distinct MlnlKind values are tracked per-file (any new kinds
// added to the enum must bump this). Indexed by the MlnlKind value.
#define MLNL_KIND_MAX 16
typedef struct {
MlnlEvent* events;
int eventCount;
bool truncated; // {"truncated":true} sentinel appeared
bool loaded; // true iff the file contained a parseable mLnL chunk
bool kindPresent[MLNL_KIND_MAX]; // which kinds the parser saw (drives the UI)
} MlnlAnnotations;
// Walk `path`'s RIFF chunks looking for `mLnL` and parse the JSONL payload.
// Returns true on success (out->loaded is set the same way). On failure
// (no chunk / unreadable / not WAV) returns false and zeroes *out.
bool LoadMlnlFromWav(const char* path, MlnlAnnotations* out);
void FreeMlnl(MlnlAnnotations* a);
// Display label for an MlnlKind.
const char* MlnlKindName(MlnlKind k);
#endif // MLNL_H