Status: Draft
Version: 3.1
Scope: fm-rds-tx runtime, control API, Overview UI, Flow UI, diagnostics
fm-rds-tx already exposes a good operational control plane: config state, runtime state, queue health, underruns, transitions, fault history, and a flow-oriented UI concept. What is still missing is true runtime broadcast-style signal metering for the multiplex chain itself.
Right now, the UI mostly shows:
Those values are useful, but they describe intent, not measured runtime signal behavior.
The goal of this concept is to turn the Composite / MPX area into a real metering surface by measuring several semantically distinct points in the DSP chain and exposing the results through a dedicated high-frequency measurements endpoint.
The current UI and API are good at showing:
But they are not yet good at showing what the multiplex signal is actually doing.
The current Composite / MPX presentation is still mostly a config summary, not a measured signal view.
Without actual runtime metering, an operator cannot quickly answer questions like:
A single config snapshot cannot answer those questions.
Broadcast-style operation needs more than transport/runtime health. The system should expose enough internal signal truth that the operator can understand:
Turn the Composite / MPX area of fm-rds-tx into a true runtime metering surface that reflects measured DSP behavior instead of only saved configuration values.
/runtime polling cost.The UI should reflect measured runtime signal values at key DSP stages, not only configured values.
The system should make visible how these stages interact:
The operator should be able to distinguish:
The UI should help answer:
Metering should map directly onto the existing GenerateFrame() signal flow so the system stays semantically clean.
internal/offline/generator.go.Before implementation, the following semantics should be corrected or clarified:
There is an optional STFT watermark step in the code path after the first-pass audio processing and before stereo encoding.
That means a tap named simply lr_post_processing is ambiguous:
For MVP, the more useful semantic choice is:
This makes the L/R meter describe the actual signal that goes into stereo encoding.
This was the biggest conceptual risk in the first draft.
A raw measured RMS of the pilot component is not the same thing as an operator-facing injection display value such as “pilot 9 %”.
Similarly, an RDS energy measurement is not identical to a familiar “RDS 4 %” display concept.
Therefore the API must separate:
pilotRms, pilotPeakAbs, rdsRms, rdsPeakAbspilotInjectionEquivalentPercent and rdsInjectionEquivalentPercentThe UI must never imply that RMS itself equals injection percent.
Names like over100Events and over110Events are too suggestive and invite wrong interpretation as if they were legal overmodulation indicators.
They are not.
For MVP they should use semantically safer names such as:
overNominalEventsoverHeadroomEventsAnd the docs must state clearly that they are based on internal normalized composite thresholds, not legal overmodulation judgement.
A true composite clipper / protection stage is not always semantically equivalent to simple hard clipping.
So the concept should avoid overpromising exact clipEvents semantics at the composite-clipping tap.
Safer metrics are:
peakAbscrestFactorclipperOrProtectionActiveThis avoids lying with names.
The BS.412 path in the current code behaves as a chunk-based running control state.
That is fine.
But UI and docs must describe bs412GainApplied accordingly:
The final composite may include more than just audio MPX + pilot + RDS.
It can also include:
So the measurement snapshot should carry flags such as:
stereoEnabledstereoModerdsEnabledrds2Enabledbs412EnabledcompositeClipperEnabledwatermarkEnabledlicenseInjectionActiveThat gives the UI enough context to interpret the meters correctly.
clipperOrProtectionActive is acceptable only if its derivation is explicitly defined.
Otherwise it becomes a soft, marketing-like boolean.
Preferred approach:
clipperLookaheadGain and clipperEnvelopelicenseInjectionActive should not mean merely that a license/jingle feature exists.
It should mean:
That is the operator-meaningful interpretation.
This field remains useful, but it must be mathematically defined before implementation.
The concept should not leave open whether it means:
The exact derivation needs to be fixed before Phase 1 code lands.
One final composite measurement is not enough.
A single final reading cannot tell the operator whether a change came from:
Also:
So the design must sample multiple semantically distinct points.
Effective left/right program audio immediately before stereo encoding.
internal/offline/generator.go → GenerateFrame()
After optional watermark has been added back into lBuf / rBuf, before stereo encoding begins.
This is the best operator-facing L/R meter because it reflects the actual signal entering the stereo encoder, even when watermarking is active.
lRmsrRmslPeakAbsrPeakAbslrBalanceDblClipEventsrClipEventsIf later needed, an additional pre-watermark engineering tap can be added separately. It should not replace this main operator-facing tap.
Audio-only multiplex after stereo encode and composite clipper/protection, but before BS.412 gain reduction.
internal/offline/generator.go → GenerateFrame()
Right after audioMPX has been built and processed by:
and before bs412Gain is applied.
This is the best place to see what the multiplex audio path is structurally producing before compliance shaping.
rmspeakAbsmonoRmsstereoRmscrestFactorclipperLookaheadGainclipperEnvelopeclipperOrProtectionActiveAudio-only multiplex after BS.412 gain reduction and before fixed protected components are added.
internal/offline/generator.go → GenerateFrame()
Immediately after:
audioMPXand before:
This is the operator’s compliance view of the audio multiplex path.
It answers:
rmspeakAbsbs412GainAppliedbs412AttenuationDbestimatedAudioPowerbs412GainApplied is a running chunk-level control value, not a sample-instantaneous causal diagnosis.
The actual final composite signal immediately before FM modulation or direct composite output.
internal/offline/generator.go → GenerateFrame()
At the final composite variable just before:
fmMod.Modulate(composite)This is the most operator-relevant metering point.
It reflects the actual final multiplex including:
rmspeakAbspilotRmspilotPeakAbspilotInjectionEquivalentPercentrdsRmsrdsPeakAbsrdsInjectionEquivalentPercent (TODO until mathematically defined; omit or expose as null in MVP if not yet specified)overNominalEventsoverHeadroomEventsComposite / MPX nodeThe percent-style fields are derived display values and must stay explicitly separate from the raw RMS fields.
rdsInjectionEquivalentPercent must not be implemented until its mathematical derivation is explicitly documented.
Chunk-local RMS for the signal component.
Maximum absolute sample magnitude within the chunk.
A derived operator-facing value that expresses the component in a deviation-/injection-style display form. It is not identical to RMS.
Use only where the tapped stage really has meaningful hard-threshold clipping semantics.
A user-facing gain-reduction metric derived from pre/post behavior.
A chunk-local operator metric for compliance visibility, not a certification-grade legal measurement.
Internal normalized composite-threshold counters. These are not legal overmodulation indicators.
Useful where shape/protection stages do not map cleanly to simple clip-event semantics.
Suggested top-level Go type:
MeasurementSnapshotSuggested structure:
flagslrPreEncodePostWatermarkaudioMpxPreBs412audioMpxPostBs412compositeFinalPreIqImportant internal consistency rule:
clipperLookaheadGain and clipperEnvelope, not only clipperOrProtectionActiveSuggested top-level metadata:
timestampsampleRateHzchunkSampleschunkDurationMssequencestalenoDataSuggested flags:
stereoEnabledstereoModerdsEnabledrds2Enabledbs412EnabledcompositeClipperEnabledwatermarkEnabledlicenseInjectionActiveThis should be stable enough that:
Use chunk-local accumulation during one GenerateFrame() call.
For each relevant tap, accumulate:
At the end of the chunk:
The generator should own measurement production because that is where the semantically rich intermediate signals exist.
The generator already sees:
If measurement is delayed until engine or driver level, too much stage identity is lost.
Preferred model:
GET /measurementsDo not overload /runtime for fast metering.
Reason:
/runtime is broad operational telemetry4 Hz5 Hz10 HzWhen TX is inactive, either:
noData=true, orstale=truePreferred MVP behavior:
noData=true when there is no current meaningful signal snapshotAdd or extend a metering section for:
Important:
Today it is mostly config-derived.
Target:
stereoMode context when non-DSB stereo operation changes expectationsShow:
Later, add a dedicated engineering meter view that can show:
sqrt and log at chunk endDeliverables:
MeasurementSnapshotflagsGenerateFrame()Deliverables:
GET /measurementsdocs/API.mdDeliverables:
/measurements separately at 4–10 HzComposite / MPX from measurement dataDeliverables:
This concept is not:
Should pilot and RDS display percentages be derived from:
Current preference:
Should internal thresholds be stored as:
Current preference:
Should /measurements include history arrays?
Current preference:
Should long-term ownership remain in generator or later move into engine stats?
Current preference:
Should clipperOrProtectionActive exist as a first-class field or be derived from raw clipper diagnostics?
Current preference:
clipperLookaheadGain and clipperEnvelopeShould a separate pre-watermark engineering tap exist later?
Current preference:
Implement Phase 1 first.
Concrete first steps:
MeasurementSnapshot, nested structs, flags, and stereoMode.GenerateFrame() at:
lr_pre_encode_post_watermarkaudio_mpx_pre_bs412audio_mpx_post_bs412composite_final_pre_iqrdsInjectionEquivalentPercent mathematically before exposing it.licenseInjectionActive as chunk-local actual activity, not feature presence.That gets the measurement semantics right first. API and UI can then be built on top of a stable internal model.
Two implementation guardrails are worth stating explicitly:
Raw clipper diagnostics must not disappear from the actual shape.
If the concept prefers clipperLookaheadGain and clipperEnvelope, then the data model must contain them. It is not sufficient to mention them in examples while leaving only clipperOrProtectionActive in the formal shape.
rdsInjectionEquivalentPercent must not be guessed into existence.
Until its derivation is mathematically fixed, MVP should treat it as:
null, orAnything else risks permanently baking an arbitrary-looking formula into the API.