package rds import ( "math" "os" ) // RDS2 subcarrier frequencies (IEC 62106-1:2018, Figure 3). // All are integer multiples of 19 kHz / 2. const ( RDS2Stream1Freq = 66500.0 // 3.5 × 19 kHz RDS2Stream2Freq = 71250.0 // 3.75 × 19 kHz RDS2Stream3Freq = 76000.0 // 4 × 19 kHz ) // RDS2Encoder generates the three additional RDS2 subcarrier signals. // Each stream carries Group Type C data at 1187.5 bps. // The output is added to the composite MPX signal. type RDS2Encoder struct { streams [3]*streamEncoder sampleRate float64 enabled bool // RFT state: file segments to transmit rftFile *RFTFile rftSegIdx int } // streamEncoder handles one RDS2 subcarrier stream. type streamEncoder struct { sampleRate float64 carrierFreq float64 carrierPhase float64 carrierStep float64 // Biphase encoding (same as Stream 0) spb int // samples per bit waveform []float64 // resampled PiFmRds biphase waveform wfLen int ring []float64 ringSize int bitBuffer [bitsPerGroup]int bitPos int prevOutput int curOutput int sampleCount int inSampleIdx int outSampleIdx int // Group C queue groups []GroupC groupIdx int } // NewRDS2Encoder creates an RDS2 encoder for the given sample rate. // Call LoadLogo() to set up the station logo for RFT transmission. func NewRDS2Encoder(sampleRate float64) *RDS2Encoder { freqs := [3]float64{RDS2Stream1Freq, RDS2Stream2Freq, RDS2Stream3Freq} e := &RDS2Encoder{sampleRate: sampleRate} for i := 0; i < 3; i++ { e.streams[i] = newStreamEncoder(sampleRate, freqs[i]) } return e } func newStreamEncoder(sampleRate, carrierFreq float64) *streamEncoder { spb := int(math.Round(sampleRate / rdsBitRate)) ratio := sampleRate / refRate // Resample PiFmRds waveform (same as main RDS encoder) wfLen := int(math.Round(float64(refFilterSize) * ratio)) waveform := make([]float64, wfLen) for i := range waveform { srcPos := float64(i) / ratio idx := int(srcPos) frac := srcPos - float64(idx) if idx+1 < refFilterSize { waveform[i] = refWaveform[idx]*(1-frac) + refWaveform[idx+1]*frac } else if idx < refFilterSize { waveform[i] = refWaveform[idx] } } // Normalize to peak=1.0 var peak float64 for _, v := range waveform { if a := math.Abs(v); a > peak { peak = a } } if peak > 0 { for i := range waveform { waveform[i] /= peak } } ringSize := spb + wfLen return &streamEncoder{ sampleRate: sampleRate, carrierFreq: carrierFreq, carrierPhase: 0, carrierStep: carrierFreq / sampleRate, spb: spb, waveform: waveform, wfLen: wfLen, ring: make([]float64, ringSize), ringSize: ringSize, bitPos: bitsPerGroup, sampleCount: spb, outSampleIdx: ringSize - 1, } } // Enable activates or deactivates RDS2 output. func (e *RDS2Encoder) Enable(on bool) { e.enabled = on } // Enabled returns whether RDS2 is active. func (e *RDS2Encoder) Enabled() bool { return e.enabled } // LoadLogo reads a logo file and segments it for RFT transmission on Stream 1. func (e *RDS2Encoder) LoadLogo(path string) error { data, err := os.ReadFile(path) if err != nil { return err } // Detect file type ft := uint8(RFTFileTypePNG) if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF { ft = RFTFileTypeJPEG } e.rftFile = SegmentFile(data, 0, ft) // fileID=0 = station logo e.rftSegIdx = 0 return nil } // NextSample returns the combined RDS2 subcarrier sample for all 3 streams. // Add this to the composite MPX signal with appropriate injection level. func (e *RDS2Encoder) NextSample() float64 { if !e.enabled { return 0 } var sum float64 for i := 0; i < 3; i++ { sum += e.streams[i].nextSample() } return sum } // NextSampleWithPilot returns the RDS2 sample using pilot-locked carriers. // pilotPhase is the current pilot oscillator phase (0-1 range). func (e *RDS2Encoder) NextSampleWithPilot(pilotPhase float64) float64 { if !e.enabled { return 0 } // RDS2 carriers are at 3.5×, 3.75×, 4× pilot frequency // sin(2π × N × pilotPhase) where N = 3.5, 3.75, 4.0 multipliers := [3]float64{3.5, 3.75, 4.0} var sum float64 for i := 0; i < 3; i++ { carrier := math.Sin(2 * math.Pi * multipliers[i] * pilotPhase) sum += e.streams[i].nextSampleWithCarrier(carrier) } return sum } // FeedGroups distributes Group C data to the stream encoders. // Call periodically (e.g. once per frame) to keep streams fed. func (e *RDS2Encoder) FeedGroups() { if !e.enabled { return } // Stream 1: Station logo via RFT (if loaded) if e.rftFile != nil && len(e.rftFile.Segments) > 0 { seg := e.rftFile.Segments[e.rftSegIdx] gc := GroupC{FH: seg.FH, Data: seg.Payload} e.streams[0].pushGroup(gc) e.rftSegIdx = (e.rftSegIdx + 1) % len(e.rftFile.Segments) } // Streams 2 & 3: available for future ODAs // For now, send idle groups (FH=0, all zeros) } // --- streamEncoder methods --- func (se *streamEncoder) pushGroup(gc GroupC) { se.groups = append(se.groups, gc) } func (se *streamEncoder) nextSample() float64 { carrier := math.Sin(2 * math.Pi * se.carrierPhase) se.carrierPhase += se.carrierStep if se.carrierPhase >= 1.0 { se.carrierPhase -= 1.0 } return se.nextSampleWithCarrier(carrier) } func (se *streamEncoder) nextSampleWithCarrier(carrier float64) float64 { if se.sampleCount >= se.spb { if se.bitPos >= bitsPerGroup { se.getNextGroup() se.bitPos = 0 } curBit := se.bitBuffer[se.bitPos] se.prevOutput = se.curOutput se.curOutput = se.prevOutput ^ curBit inverting := (se.curOutput == 1) idx := se.inSampleIdx for j := 0; j < se.wfLen; j++ { val := se.waveform[j] if inverting { val = -val } se.ring[idx] += val idx++ if idx >= se.ringSize { idx = 0 } } se.inSampleIdx += se.spb if se.inSampleIdx >= se.ringSize { se.inSampleIdx -= se.ringSize } se.bitPos++ se.sampleCount = 0 } envelope := se.ring[se.outSampleIdx] se.ring[se.outSampleIdx] = 0 se.outSampleIdx++ if se.outSampleIdx >= se.ringSize { se.outSampleIdx = 0 } se.sampleCount++ return envelope * carrier } func (se *streamEncoder) getNextGroup() { var group [4]uint16 if se.groupIdx < len(se.groups) { gc := se.groups[se.groupIdx] group = buildGroupC(gc) se.groupIdx++ if se.groupIdx >= len(se.groups) { se.groupIdx = 0 // loop } } // else: idle group (all zeros) // Encode group into bit buffer using same CRC/offset as Stream 0 pos := 0 for blk, off := range [4]byte{'A', 'B', 'C', 'D'} { encoded := encodeBlock(group[blk], off) for bit := bitsPerBlock - 1; bit >= 0; bit-- { se.bitBuffer[pos] = int((encoded >> uint(bit)) & 1) pos++ } } }