package stereo import ( "math" "github.com/jan/fm-rds-tx/internal/audio" "github.com/jan/fm-rds-tx/internal/dsp" ) // Components holds the individual MPX components produced by the stereo encoder. // All outputs are unity-normalized. The combiner controls actual injection levels. type Components struct { Mono float64 // (L+R)/2 baseband Stereo float64 // (L-R)/2 * sin(2 * pilotPhase), unity subcarrier Pilot float64 // sin(pilotPhase), unity amplitude } // StereoEncoder generates stereo MPX primitives from stereo audio frames. // The 38 kHz subcarrier is derived from the pilot phase (2× multiplication), // guaranteeing perfect phase coherence as required by the FM stereo standard. type StereoEncoder struct { pilot dsp.PilotGenerator } // NewStereoEncoder creates a StereoEncoder configured for the provided sample rate. func NewStereoEncoder(sampleRate float64) StereoEncoder { return StereoEncoder{ pilot: dsp.NewPilotGenerator(sampleRate), } } // Encode converts a stereo frame into MPX components. // The 38 kHz subcarrier is sin(2*pilotPhase), derived directly from the pilot // oscillator's phase — not from a separate oscillator. func (s *StereoEncoder) Encode(frame audio.Frame) Components { // Advance pilot and capture its phase BEFORE generating the sample pilot := s.pilot.Sample() // sin(2π * 19000 * t) pilotPhase := s.pilot.Phase() // 38 kHz subcarrier = sin(2 * pilotPhase * 2π) = sin(4π * 19000 * t) // This is mathematically identical to sin(2π * 38000 * t) but guaranteed // phase-locked to the pilot. FM receivers PLL onto the pilot and derive // the 38 kHz reference this exact same way. sub38 := math.Sin(2 * math.Pi * 2 * pilotPhase) return Components{ Mono: float64(frame.Mono()), Stereo: float64(frame.Difference()) * sub38, Pilot: pilot, } } // Reset restarts the pilot generator. func (s *StereoEncoder) Reset() { s.pilot.Reset() } // PilotPhase returns the current pilot oscillator phase in [0, 1). // Used to derive phase-coherent subcarriers (38 kHz = 2×, 57 kHz = 3×). func (s *StereoEncoder) PilotPhase() float64 { return s.pilot.Phase() } // RDSCarrier returns sin(3 * pilotPhase * 2π) — the 57 kHz carrier // phase-locked to the pilot, as required by the RDS standard. func (s *StereoEncoder) RDSCarrier() float64 { return math.Sin(2 * math.Pi * 3 * s.pilot.Phase()) }