|
|
@@ -1,6 +1,8 @@ |
|
|
package stereo |
|
|
package stereo |
|
|
|
|
|
|
|
|
import ( |
|
|
import ( |
|
|
|
|
|
"math" |
|
|
|
|
|
|
|
|
"github.com/jan/fm-rds-tx/internal/audio" |
|
|
"github.com/jan/fm-rds-tx/internal/audio" |
|
|
"github.com/jan/fm-rds-tx/internal/dsp" |
|
|
"github.com/jan/fm-rds-tx/internal/dsp" |
|
|
) |
|
|
) |
|
|
@@ -9,28 +11,37 @@ import ( |
|
|
// All outputs are unity-normalized. The combiner controls actual injection levels. |
|
|
// All outputs are unity-normalized. The combiner controls actual injection levels. |
|
|
type Components struct { |
|
|
type Components struct { |
|
|
Mono float64 // (L+R)/2 baseband |
|
|
Mono float64 // (L+R)/2 baseband |
|
|
Stereo float64 // (L-R)/2 * sin(2π·38kHz·t), unity subcarrier |
|
|
|
|
|
Pilot float64 // sin(2π·19kHz·t), unity amplitude |
|
|
|
|
|
|
|
|
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. |
|
|
// 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 { |
|
|
type StereoEncoder struct { |
|
|
pilot dsp.PilotGenerator |
|
|
|
|
|
subcarrier dsp.Oscillator |
|
|
|
|
|
|
|
|
pilot dsp.PilotGenerator |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// NewStereoEncoder creates a StereoEncoder configured for the provided sample rate. |
|
|
// NewStereoEncoder creates a StereoEncoder configured for the provided sample rate. |
|
|
func NewStereoEncoder(sampleRate float64) StereoEncoder { |
|
|
func NewStereoEncoder(sampleRate float64) StereoEncoder { |
|
|
return StereoEncoder{ |
|
|
return StereoEncoder{ |
|
|
pilot: dsp.NewPilotGenerator(sampleRate), |
|
|
|
|
|
subcarrier: dsp.Oscillator{Frequency: 38000, SampleRate: sampleRate}, |
|
|
|
|
|
|
|
|
pilot: dsp.NewPilotGenerator(sampleRate), |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Encode converts a stereo frame into MPX components. |
|
|
// 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 { |
|
|
func (s *StereoEncoder) Encode(frame audio.Frame) Components { |
|
|
pilot := s.pilot.Sample() |
|
|
|
|
|
sub38 := s.subcarrier.Tick() |
|
|
|
|
|
|
|
|
// 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{ |
|
|
return Components{ |
|
|
Mono: float64(frame.Mono()), |
|
|
Mono: float64(frame.Mono()), |
|
|
@@ -39,8 +50,19 @@ func (s *StereoEncoder) Encode(frame audio.Frame) Components { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Reset restarts the pilot and subcarrier generators. |
|
|
|
|
|
|
|
|
// Reset restarts the pilot generator. |
|
|
func (s *StereoEncoder) Reset() { |
|
|
func (s *StereoEncoder) Reset() { |
|
|
s.pilot.Reset() |
|
|
s.pilot.Reset() |
|
|
s.subcarrier.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()) |
|
|
} |
|
|
} |