Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

222 rindas
5.5KB

  1. package stereo
  2. import (
  3. "math"
  4. "github.com/jan/fm-rds-tx/internal/audio"
  5. "github.com/jan/fm-rds-tx/internal/dsp"
  6. )
  7. const ssbHilbertTaps = 127
  8. // Mode selects the stereo subcarrier modulation method.
  9. type Mode int
  10. const (
  11. ModeDSB Mode = iota // Standard DSB-SC (FCC §73.322 compliant)
  12. ModeSSB // SSB-SC LSB only (Foti/Tarsio, experimental)
  13. ModeVSB // Vestigial SB (0-200Hz DSB, above SSB; experimental)
  14. )
  15. // ParseMode converts a string to Mode. Returns ModeDSB for unknown values.
  16. func ParseMode(s string) Mode {
  17. switch s {
  18. case "SSB", "ssb":
  19. return ModeSSB
  20. case "VSB", "vsb":
  21. return ModeVSB
  22. default:
  23. return ModeDSB
  24. }
  25. }
  26. // String returns the mode name.
  27. func (m Mode) String() string {
  28. switch m {
  29. case ModeSSB:
  30. return "SSB"
  31. case ModeVSB:
  32. return "VSB"
  33. default:
  34. return "DSB"
  35. }
  36. }
  37. // Components holds the individual MPX components produced by the stereo encoder.
  38. // All outputs are unity-normalized. The combiner controls actual injection levels.
  39. type Components struct {
  40. Mono float64 // (L+R)/2 baseband
  41. Stereo float64 // (L-R)/2 modulated onto 38 kHz subcarrier
  42. Pilot float64 // sin(pilotPhase), unity amplitude
  43. }
  44. // sampleDelay is a simple fixed-sample delay line.
  45. type sampleDelay struct {
  46. buf []float64
  47. pos int
  48. }
  49. func newSampleDelay(samples int) *sampleDelay {
  50. if samples <= 0 {
  51. return nil
  52. }
  53. return &sampleDelay{buf: make([]float64, samples)}
  54. }
  55. func (d *sampleDelay) Process(in float64) float64 {
  56. if d == nil || len(d.buf) == 0 {
  57. return in
  58. }
  59. out := d.buf[d.pos]
  60. d.buf[d.pos] = in
  61. d.pos++
  62. if d.pos >= len(d.buf) {
  63. d.pos = 0
  64. }
  65. return out
  66. }
  67. func (d *sampleDelay) Reset() {
  68. if d == nil {
  69. return
  70. }
  71. for i := range d.buf {
  72. d.buf[i] = 0
  73. }
  74. d.pos = 0
  75. }
  76. // StereoEncoder generates stereo MPX primitives from stereo audio frames.
  77. // Supports DSB-SC (standard), SSB-SC (lower sideband only), and VSB modes.
  78. type StereoEncoder struct {
  79. pilot dsp.PilotGenerator
  80. lastPhase float64
  81. mode Mode
  82. // SSB/VSB paths use a Hilbert transformer. The Hilbert FIR introduces a
  83. // fixed group delay, so the mono path must be delayed by the same amount.
  84. hilbert *dsp.HilbertFilter
  85. monoDelay *sampleDelay
  86. // VSB remains experimental. The low band is kept as DSB, while the high band
  87. // is encoded as SSB. Mono delay compensation still applies so the decoded
  88. // stereo matrix does not produce comb/reverb artefacts.
  89. vsbLPF *dsp.FilterChain
  90. hilbertHi *dsp.HilbertFilter
  91. }
  92. // NewStereoEncoder creates a StereoEncoder configured for the provided sample rate.
  93. func NewStereoEncoder(sampleRate float64) StereoEncoder {
  94. return StereoEncoder{
  95. pilot: dsp.NewPilotGenerator(sampleRate),
  96. mode: ModeDSB,
  97. }
  98. }
  99. // SetMode changes the stereo encoding mode and (re)initializes internal state.
  100. func (s *StereoEncoder) SetMode(mode Mode, sampleRate float64) {
  101. s.mode = mode
  102. s.hilbert = nil
  103. s.hilbertHi = nil
  104. s.vsbLPF = nil
  105. s.monoDelay = nil
  106. switch mode {
  107. case ModeSSB:
  108. s.hilbert = dsp.NewHilbertFilter(ssbHilbertTaps)
  109. s.monoDelay = newSampleDelay((ssbHilbertTaps - 1) / 2)
  110. case ModeVSB:
  111. s.hilbert = dsp.NewHilbertFilter(ssbHilbertTaps)
  112. s.hilbertHi = dsp.NewHilbertFilter(ssbHilbertTaps)
  113. s.vsbLPF = dsp.NewLPF4(200, sampleRate)
  114. s.monoDelay = newSampleDelay((ssbHilbertTaps - 1) / 2)
  115. }
  116. }
  117. // Encode converts a stereo frame into MPX components.
  118. func (s *StereoEncoder) Encode(frame audio.Frame) Components {
  119. pilotPhase := s.pilot.Phase()
  120. s.lastPhase = pilotPhase
  121. pilot := s.pilot.Sample()
  122. sub38sin := math.Sin(2 * math.Pi * 2 * pilotPhase)
  123. sub38cos := math.Cos(2 * math.Pi * 2 * pilotPhase)
  124. diff := float64(frame.Difference())
  125. mono := float64(frame.Mono())
  126. monoOut := mono
  127. var stereoOut float64
  128. switch s.mode {
  129. case ModeSSB:
  130. if s.hilbert == nil {
  131. stereoOut = diff * sub38sin
  132. break
  133. }
  134. monoOut = s.monoDelay.Process(mono)
  135. // SSB-LSB: s(t) = m_delayed(t)·sin(ωt) - m̂(t)·cos(ωt)
  136. // The - sign selects LSB (below 38 kHz).
  137. // ×2 compensates for the removed upper sideband (+6 dB).
  138. delayedDiff, hilb := s.hilbert.Process(diff)
  139. stereoOut = 2 * (delayedDiff*sub38sin - hilb*sub38cos)
  140. case ModeVSB:
  141. if s.hilbertHi == nil || s.vsbLPF == nil {
  142. stereoOut = diff * sub38sin
  143. break
  144. }
  145. monoOut = s.monoDelay.Process(mono)
  146. // Experimental VSB split:
  147. // - 0..200 Hz remains DSB to avoid aggressive low-frequency image shift.
  148. // - Above 200 Hz the residual is encoded as SSB-LSB.
  149. // The critical reverb bug was the mono-vs-diff timing mismatch; that is
  150. // fixed here by delaying mono by the Hilbert group delay.
  151. lo := s.vsbLPF.Process(diff)
  152. hi := diff - lo
  153. dsbPart := lo * sub38sin
  154. delayedHi, hilbHi := s.hilbertHi.Process(hi)
  155. ssbPart := 2 * (delayedHi*sub38sin - hilbHi*sub38cos)
  156. stereoOut = dsbPart + ssbPart
  157. default: // ModeDSB
  158. stereoOut = diff * sub38sin
  159. }
  160. return Components{
  161. Mono: monoOut,
  162. Stereo: stereoOut,
  163. Pilot: pilot,
  164. }
  165. }
  166. // Reset restarts the pilot generator and clears filter state.
  167. func (s *StereoEncoder) Reset() {
  168. s.pilot.Reset()
  169. if s.hilbert != nil {
  170. s.hilbert.Reset()
  171. }
  172. if s.hilbertHi != nil {
  173. s.hilbertHi.Reset()
  174. }
  175. if s.vsbLPF != nil {
  176. s.vsbLPF.Reset()
  177. }
  178. if s.monoDelay != nil {
  179. s.monoDelay.Reset()
  180. }
  181. }
  182. // PilotPhase returns the pilot phase used in the most recent Encode() call.
  183. func (s *StereoEncoder) PilotPhase() float64 {
  184. return s.lastPhase
  185. }
  186. // RDSCarrier returns sin(3 * pilotPhase * 2π) — the 57 kHz carrier
  187. // phase-locked to the pilot, as required by the RDS standard.
  188. func (s *StereoEncoder) RDSCarrier() float64 {
  189. return math.Sin(2 * math.Pi * 3 * s.lastPhase)
  190. }