|
- 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())
- }
|