Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

74 行
2.7KB

  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. // Components holds the individual MPX components produced by the stereo encoder.
  8. // All outputs are unity-normalized. The combiner controls actual injection levels.
  9. type Components struct {
  10. Mono float64 // (L+R)/2 baseband
  11. Stereo float64 // (L-R)/2 * sin(2 * pilotPhase), unity subcarrier
  12. Pilot float64 // sin(pilotPhase), unity amplitude
  13. }
  14. // StereoEncoder generates stereo MPX primitives from stereo audio frames.
  15. // The 38 kHz subcarrier is derived from the pilot phase (2× multiplication),
  16. // guaranteeing perfect phase coherence as required by the FM stereo standard.
  17. type StereoEncoder struct {
  18. pilot dsp.PilotGenerator
  19. lastPhase float64 // phase captured in last Encode(), for coherent RDS carrier
  20. }
  21. // NewStereoEncoder creates a StereoEncoder configured for the provided sample rate.
  22. func NewStereoEncoder(sampleRate float64) StereoEncoder {
  23. return StereoEncoder{
  24. pilot: dsp.NewPilotGenerator(sampleRate),
  25. }
  26. }
  27. // Encode converts a stereo frame into MPX components.
  28. // The 38 kHz subcarrier is sin(2*pilotPhase), derived directly from the pilot
  29. // oscillator's phase — not from a separate oscillator.
  30. func (s *StereoEncoder) Encode(frame audio.Frame) Components {
  31. // Capture phase BEFORE advancing — the 38 kHz subcarrier must use the
  32. // same phase instant as the pilot sample to maintain coherence.
  33. pilotPhase := s.pilot.Phase()
  34. s.lastPhase = pilotPhase
  35. pilot := s.pilot.Sample() // sin(2π * 19000 * t), then advances phase
  36. // 38 kHz subcarrier = sin(2 * pilotPhase * 2π) = sin(4π * 19000 * t)
  37. // This is mathematically identical to sin(2π * 38000 * t) but guaranteed
  38. // phase-locked to the pilot. FM receivers PLL onto the pilot and derive
  39. // the 38 kHz reference this exact same way.
  40. sub38 := math.Sin(2 * math.Pi * 2 * pilotPhase)
  41. return Components{
  42. Mono: float64(frame.Mono()),
  43. Stereo: float64(frame.Difference()) * sub38,
  44. Pilot: pilot,
  45. }
  46. }
  47. // Reset restarts the pilot generator.
  48. func (s *StereoEncoder) Reset() {
  49. s.pilot.Reset()
  50. }
  51. // PilotPhase returns the pilot phase used in the most recent Encode() call.
  52. // This is the coherent phase instant for deriving subcarriers (38 kHz = 2×, 57 kHz = 3×).
  53. func (s *StereoEncoder) PilotPhase() float64 {
  54. return s.lastPhase
  55. }
  56. // RDSCarrier returns sin(3 * pilotPhase * 2π) — the 57 kHz carrier
  57. // phase-locked to the pilot, as required by the RDS standard.
  58. // Uses the phase captured in the most recent Encode() call so that
  59. // pilot, 38 kHz subcarrier, and 57 kHz RDS carrier are all coherent.
  60. func (s *StereoEncoder) RDSCarrier() float64 {
  61. return math.Sin(2 * math.Pi * 3 * s.lastPhase)
  62. }