|
- 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++
- }
- }
- }
|