|
- package rds
-
- import (
- "fmt"
- "math"
- )
-
- // Decoder performs RDS baseband decode with Costas loop carrier recovery
- // and Mueller & Muller symbol timing synchronization.
- type Decoder struct {
- ps [8]rune
- rt [64]rune
- lastPI uint16
- // Costas loop state (persistent across calls)
- costasPhase float64
- costasFreq float64
- // Symbol sync state
- syncMu float64
- syncPrev complex64
- syncPrevDecision complex64
- syncHasPrev bool
- lastSampleRate int
- // Differential decode state across Decode() calls
- lastHardBit int
- hasLastHardBit bool
- // Diagnostic counters
- TotalDecodes int
- BlockAHits int
- GroupsFound int
- LastDiag string
- }
-
- type Result struct {
- PI uint16 `json:"pi"`
- PS string `json:"ps"`
- RT string `json:"rt"`
- }
-
- type scanDiag struct {
- pol string
- blockHits int
- offAHits int
- offBHits int
- offCHits int
- offCpHits int
- offDHits int
- abSeq int
- abcSeq int
- groups int
- piHint uint16
- piHintCount int
- fecBlockFix int
- grpFecFix int
- blockAmbig int
- seqAmbig int
- }
-
- func (s scanDiag) score() int {
- return s.groups*100000 + s.abcSeq*1000 + s.abSeq*100 + s.blockHits
- }
-
- func bestDiag(a, b scanDiag) scanDiag {
- if b.score() > a.score() {
- return b
- }
- if b.score() == a.score() {
- if b.piHintCount > a.piHintCount {
- return b
- }
- if b.fecBlockFix > a.fecBlockFix {
- return b
- }
- }
- return a
- }
-
- // Decode takes complex baseband samples at ~20kHz and extracts RDS data.
- func (d *Decoder) Decode(samples []complex64, sampleRate int) Result {
- if len(samples) < 104 || sampleRate <= 0 {
- return Result{PI: d.lastPI, PS: d.psString(), RT: d.rtString()}
- }
- d.TotalDecodes++
-
- if d.lastSampleRate != 0 && d.lastSampleRate != sampleRate {
- d.costasPhase = 0
- d.costasFreq = 0
- d.syncMu = 0
- d.syncPrev = 0
- d.syncPrevDecision = 0
- d.syncHasPrev = false
- d.lastHardBit = 0
- d.hasLastHardBit = false
- }
- d.lastSampleRate = sampleRate
-
- sps := float64(sampleRate) / 1187.5 // samples per symbol
-
- // === Mueller & Muller symbol timing recovery (persistent across calls) ===
- mu := d.syncMu
- if mu < 0 || mu >= sps || math.IsNaN(mu) || math.IsInf(mu, 0) {
- mu = sps / 2
- }
- symbols := make([]complex64, 0, len(samples)/int(sps)+1)
- prev := d.syncPrev
- prevDecision := d.syncPrevDecision
- havePrev := d.syncHasPrev
- for mu < float64(len(samples)-1) {
- idx := int(mu)
- frac := mu - float64(idx)
- if idx+1 >= len(samples) {
- break
- }
- samp := complex64(complex(
- float64(real(samples[idx]))*(1-frac)+float64(real(samples[idx+1]))*frac,
- float64(imag(samples[idx]))*(1-frac)+float64(imag(samples[idx+1]))*frac,
- ))
-
- var decision complex64
- if real(samp) >= 0 {
- decision = 1
- } else {
- decision = -1
- }
-
- if havePrev {
- errR := float64(real(decision)-real(prevDecision))*float64(real(prev)) -
- float64(real(samp)-real(prev))*float64(real(prevDecision))
- mu += sps + 0.01*errR
- } else {
- mu += sps
- havePrev = true
- }
-
- prevDecision = decision
- prev = samp
- symbols = append(symbols, samp)
- }
- residualMu := mu - float64(len(samples))
- for residualMu < 0 {
- residualMu += sps
- }
- for residualMu >= sps {
- residualMu -= sps
- }
- d.syncMu = residualMu
- d.syncPrev = prev
- d.syncPrevDecision = prevDecision
- d.syncHasPrev = havePrev
-
- if len(symbols) < 26*4 {
- d.LastDiag = fmt.Sprintf("too few symbols: %d sps=%.1f mu=%.3f", len(symbols), sps, d.syncMu)
- return Result{PI: d.lastPI, PS: d.psString(), RT: d.rtString()}
- }
-
- // === Costas loop for fine frequency/phase synchronization (persistent across calls) ===
- alpha := 0.132
- beta := alpha * alpha / 4.0
- phase := d.costasPhase
- freq := d.costasFreq
- synced := make([]complex64, len(symbols))
- for i, s := range symbols {
- // Multiply by exp(-j*phase) to de-rotate.
- cosP := float32(math.Cos(phase))
- sinP := float32(math.Sin(phase))
- synced[i] = complex(
- real(s)*cosP+imag(s)*sinP,
- imag(s)*cosP-real(s)*sinP,
- )
- // BPSK phase error: sign(I) * Q.
- var err float64
- if real(synced[i]) >= 0 {
- err = float64(imag(synced[i]))
- } else {
- err = -float64(imag(synced[i]))
- }
- freq += beta * err
- phase += freq + alpha*err
- for phase > math.Pi {
- phase -= 2 * math.Pi
- }
- for phase < -math.Pi {
- phase += 2 * math.Pi
- }
- }
- d.costasPhase = phase
- d.costasFreq = freq
-
- // Measure signal quality: average |I| and |Q| after Costas.
- var sumI, sumQ float64
- for _, s := range synced {
- ri := math.Abs(float64(real(s)))
- rq := math.Abs(float64(imag(s)))
- sumI += ri
- sumQ += rq
- }
- avgI := sumI / float64(len(synced))
- avgQ := sumQ / float64(len(synced))
-
- // === BPSK demodulation ===
- hardBits := make([]int, len(synced))
- for i, s := range synced {
- if real(s) >= 0 {
- hardBits[i] = 1
- } else {
- hardBits[i] = 0
- }
- }
-
- // === Differential decoding ===
- // Preserve the differential transition across Decode() calls. On the very
- // first call we keep the historical behavior (N hard bits -> N-1 diff bits);
- // once carry state exists we prepend the cross-chunk transition bit.
- var bits []int
- if len(hardBits) > 0 {
- if d.hasLastHardBit {
- bits = make([]int, len(hardBits))
- bits[0] = hardBits[0] ^ d.lastHardBit
- for i := 1; i < len(hardBits); i++ {
- bits[i] = hardBits[i] ^ hardBits[i-1]
- }
- } else if len(hardBits) >= 2 {
- bits = make([]int, len(hardBits)-1)
- for i := 1; i < len(hardBits); i++ {
- bits[i-1] = hardBits[i] ^ hardBits[i-1]
- }
- }
- d.lastHardBit = hardBits[len(hardBits)-1]
- d.hasLastHardBit = true
- }
- invBits := make([]int, len(bits))
- for i, b := range bits {
- invBits[i] = 1 - b
- }
-
- // === Diagnostics before/after 1-bit FEC ===
- rawBest := bestDiag(analyzeStream(bits, false, "dir"), analyzeStream(invBits, false, "inv"))
- fecBest := bestDiag(analyzeStream(bits, true, "dir"), analyzeStream(invBits, true, "inv"))
- d.BlockAHits += rawBest.offAHits
-
- // === Block sync + CRC decode with conservative 1-bit FEC ===
- groupsFound := d.tryDecode(bits, true)
- usedPol := "dir"
- if groupsFound == 0 {
- groupsFound = d.tryDecode(invBits, true)
- if groupsFound > 0 {
- usedPol = "inv"
- } else {
- usedPol = "none"
- }
- }
- if groupsFound > 0 {
- d.GroupsFound++
- }
-
- d.LastDiag = fmt.Sprintf(
- "syms=%d sps=%.1f mu=%.3f costasFreq=%.4f avgI=%.4f avgQ=%.4f diffCarry=%t raw[%s blk=%d A/B/C/Cp/D=%d/%d/%d/%d/%d AB=%d ABC=%d grp=%d pi=%04Xx%d] fec[%s blk=%d A/B/C/Cp/D=%d/%d/%d/%d/%d AB=%d ABC=%d grp=%d pi=%04Xx%d fixBlk=%d fixGrp=%d ambBlk=%d ambSeq=%d] use=%s found=%d okCalls=%d",
- len(symbols), sps, d.syncMu, d.costasFreq, avgI, avgQ, d.hasLastHardBit,
- rawBest.pol, rawBest.blockHits, rawBest.offAHits, rawBest.offBHits, rawBest.offCHits, rawBest.offCpHits, rawBest.offDHits, rawBest.abSeq, rawBest.abcSeq, rawBest.groups, rawBest.piHint, rawBest.piHintCount,
- fecBest.pol, fecBest.blockHits, fecBest.offAHits, fecBest.offBHits, fecBest.offCHits, fecBest.offCpHits, fecBest.offDHits, fecBest.abSeq, fecBest.abcSeq, fecBest.groups, fecBest.piHint, fecBest.piHintCount, fecBest.fecBlockFix, fecBest.grpFecFix, fecBest.blockAmbig, fecBest.seqAmbig,
- usedPol, groupsFound, d.GroupsFound,
- )
-
- return Result{PI: d.lastPI, PS: d.psString(), RT: d.rtString()}
- }
-
- func analyzeStream(bits []int, useFEC bool, pol string) scanDiag {
- diag := scanDiag{pol: pol}
- if len(bits) < 26 {
- return diag
- }
-
- piCounts := make(map[uint16]int)
- for i := 0; i+26 <= len(bits); i++ {
- blk, ok, corrected, ambiguous := decodeBlockAny(bits[i:i+26], useFEC)
- if ambiguous {
- diag.blockAmbig++
- }
- if !ok {
- continue
- }
- diag.blockHits++
- if corrected {
- diag.fecBlockFix++
- }
- switch blk.offset {
- case offA:
- diag.offAHits++
- if blk.data != 0 {
- piCounts[blk.data]++
- }
- case offB:
- diag.offBHits++
- case offC:
- diag.offCHits++
- case offCp:
- diag.offCpHits++
- case offD:
- diag.offDHits++
- }
- }
- for pi, n := range piCounts {
- if n > diag.piHintCount {
- diag.piHint = pi
- diag.piHintCount = n
- }
- }
-
- const groupBits = 26 * 4
- for i := 0; i+groupBits <= len(bits); i++ {
- fixes := 0
-
- _, okA, fixedA, ambA := decodeBlockExpected(bits[i:i+26], []uint16{offA}, useFEC)
- if ambA {
- diag.seqAmbig++
- }
- if !okA {
- continue
- }
- if fixedA {
- fixes++
- }
-
- _, okB, fixedB, ambB := decodeBlockExpected(bits[i+26:i+52], []uint16{offB}, useFEC)
- if ambB {
- diag.seqAmbig++
- }
- if !okB {
- continue
- }
- diag.abSeq++
- if fixedB {
- fixes++
- }
-
- _, okC, fixedC, ambC := decodeBlockExpected(bits[i+52:i+78], []uint16{offC, offCp}, useFEC)
- if ambC {
- diag.seqAmbig++
- }
- if !okC {
- continue
- }
- diag.abcSeq++
- if fixedC {
- fixes++
- }
-
- _, okD, fixedD, ambD := decodeBlockExpected(bits[i+78:i+104], []uint16{offD}, useFEC)
- if ambD {
- diag.seqAmbig++
- }
- if !okD {
- continue
- }
- if fixedD {
- fixes++
- }
-
- diag.groups++
- diag.grpFecFix += fixes
- }
-
- return diag
- }
-
- func (d *Decoder) tryDecode(bits []int, useFEC bool) int {
- const (
- groupBits = 26 * 4
- flywheelJitter = 3
- )
- groups := 0
- for i := 0; i+groupBits <= len(bits); {
- grp, _, ok := decodeGroupAt(bits, i, useFEC)
- if !ok {
- i++
- continue
- }
-
- groups++
- d.applyGroup(grp)
-
- // Flywheel: once a valid group was found, prefer the next expected 104-bit
- // boundary and search only in a tiny jitter window around it before falling
- // back to full bitwise scanning.
- nextExpected := i + groupBits
- locked := true
- for locked && nextExpected+groupBits <= len(bits) {
- if nextGrp, _, ok := decodeGroupAt(bits, nextExpected, useFEC); ok {
- d.applyGroup(nextGrp)
- groups++
- nextExpected += groupBits
- continue
- }
-
- matched := false
- for delta := 1; delta <= flywheelJitter && !matched; delta++ {
- left := nextExpected - delta
- if left >= 0 {
- if nextGrp, _, ok := decodeGroupAt(bits, left, useFEC); ok {
- d.applyGroup(nextGrp)
- groups++
- nextExpected = left + groupBits
- matched = true
- break
- }
- }
- right := nextExpected + delta
- if right+groupBits <= len(bits) {
- if nextGrp, _, ok := decodeGroupAt(bits, right, useFEC); ok {
- d.applyGroup(nextGrp)
- groups++
- nextExpected = right + groupBits
- matched = true
- break
- }
- }
- }
-
- if !matched {
- locked = false
- }
- }
-
- resume := nextExpected - flywheelJitter
- if resume <= i {
- resume = i + 1
- }
- i = resume
- }
- return groups
- }
-
- func decodeGroupAt(bits []int, start int, useFEC bool) ([4]block, int, bool) {
- const groupBits = 26 * 4
- var grp [4]block
- if start < 0 || start+groupBits > len(bits) {
- return grp, 0, false
- }
- fixes := 0
-
- bA, okA, fixedA, _ := decodeBlockExpected(bits[start:start+26], []uint16{offA}, useFEC)
- if !okA {
- return grp, 0, false
- }
- if fixedA {
- fixes++
- }
- bB, okB, fixedB, _ := decodeBlockExpected(bits[start+26:start+52], []uint16{offB}, useFEC)
- if !okB {
- return grp, 0, false
- }
- if fixedB {
- fixes++
- }
- bC, okC, fixedC, _ := decodeBlockExpected(bits[start+52:start+78], []uint16{offC, offCp}, useFEC)
- if !okC {
- return grp, 0, false
- }
- if fixedC {
- fixes++
- }
- bD, okD, fixedD, _ := decodeBlockExpected(bits[start+78:start+104], []uint16{offD}, useFEC)
- if !okD {
- return grp, 0, false
- }
- if fixedD {
- fixes++
- }
- grp[0] = bA
- grp[1] = bB
- grp[2] = bC
- grp[3] = bD
- return grp, fixes, true
- }
-
- func (d *Decoder) applyGroup(grp [4]block) {
- bA, bB, bC, bD := grp[0], grp[1], grp[2], grp[3]
- pi := bA.data
- if pi != 0 {
- d.lastPI = pi
- }
- groupType := (bB.data >> 12) & 0xF
- versionA := ((bB.data >> 11) & 0x1) == 0
- if groupType == 0 {
- addr := bB.data & 0x3
- if versionA {
- chars := []byte{byte(bD.data >> 8), byte(bD.data & 0xFF)}
- idx := int(addr) * 2
- if idx+1 < len(d.ps) {
- d.ps[idx] = sanitizeRune(chars[0])
- d.ps[idx+1] = sanitizeRune(chars[1])
- }
- }
- }
- if groupType == 2 && versionA {
- addr := bB.data & 0xF
- chars := []byte{byte(bC.data >> 8), byte(bC.data & 0xFF), byte(bD.data >> 8), byte(bD.data & 0xFF)}
- idx := int(addr) * 4
- for j := 0; j < 4 && idx+j < len(d.rt); j++ {
- d.rt[idx+j] = sanitizeRune(chars[j])
- }
- }
- }
-
- type block struct {
- data uint16
- offset uint16
- }
-
- const (
- offA uint16 = 0x0FC
- offB uint16 = 0x198
- offC uint16 = 0x168
- offCp uint16 = 0x350
- offD uint16 = 0x1B4
- )
-
- var allOffsets = []uint16{offA, offB, offC, offCp, offD}
-
- func decodeBlock(bits []int) (block, bool) {
- if len(bits) != 26 {
- return block{}, false
- }
- return decodeRawBlock(bitsToRaw(bits))
- }
-
- func bitsToRaw(bits []int) uint32 {
- var raw uint32
- for _, b := range bits {
- raw = (raw << 1) | uint32(b&1)
- }
- return raw
- }
-
- func decodeRawBlock(raw uint32) (block, bool) {
- data := uint16(raw >> 10)
- synd := crcSyndrome(raw)
- switch synd {
- case offA, offB, offC, offCp, offD:
- return block{data: data, offset: synd}, true
- default:
- return block{}, false
- }
- }
-
- func decodeBlockAny(bits []int, useFEC bool) (block, bool, bool, bool) {
- return decodeBlockExpected(bits, allOffsets, useFEC)
- }
-
- func decodeBlockExpected(bits []int, allowed []uint16, useFEC bool) (block, bool, bool, bool) {
- if len(bits) != 26 {
- return block{}, false, false, false
- }
- if blk, ok := decodeBlock(bits); ok && offsetAllowed(blk.offset, allowed) {
- return blk, true, false, false
- }
- if !useFEC {
- return block{}, false, false, false
- }
-
- raw := bitsToRaw(bits)
- var candidate block
- candidateCount := 0
- for i := 0; i < 26; i++ {
- flipped := raw ^ (uint32(1) << uint(25-i))
- blk, ok := decodeRawBlock(flipped)
- if !ok || !offsetAllowed(blk.offset, allowed) {
- continue
- }
- candidate = blk
- candidateCount++
- if candidateCount > 1 {
- return block{}, false, false, true
- }
- }
- if candidateCount == 1 {
- return candidate, true, true, false
- }
- return block{}, false, false, false
- }
-
- func offsetAllowed(offset uint16, allowed []uint16) bool {
- for _, want := range allowed {
- if offset == want {
- return true
- }
- }
- return false
- }
-
- func crcSyndrome(raw uint32) uint16 {
- poly := uint32(0x1B9)
- reg := raw
- for i := 25; i >= 10; i-- {
- if (reg>>uint(i))&1 == 1 {
- reg ^= poly << uint(i-10)
- }
- }
- return uint16(reg & 0x3FF)
- }
-
- func sanitizeRune(b byte) rune {
- if b < 32 || b > 126 {
- return ' '
- }
- return rune(b)
- }
-
- func (d *Decoder) psString() string {
- out := make([]rune, 0, len(d.ps))
- for _, r := range d.ps {
- if r == 0 {
- r = ' '
- }
- out = append(out, r)
- }
- return trimRight(out)
- }
-
- func (d *Decoder) rtString() string {
- out := make([]rune, 0, len(d.rt))
- for _, r := range d.rt {
- if r == 0 {
- r = ' '
- }
- out = append(out, r)
- }
- return trimRight(out)
- }
-
- func trimRight(in []rune) string {
- end := len(in)
- for end > 0 && in[end-1] == ' ' {
- end--
- }
- return string(in[:end])
- }
|