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

69 строки
2.3KB

  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. }
  20. // NewStereoEncoder creates a StereoEncoder configured for the provided sample rate.
  21. func NewStereoEncoder(sampleRate float64) StereoEncoder {
  22. return StereoEncoder{
  23. pilot: dsp.NewPilotGenerator(sampleRate),
  24. }
  25. }
  26. // Encode converts a stereo frame into MPX components.
  27. // The 38 kHz subcarrier is sin(2*pilotPhase), derived directly from the pilot
  28. // oscillator's phase — not from a separate oscillator.
  29. func (s *StereoEncoder) Encode(frame audio.Frame) Components {
  30. // Advance pilot and capture its phase BEFORE generating the sample
  31. pilot := s.pilot.Sample() // sin(2π * 19000 * t)
  32. pilotPhase := s.pilot.Phase()
  33. // 38 kHz subcarrier = sin(2 * pilotPhase * 2π) = sin(4π * 19000 * t)
  34. // This is mathematically identical to sin(2π * 38000 * t) but guaranteed
  35. // phase-locked to the pilot. FM receivers PLL onto the pilot and derive
  36. // the 38 kHz reference this exact same way.
  37. sub38 := math.Sin(2 * math.Pi * 2 * pilotPhase)
  38. return Components{
  39. Mono: float64(frame.Mono()),
  40. Stereo: float64(frame.Difference()) * sub38,
  41. Pilot: pilot,
  42. }
  43. }
  44. // Reset restarts the pilot generator.
  45. func (s *StereoEncoder) Reset() {
  46. s.pilot.Reset()
  47. }
  48. // PilotPhase returns the current pilot oscillator phase in [0, 1).
  49. // Used to derive phase-coherent subcarriers (38 kHz = 2×, 57 kHz = 3×).
  50. func (s *StereoEncoder) PilotPhase() float64 {
  51. return s.pilot.Phase()
  52. }
  53. // RDSCarrier returns sin(3 * pilotPhase * 2π) — the 57 kHz carrier
  54. // phase-locked to the pilot, as required by the RDS standard.
  55. func (s *StereoEncoder) RDSCarrier() float64 {
  56. return math.Sin(2 * math.Pi * 3 * s.pilot.Phase())
  57. }