Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

632 wiersze
27KB

  1. {
  2. "concept": "fm-rds-tx composite/mpx live metering",
  3. "version": 3.1,
  4. "status": "draft",
  5. "summary": "Introduce true runtime broadcast-style metering for the FM stereo/RDS multiplex chain by measuring multiple semantically distinct DSP stages and exposing them through a dedicated measurements endpoint for Overview, Flow, and diagnostics UI surfaces.",
  6. "problemStatement": {
  7. "currentState": [
  8. "The current UI mostly exposes configured MPX-related values such as pilotLevel, rdsInjection, mpxGain, BS.412 state, and composite clipper state.",
  9. "These values describe intended configuration but not the actual measured runtime behavior of the multiplex chain.",
  10. "The current Flow Composite / MPX node is therefore closer to a config summary than to a true live broadcast metering surface.",
  11. "Runtime telemetry currently focuses on transport and engine health such as queue health, underruns, runtime states, and fault transitions, but not on actual composite/MPX signal composition."
  12. ],
  13. "operationalGap": [
  14. "Operators cannot currently see how audio processing, stereo encoding, composite clipping, BS.412, pilot injection, and RDS injection interact on the live signal.",
  15. "Without live stage-aware metering, it is difficult to identify whether signal behavior is caused by audio processing, compliance shaping, or final composite assembly.",
  16. "A single config snapshot is not enough to answer whether the transmitted multiplex is nominal, compliance-limited, overdriven, missing pilot, or missing RDS."
  17. ]
  18. },
  19. "objective": {
  20. "primary": "Turn the Composite / MPX area of fm-rds-tx into a true runtime metering surface that reflects measured DSP behavior rather than only saved configuration values.",
  21. "secondary": [
  22. "Give operators a trustworthy view of what the DSP chain is actually producing at critical stages.",
  23. "Separate audio-path behavior, BS.412 compliance behavior, and final composite/on-air behavior into distinct measured views.",
  24. "Support a richer Overview and Flow UI without increasing full-runtime polling cost.",
  25. "Create a stable foundation for future alerts, engineering diagnostics, and signal-quality workflows."
  26. ]
  27. },
  28. "whatWeAreTryingToAchieve": [
  29. {
  30. "goal": "Show real measured multiplex behavior",
  31. "description": "Measure actual runtime signal values at key DSP stages so the UI reflects what is really happening in the chain, not only what was configured."
  32. },
  33. {
  34. "goal": "Explain stage interactions",
  35. "description": "Make visible how processing, stereo encoding, composite clipping, BS.412, pilot injection, and RDS injection each affect the final multiplex."
  36. },
  37. {
  38. "goal": "Separate compliance and on-air views",
  39. "description": "Provide distinct views before BS.412, after BS.412, and at the final composite point directly before FM modulation."
  40. },
  41. {
  42. "goal": "Support operator decisions",
  43. "description": "Allow operators to quickly assess whether the system is nominal, overdriven, compliance-limited, missing pilot/RDS, or behaving unexpectedly."
  44. },
  45. {
  46. "goal": "Stay aligned with the actual DSP architecture",
  47. "description": "Add metering in a way that matches the current GenerateFrame() signal flow and preserves semantic clarity between stages."
  48. }
  49. ],
  50. "expectedOutcome": {
  51. "overviewUi": [
  52. "Overview can show measured L/R and final MPX/composite values instead of only static configuration values.",
  53. "Pilot and RDS indicators become measurement-backed rather than purely declarative."
  54. ],
  55. "flowUi": [
  56. "The Composite / MPX node becomes a true live-status node.",
  57. "The Flow popover can compare pre-BS.412, post-BS.412, and final pre-IQ behavior."
  58. ],
  59. "diagnostics": [
  60. "Engineers can determine whether a problem is introduced before BS.412, by BS.412, or only in the final composite stage."
  61. ],
  62. "futureWork": [
  63. "The same model can support alerts, trends, compliance heuristics, and richer engineering telemetry later."
  64. ]
  65. },
  66. "designPrinciples": [
  67. "Meter measured runtime signal states, not only configured target values.",
  68. "Keep measurement taps aligned with actual DSP semantics already present in generator.GenerateFrame().",
  69. "Separate audio-path compliance measurements from final composite/on-air measurements.",
  70. "Expose a dedicated faster measurements endpoint instead of raising full UI polling frequency.",
  71. "Provide a stable and explicit measurement model that can serve UI, diagnostics, and future alerting.",
  72. "Use chunk-level aggregation, not per-sample external reporting.",
  73. "Keep measurement logic low-overhead and safe for the hot path.",
  74. "Prefer semantically conservative field names over catchy but ambiguous meter labels."
  75. ],
  76. "whyMultipleTaps": [
  77. "A single final composite reading cannot explain whether a change came from L/R processing, composite clipping, BS.412 gain reduction, or fixed pilot/RDS injection.",
  78. "BS.412 acts on the audio MPX budget, not on pilot/RDS directly, so pre-BS.412 and post-BS.412 views are semantically different.",
  79. "Pilot and RDS become meaningful only in the final composite stage, so they require final pre-IQ measurement.",
  80. "Operator-facing UI needs both an audio/compliance view and a final on-air-adjacent composite view."
  81. ],
  82. "semanticAdjustmentsFromReview": [
  83. "The L/R tap must account for optional watermark insertion between first-pass audio processing and stereo encoding.",
  84. "Pilot and RDS percent-style fields must not conflate RMS measurements with injection-equivalent display values.",
  85. "Composite threshold counters must avoid overmodulation-suggestive names unless their semantics are explicitly regulatory, which they are not.",
  86. "Composite clipper-related metrics should avoid claiming hard clip event semantics where the implementation is actually shaping/protection-oriented.",
  87. "BS.412 gain must be described as a chunk-level running control state, not a sample-exact causal response.",
  88. "Final-composite interpretation should expose feature flags such as watermark, RDS2, and license injection activity.",
  89. "stereoMode should be exposed alongside stereoEnabled so non-DSB operation remains interpretable in diagnostics and UI.",
  90. "licenseInjectionActive must mean chunk-local actual activity, not merely that the feature exists.",
  91. "rdsInjectionEquivalentPercent must be mathematically defined as a derived operator-facing quantity before implementation begins."
  92. ],
  93. "measurementTaps": [
  94. {
  95. "id": "lr_pre_encode_post_watermark",
  96. "label": "L/R Pre-Encode Post-Watermark",
  97. "stage": "after pre-emphasis, audio LPF, pilot notch, output drive, stereo limiter, cleanup LPF, final L/R hard clipping, and optional watermark injection; immediately before stereo encoding",
  98. "locationInCode": {
  99. "file": "internal/offline/generator.go",
  100. "function": "GenerateFrame",
  101. "region": "after optional watermark has been added back into lBuf/rBuf and before stereo encoding pass"
  102. },
  103. "semanticPurpose": [
  104. "Represents the effective left/right program audio that actually enters the stereo encoder.",
  105. "Avoids ambiguity when watermarking is enabled."
  106. ],
  107. "primaryUseCases": [
  108. "Overview L/R meters",
  109. "Channel imbalance visibility",
  110. "Audio processing inspection"
  111. ],
  112. "metrics": [
  113. "l_rms",
  114. "r_rms",
  115. "l_peak_abs",
  116. "r_peak_abs",
  117. "lr_balance_db",
  118. "l_clip_events",
  119. "r_clip_events"
  120. ],
  121. "notes": [
  122. "If a future pre-watermark tap is desired for engineering comparison, it should be added as a separate optional diagnostic tap rather than overloading this one."
  123. ],
  124. "required": true
  125. },
  126. {
  127. "id": "audio_mpx_pre_bs412",
  128. "label": "Audio MPX Pre-BS.412",
  129. "stage": "after stereo encode and composite clipper/protection, before BS.412 gain is applied",
  130. "locationInCode": {
  131. "file": "internal/offline/generator.go",
  132. "function": "GenerateFrame",
  133. "region": "after audioMPX is finalized from mono/stereo and after composite clipper or legacy clip/notch path, before bs412Gain multiplication"
  134. },
  135. "semanticPurpose": [
  136. "Represents the raw audio-only multiplex signal before BS.412 compliance shaping.",
  137. "Shows what the multiplex audio path is structurally producing."
  138. ],
  139. "primaryUseCases": [
  140. "Raw MPX level view",
  141. "Comparison against post-BS.412 attenuation",
  142. "Explaining why final loudness changed"
  143. ],
  144. "metrics": [
  145. "audio_mpx_rms",
  146. "audio_mpx_peak_abs",
  147. "mono_component_rms",
  148. "stereo_component_rms",
  149. "crest_factor",
  150. "clipper_lookahead_gain",
  151. "clipper_envelope",
  152. "clipper_or_protection_active"
  153. ],
  154. "notes": [
  155. "Avoid promising exact hard clip event semantics here when the active implementation may be a true composite clipper/protection stage rather than simple hard clipping.",
  156. "If clipper_or_protection_active is exposed, it must be derived from a documented condition. Prefer exposing raw clipper diagnostics such as lookahead gain and envelope so UI can derive simpler booleans later."
  157. ],
  158. "required": true
  159. },
  160. {
  161. "id": "audio_mpx_post_bs412",
  162. "label": "Audio MPX Post-BS.412",
  163. "stage": "after BS.412 gain reduction on audio MPX, before pilot and RDS injection",
  164. "locationInCode": {
  165. "file": "internal/offline/generator.go",
  166. "function": "GenerateFrame",
  167. "region": "immediately after bs412Gain is applied to audioMPX and before composite starts from audioMPX"
  168. },
  169. "semanticPurpose": [
  170. "Represents the compliance-shaped audio multiplex signal.",
  171. "Shows what survives BS.412 before fixed protected components are added."
  172. ],
  173. "primaryUseCases": [
  174. "BS.412 activity visibility",
  175. "Compliance-focused diagnostics",
  176. "Gain reduction explanation"
  177. ],
  178. "metrics": [
  179. "audio_mpx_rms",
  180. "audio_mpx_peak_abs",
  181. "bs412_gain_applied",
  182. "bs412_attenuation_db",
  183. "estimated_audio_power"
  184. ],
  185. "notes": [
  186. "bs412_gain_applied is a chunk-level running control value and must not be presented as a sample-instantaneous causal reaction."
  187. ],
  188. "required": true
  189. },
  190. {
  191. "id": "composite_final_pre_iq",
  192. "label": "Composite Final Pre-IQ",
  193. "stage": "after audio MPX plus pilot plus RDS plus optional RDS2 and optional license injection, immediately before fmMod.Modulate(composite) or direct composite output",
  194. "locationInCode": {
  195. "file": "internal/offline/generator.go",
  196. "function": "GenerateFrame",
  197. "region": "final composite variable just before FM modulation or direct composite write"
  198. },
  199. "semanticPurpose": [
  200. "Represents the true final composite signal that is about to enter FM modulation or composite output.",
  201. "This is the most operator-relevant on-air-adjacent metering point."
  202. ],
  203. "primaryUseCases": [
  204. "Final MPX meter",
  205. "Pilot and RDS measurement",
  206. "Peak/deviation proxy",
  207. "On-air confidence"
  208. ],
  209. "metrics": [
  210. "composite_rms",
  211. "composite_peak_abs",
  212. "pilot_rms",
  213. "pilot_peak_abs",
  214. "pilot_injection_equivalent_percent",
  215. "rds_rms",
  216. "rds_peak_abs",
  217. "rds_injection_equivalent_percent",
  218. "composite_over_nominal_events",
  219. "composite_over_headroom_events"
  220. ],
  221. "notes": [
  222. "Percent-style fields here are operator-facing derived values and must be clearly separated from raw RMS values.",
  223. "Composite threshold counters are internal normalized envelope indicators, not regulatory overmodulation judgements.",
  224. "rdsInjectionEquivalentPercent must be implemented only after its mathematical derivation has been explicitly documented."
  225. ],
  226. "required": true
  227. }
  228. ],
  229. "preferredMetricSemantics": {
  230. "rms": "Chunk-local RMS of the measured signal component.",
  231. "peak_abs": "Maximum absolute sample magnitude within the chunk.",
  232. "injection_equivalent_percent": "A derived operator-facing injection/deviation-style display value, not identical to RMS. This should be documented separately from raw measured signal energy.",
  233. "clip_events": "Count of samples that hit or exceed a defined clipping threshold. Use only where the tapped stage really has hard-threshold clip semantics.",
  234. "attenuation_db": "Derived gain-reduction view, typically 20*log10(post/pre) when meaningful.",
  235. "estimated_audio_power": "Chunk-local power estimate suitable for BS.412-related operator display, but not legal certification.",
  236. "over_nominal_events": "Count of samples above an internal nominal composite reference threshold in normalized composite units.",
  237. "over_headroom_events": "Count of samples above a higher internal headroom reference threshold in normalized composite units.",
  238. "crest_factor": "Derived peak-versus-RMS characteristic useful where clipper/protection stages do not map cleanly to simple clip event counters."
  239. },
  240. "recommendedMeasurementFields": {
  241. "topLevel": [
  242. "timestamp",
  243. "sampleRateHz",
  244. "chunkSamples",
  245. "chunkDurationMs",
  246. "sequence",
  247. "stale",
  248. "noData",
  249. "flags"
  250. ],
  251. "flags": [
  252. "stereoEnabled",
  253. "stereoMode",
  254. "rdsEnabled",
  255. "rds2Enabled",
  256. "bs412Enabled",
  257. "compositeClipperEnabled",
  258. "watermarkEnabled",
  259. "licenseInjectionActive"
  260. ],
  261. "groups": {
  262. "lrPreEncodePostWatermark": [
  263. "lRms",
  264. "rRms",
  265. "lPeakAbs",
  266. "rPeakAbs",
  267. "lrBalanceDb",
  268. "lClipEvents",
  269. "rClipEvents"
  270. ],
  271. "audioMpxPreBs412": [
  272. "rms",
  273. "peakAbs",
  274. "monoRms",
  275. "stereoRms",
  276. "crestFactor",
  277. "clipperLookaheadGain",
  278. "clipperEnvelope",
  279. "clipperOrProtectionActive"
  280. ],
  281. "audioMpxPostBs412": [
  282. "rms",
  283. "peakAbs",
  284. "bs412GainApplied",
  285. "bs412AttenuationDb",
  286. "estimatedAudioPower"
  287. ],
  288. "compositeFinalPreIq": [
  289. "rms",
  290. "peakAbs",
  291. "pilotRms",
  292. "pilotPeakAbs",
  293. "pilotInjectionEquivalentPercent",
  294. "rdsRms",
  295. "rdsPeakAbs",
  296. "rdsInjectionEquivalentPercent",
  297. "overNominalEvents",
  298. "overHeadroomEvents"
  299. ]
  300. }
  301. },
  302. "dataModelProposal": {
  303. "goTypeName": "MeasurementSnapshot",
  304. "shape": {
  305. "timestamp": "time.Time",
  306. "sampleRateHz": "float64",
  307. "chunkSamples": "int",
  308. "chunkDurationMs": "float64",
  309. "sequence": "uint64",
  310. "stale": "bool",
  311. "noData": "bool",
  312. "flags": {
  313. "stereoEnabled": "bool",
  314. "stereoMode": "string",
  315. "rdsEnabled": "bool",
  316. "rds2Enabled": "bool",
  317. "bs412Enabled": "bool",
  318. "compositeClipperEnabled": "bool",
  319. "watermarkEnabled": "bool",
  320. "licenseInjectionActive": "bool"
  321. },
  322. "lrPreEncodePostWatermark": {
  323. "lRms": "float64",
  324. "rRms": "float64",
  325. "lPeakAbs": "float64",
  326. "rPeakAbs": "float64",
  327. "lrBalanceDb": "float64",
  328. "lClipEvents": "uint32",
  329. "rClipEvents": "uint32"
  330. },
  331. "audioMpxPreBs412": {
  332. "rms": "float64",
  333. "peakAbs": "float64",
  334. "monoRms": "float64",
  335. "stereoRms": "float64",
  336. "crestFactor": "float64",
  337. "clipperLookaheadGain": "float64",
  338. "clipperEnvelope": "float64",
  339. "clipperOrProtectionActive": "bool"
  340. },
  341. "audioMpxPostBs412": {
  342. "rms": "float64",
  343. "peakAbs": "float64",
  344. "bs412GainApplied": "float64",
  345. "bs412AttenuationDb": "float64",
  346. "estimatedAudioPower": "float64"
  347. },
  348. "compositeFinalPreIq": {
  349. "rms": "float64",
  350. "peakAbs": "float64",
  351. "pilotRms": "float64",
  352. "pilotPeakAbs": "float64",
  353. "pilotInjectionEquivalentPercent": "float64",
  354. "rdsRms": "float64",
  355. "rdsPeakAbs": "float64",
  356. "rdsInjectionEquivalentPercent": "float64 // TODO: only expose after mathematical derivation is fixed; otherwise omit or null in MVP",
  357. "overNominalEvents": "uint32",
  358. "overHeadroomEvents": "uint32"
  359. }
  360. }
  361. },
  362. "aggregationApproach": {
  363. "method": "chunk-local accumulation",
  364. "details": [
  365. "Accumulate sums, absolute peaks, and counters during one GenerateFrame() call.",
  366. "Finalize RMS and derived values once per chunk into one immutable snapshot.",
  367. "Publish only the latest completed snapshot.",
  368. "Do not emit per-sample external telemetry or unbounded histories from the hot path."
  369. ],
  370. "why": [
  371. "Matches current engine/generator architecture.",
  372. "Provides enough temporal resolution for 4-10 Hz UI polling.",
  373. "Keeps CPU and allocation overhead predictable."
  374. ]
  375. },
  376. "placementRecommendation": {
  377. "primaryOwner": "generator",
  378. "reasoning": [
  379. "All semantically relevant intermediate signals already exist in GenerateFrame().",
  380. "The generator has direct access to mono, stereo, pilot, RDS, audioMPX, and final composite values before they are flattened into output samples.",
  381. "Later measurement would lose stage separation and component identity."
  382. ],
  383. "preferredPublicationModel": "Generator computes and stores the latest MeasurementSnapshot through an atomic pointer or equivalent low-contention accessor."
  384. },
  385. "apiProposal": {
  386. "newEndpoint": "/measurements",
  387. "method": "GET",
  388. "pollingHz": {
  389. "min": 4,
  390. "recommended": 5,
  391. "max": 10
  392. },
  393. "whySeparateEndpoint": [
  394. "/runtime should remain focused on operational/runtime telemetry.",
  395. "UI should not need to poll the full runtime payload at high frequency for metering.",
  396. "Measurements have different freshness and payload-shape requirements than runtime state and config."
  397. ],
  398. "responseShapeExample": {
  399. "timestamp": "2026-04-13T05:30:00Z",
  400. "sampleRateHz": 228000,
  401. "chunkSamples": 11400,
  402. "chunkDurationMs": 50,
  403. "sequence": 12345,
  404. "stale": false,
  405. "noData": false,
  406. "flags": {
  407. "stereoEnabled": true,
  408. "stereoMode": "DSB",
  409. "rdsEnabled": true,
  410. "rds2Enabled": false,
  411. "bs412Enabled": true,
  412. "compositeClipperEnabled": true,
  413. "watermarkEnabled": false,
  414. "licenseInjectionActive": false
  415. },
  416. "lrPreEncodePostWatermark": {
  417. "lRms": 0.41,
  418. "rRms": 0.39,
  419. "lPeakAbs": 0.98,
  420. "rPeakAbs": 0.96,
  421. "lrBalanceDb": 0.42,
  422. "lClipEvents": 12,
  423. "rClipEvents": 8
  424. },
  425. "audioMpxPreBs412": {
  426. "rms": 0.52,
  427. "peakAbs": 1.0,
  428. "monoRms": 0.34,
  429. "stereoRms": 0.18,
  430. "crestFactor": 1.92,
  431. "clipperLookaheadGain": 0.94,
  432. "clipperEnvelope": 1.03,
  433. "clipperOrProtectionActive": true
  434. },
  435. "audioMpxPostBs412": {
  436. "rms": 0.46,
  437. "peakAbs": 0.91,
  438. "bs412GainApplied": 0.88,
  439. "bs412AttenuationDb": -1.11,
  440. "estimatedAudioPower": 0.21
  441. },
  442. "compositeFinalPreIq": {
  443. "rms": 0.49,
  444. "peakAbs": 1.08,
  445. "pilotRms": 0.064,
  446. "pilotPeakAbs": 0.09,
  447. "pilotInjectionEquivalentPercent": 9.0,
  448. "rdsRms": 0.028,
  449. "rdsPeakAbs": 0.04,
  450. "rdsInjectionEquivalentPercent": 4.0,
  451. "overNominalEvents": 91,
  452. "overHeadroomEvents": 0
  453. }
  454. },
  455. "emptyStateBehavior": {
  456. "whenTxInactive": "return either noData=true or the last snapshot with stale=true",
  457. "preferred": "explicit noData=true when no current generator output exists"
  458. }
  459. },
  460. "uiIntegrationPlan": {
  461. "overview": {
  462. "changes": [
  463. "Add or extend a metering section for measured L/R and final composite/MPX values.",
  464. "Show measured pilot and RDS values with explicit separation between raw measurements and operator-facing injection-equivalent display values.",
  465. "Optionally add compact L/R bar meters from lrPreEncodePostWatermark."
  466. ],
  467. "avoid": [
  468. "Do not replace queue/runtime health indicators with signal metering.",
  469. "Do not raise /runtime polling frequency just to animate meters.",
  470. "Do not label derived percent-style values as RMS values."
  471. ]
  472. },
  473. "flowTab": {
  474. "compositeMpxNode": [
  475. "Keep config-derived metadata as fallback.",
  476. "Use measured values as the primary state/detail source where available.",
  477. "Drive state and detail from final composite measurements plus compliance indicators.",
  478. "Include flags-based context when watermark, RDS2, or license injection changes the final composite interpretation.",
  479. "Include stereoMode context when non-DSB stereo operation changes signal expectations."
  480. ],
  481. "popover": [
  482. "Add pre-BS.412 vs post-BS.412 comparison rows.",
  483. "Show final composite peak and measured pilot/RDS values.",
  484. "If desired, show both raw measured values and operator-friendly injection-equivalent display values side by side."
  485. ]
  486. },
  487. "diagnostics": {
  488. "future": [
  489. "Add a dedicated engineering metering panel.",
  490. "Optionally add short client-side sparklines for composite RMS or BS.412 attenuation."
  491. ]
  492. }
  493. },
  494. "statusLogicProposal": {
  495. "compositeMpxNode": {
  496. "green": [
  497. "Measurements available",
  498. "Final composite is within expected internal normalized envelope",
  499. "Pilot and RDS are near expected target windows",
  500. "No severe compliance attention"
  501. ],
  502. "amber": [
  503. "Measurement is stale",
  504. "Pilot or RDS materially deviates from expected target",
  505. "High composite peak or visible compliance shaping",
  506. "Restart-pending settings affect MPX semantics"
  507. ],
  508. "red": [
  509. "Measurement unavailable while TX is running",
  510. "Final composite exceeds internal critical envelope threshold repeatedly",
  511. "Pilot missing when stereo is enabled",
  512. "RDS missing when RDS is enabled and expected"
  513. ],
  514. "gray": [
  515. "TX idle or no active signal path"
  516. ]
  517. }
  518. },
  519. "performanceConstraints": {
  520. "must": [
  521. "No contended locks in the per-sample inner loop.",
  522. "No heap allocation per sample.",
  523. "At most one snapshot publication per chunk.",
  524. "Endpoint read must be cheap and non-blocking."
  525. ],
  526. "acceptableCosts": [
  527. "A few additional per-sample accumulations such as abs, squares, and counters.",
  528. "A small per-chunk finalize step with sqrt and log calculations.",
  529. "Sampling lightweight clipper diagnostics when available."
  530. ]
  531. },
  532. "implementationPhases": [
  533. {
  534. "phase": 1,
  535. "name": "measurement model and generator instrumentation",
  536. "deliverables": [
  537. "Define MeasurementSnapshot and stage structs.",
  538. "Add accumulator logic in GenerateFrame().",
  539. "Publish latest completed snapshot from generator."
  540. ]
  541. },
  542. {
  543. "phase": 2,
  544. "name": "control-plane endpoint",
  545. "deliverables": [
  546. "Add GET /measurements handler.",
  547. "Wire generator/engine access into control server.",
  548. "Document endpoint in docs/API.md.",
  549. "Add tests for active, stale, and noData cases."
  550. ]
  551. },
  552. {
  553. "phase": 3,
  554. "name": "UI integration MVP",
  555. "deliverables": [
  556. "Poll /measurements separately at 4-10 Hz.",
  557. "Add measured MPX display in Overview.",
  558. "Drive Flow Composite / MPX node from measurement data."
  559. ]
  560. },
  561. {
  562. "phase": 4,
  563. "name": "diagnostics refinement",
  564. "deliverables": [
  565. "Add engineering-oriented meter details.",
  566. "Add client-side trend memory for sparkline rendering.",
  567. "Refine thresholds and warning logic."
  568. ]
  569. }
  570. ],
  571. "nonGoals": [
  572. "This is not a legal or certification-grade compliance measurement system.",
  573. "This does not replace external tools such as MpxTool or RF instrumentation.",
  574. "This does not implement full spectral analysis in the browser polling path.",
  575. "This does not yet define Prometheus/exporter metrics."
  576. ],
  577. "openQuestions": [
  578. {
  579. "id": "Q1",
  580. "question": "Should pilot and RDS percentages be derived from configured injection targets, measured component values, or both?",
  581. "currentPreference": "Expose measured component values and derived operator-facing injection-equivalent display fields separately, with the RDS derivation documented mathematically before implementation."
  582. },
  583. {
  584. "id": "Q2",
  585. "question": "Should internal thresholds be stored as normalized signal values or broadcast-style deviation percentages?",
  586. "currentPreference": "Store normalized internal values and format into broadcast-friendly display values in UI."
  587. },
  588. {
  589. "id": "Q3",
  590. "question": "Should /measurements include short rolling history arrays?",
  591. "currentPreference": "No for MVP; keep endpoint single-snapshot and let UI build short client-side history."
  592. },
  593. {
  594. "id": "Q4",
  595. "question": "Should long-term ownership remain in generator or later move into engine stats?",
  596. "currentPreference": "Generator ownership first, engine exposure second."
  597. },
  598. {
  599. "id": "Q5",
  600. "question": "Should clipperOrProtectionActive exist as a first-class field or be derived from raw clipper diagnostics?",
  601. "currentPreference": "Expose raw clipper diagnostics such as lookahead gain and envelope; only expose the boolean if its derivation rule is explicitly documented."
  602. },
  603. {
  604. "id": "Q6",
  605. "question": "Should a second optional pre-watermark engineering tap be added later?",
  606. "currentPreference": "Not for MVP; only add it if there is a real diagnostic need."
  607. }
  608. ],
  609. "reviewerFollowup": {
  610. "resolvedContradictions": [
  611. "audioMpxPreBs412.shape now includes clipperLookaheadGain and clipperEnvelope so the data-model shape matches the documented preference for raw clipper diagnostics first.",
  612. "rdsInjectionEquivalentPercent is now explicitly marked as a mathematically pending field that must be omitted, null, or TODO-gated in MVP until its derivation is fixed."
  613. ],
  614. "implementationGuardrails": [
  615. "Do not let developers treat dataModelProposal.shape as permission to drop raw clipper diagnostics in favor of only a boolean.",
  616. "Do not implement rdsInjectionEquivalentPercent with an ad-hoc plausible-looking formula; either define it rigorously first or keep it unexposed in MVP."
  617. ]
  618. },
  619. "recommendedNextStep": {
  620. "action": "implement phase 1",
  621. "firstConcreteWorkItems": [
  622. "Define MeasurementSnapshot and nested structs including flags and stereoMode.",
  623. "Add latest-measurement storage/accessor in generator.",
  624. "Instrument GenerateFrame() at lr_pre_encode_post_watermark, audio_mpx_pre_bs412, audio_mpx_post_bs412, and composite_final_pre_iq.",
  625. "Keep derived injection-equivalent fields and raw RMS fields explicitly separate from day one.",
  626. "Define rdsInjectionEquivalentPercent mathematically before exposing it.",
  627. "Treat licenseInjectionActive as chunk-local actual activity, not feature presence.",
  628. "Prefer raw clipper diagnostics over an underdefined active boolean unless the boolean rule is documented."
  629. ]
  630. }
  631. }