Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

113 строки
5.3KB

  1. fix: 13 bugs from systematic codebase review (fix25)
  2. feat: add production-grade FMUpsampler (not yet wired)
  3. === SIGNAL PATH (fixes audible/measurable on HW) ===
  4. [CRITICAL] stereo: fix 38kHz subcarrier 1-sample phase offset
  5. Encode() called Sample() before capturing Phase(), so the 38kHz
  6. subcarrier used phase_{n+1} while pilot used phase_n — a constant
  7. 60 deg phase error at 228kHz, degrading stereo separation to ~50%.
  8. Now captures phase BEFORE tick; stores lastPhase for coherent
  9. RDS carrier derivation.
  10. [HIGH] rds: phase-lock 57kHz carrier to pilot via StereoEncoder
  11. RDS encoder had its own free-running 57kHz oscillator instead of
  12. deriving from 3x pilot. Two independent float64 oscillators drift
  13. apart over hours. Added NextSampleWithCarrier(carrier float64),
  14. generator now passes stereoEncoder.RDSCarrier() (sin(3*pilotPhase))
  15. so all subcarriers share one phase reference. NextSample() remains
  16. as backward-compat fallback with internal carrier.
  17. [MEDIUM] dsp/fmmod: fix phase wrapping for negative phase
  18. Wrap condition only triggered on phase > pi. Negative phase from
  19. negative-biased composite could grow unbounded, losing float64
  20. precision after extended runtime. Now wraps on |phase| > pi.
  21. [MEDIUM] dsp/oscillator: phase wrapping handles both directions
  22. Same pattern as fmmod — only wrapped positive overflow. Now wraps
  23. on phase >= 1 OR phase < 0 for defensive robustness.
  24. === DAUERLAUF / SBC STABILITY ===
  25. [HIGH] offline/generator: eliminate per-chunk allocation (912KB @ 2.28MHz)
  26. GenerateFrame() allocated a new []IQSample every call. At Pluto
  27. rate (2.28MHz, 50ms chunks) that's 114k*8=912KB * 20/sec = 18MB/s
  28. garbage, causing GC pauses and hardware underruns on Raspi.
  29. Now pre-allocates frameBuf, reuses across calls. Grow-only policy,
  30. never shrinks. Safe because driver.Write() is blocking.
  31. [MEDIUM] output/file: batch-write entire frame in one syscall
  32. Was writing 8 bytes per sample = 114k syscalls per chunk on Pluto.
  33. Now serializes into a reusable byte buffer, single file.Write().
  34. Orders of magnitude faster on Raspi SD-card I/O.
  35. [MEDIUM] app/engine: add error backoff in TX loop
  36. run() tight-looped at 100% CPU on persistent driver errors
  37. (hardware disconnect, USB reset). Now backs off by chunkDuration
  38. per error using select with ctx.Done() for clean cancellation.
  39. [MEDIUM] app/engine: replace time.Sleep shutdown with sync.WaitGroup
  40. Stop() used time.Sleep(2*chunkDuration) hoping run() would exit.
  41. Race condition if hardware Write() stalls. Now uses wg.Wait() for
  42. deterministic goroutine join before Flush/Stop.
  43. === CORRECTNESS / HYGIENE ===
  44. [LOW] rds/normalize: filter to ASCII, prevent mid-rune truncation
  45. normalizePS truncated at 8 bytes, not 8 characters. UTF-8 input
  46. (e.g. umlauts) could split mid-rune producing corrupt RDS bitstream.
  47. Now replaces non-ASCII with space before truncation.
  48. [LOW] offline/generator: increment frame sequence counter
  49. Sequence was hardcoded to 1 on every frame, useless for debugging.
  50. Now increments per GenerateFrame() call.
  51. [LOW] control: document that config PATCH doesn't reach running engine
  52. Added TODO noting that POST /config updates server's copy only;
  53. running Engine/Generator holds its own snapshot.
  54. [COSMETIC] dsp/preemphasis: remove dead fields y1, a1
  55. PreEmphasis.y1 and .a1 were never read in Process(). Removed from
  56. struct, constructor, and Reset(). Fixed misleading doc comment on
  57. PreEmphasizedSource (claims audio-rate, actually composite-rate).
  58. === NEW: FMUpsampler (not wired, for future split-rate path) ===
  59. dsp/fmupsample.go — production-grade phase-domain FM upsampler.
  60. Accumulates FM phase at source rate (228kHz), linearly interpolates
  61. to device rate (2.28MHz), emits IQ via sin/cos. Zero-allocation
  62. steady state. Cross-chunk boundary via prevPhase + srcPos carry.
  63. Symmetric phase wrapping. Virtual index coordinate system places
  64. prevPhase at vi=0, srcPhases at vi=1..N for seamless boundaries.
  65. dsp/fmupsample_test.go — 16 tests + 1 benchmark:
  66. - Zero/DC/varying composite signals
  67. - Output count for exact and non-integer ratios
  68. - Phase continuity across chunk boundaries (critical)
  69. - Non-integer ratio boundary continuity (50 chunks)
  70. - Phase wrapping over 100 chunks at full deviation
  71. - Negative deviation (tests symmetric wrapping)
  72. - Equivalence with FMModulator via frequency comparison
  73. - Buffer reuse verification (pointer identity)
  74. - Reset state clearing
  75. - Tiny chunks (1 sample), alternating composite (stress)
  76. - Long-run: 72000 chunks simulating 1 hour
  77. - Benchmark with ReportAllocs
  78. NOT wired into Engine — will be integrated when split-rate path
  79. is enabled for Pluto/HackRF on Raspi.
  80. Files changed:
  81. internal/stereo/encoder.go - phase coherence fix
  82. internal/rds/encoder.go - pilot-locked carrier API
  83. internal/rds/normalize.go - ASCII safety
  84. internal/offline/generator.go - buffer reuse + sequence + doc
  85. internal/dsp/fmmod.go - bidirectional phase wrap
  86. internal/dsp/oscillator.go - bidirectional phase wrap
  87. internal/dsp/preemphasis.go - dead field removal
  88. internal/dsp/fmupsample.go - NEW: production FM upsampler
  89. internal/dsp/fmupsample_test.go - NEW: 16 tests + benchmark
  90. internal/output/file.go - batch write
  91. internal/app/engine.go - WaitGroup + backoff
  92. internal/control/control.go - TODO config propagation