ac262505c1
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>
99 lines
3.8 KiB
C
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
|