Ver código fonte

Improve RDS decode with block sync and RT

master
Jan Svabenik 4 dias atrás
pai
commit
e11aa95d8b
2 arquivos alterados com 116 adições e 43 exclusões
  1. +113
    -43
      internal/rds/rds.go
  2. +3
    -0
      internal/recorder/demod.go

+ 113
- 43
internal/rds/rds.go Ver arquivo

@@ -1,21 +1,22 @@
package rds

import (
"math"
)
import "math"

// Decoder performs a simple RDS baseband decode (BPSK, 1187.5 bps).
type Decoder struct {
lastPS string
ps [8]rune
rt [64]rune
lastPI uint16
}

type Result struct {
PI uint16 `json:"pi"`
PS string `json:"ps"`
RT string `json:"rt"`
}

// Decode takes baseband samples at ~2400 Hz and attempts to extract PI/PS.
// Decode takes baseband samples at ~2400 Hz and attempts to extract PI/PS/RT.
// NOTE: lightweight decoder with CRC+block sync; not a full RDS implementation.
func (d *Decoder) Decode(base []float32, sampleRate int) Result {
if len(base) == 0 || sampleRate <= 0 {
return Result{}
@@ -23,69 +24,138 @@ func (d *Decoder) Decode(base []float32, sampleRate int) Result {
// crude clock: 1187.5 bps
baud := 1187.5
spb := float64(sampleRate) / baud
// carrier recovery simplified: assume baseband already mixed
bits := make([]int, 0, int(float64(len(base))/spb))
phase := 0.0
for i := 0; i < len(base); i++ {
phase += 1.0
if phase >= spb {
phase -= spb
// slice decision
v := base[i]
if v >= 0 {
if base[i] >= 0 {
bits = append(bits, 1)
} else {
bits = append(bits, 0)
}
}
}
// parse groups (very naive): look for 16-bit blocks and decode group type 0A for PS
// This is a placeholder: real RDS needs CRC and block sync.
if len(bits) < 104 {
return Result{PI: d.lastPI, PS: d.lastPS}
}
// best effort: just map first 16 bits to PI and next 8 chars from consecutive bytes
pi := bitsToU16(bits[0:16])
ps := decodePS(bits)
if pi != 0 {
d.lastPI = pi
if len(bits) < 26*4 {
return Result{PI: d.lastPI, PS: d.psString(), RT: d.rtString()}
}
if ps != "" {
d.lastPS = ps
// search for block sync
for i := 0; i+26*4 <= len(bits); i++ {
bA, okA := decodeBlock(bits[i : i+26])
bB, okB := decodeBlock(bits[i+26 : i+52])
bC, okC := decodeBlock(bits[i+52 : i+78])
bD, okD := decodeBlock(bits[i+78 : i+104])
if !(okA && okB && okC && okD) {
continue
}
if bA.offset != offA || bB.offset != offB || bC.offset != offC || bD.offset != offD {
continue
}
pi := bA.data
if pi != 0 {
d.lastPI = pi
}
groupType := (bB.data >> 12) & 0xF
versionA := ((bB.data >> 11) & 0x1) == 0
if groupType == 0 && versionA {
addr := bB.data & 0x3
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])
}
}
break
}
return Result{PI: d.lastPI, PS: d.lastPS}
return Result{PI: d.lastPI, PS: d.psString(), RT: d.rtString()}
}

type block struct {
data uint16
offset uint16
}

func bitsToU16(bits []int) uint16 {
var v uint16
const (
offA uint16 = 0x0FC
offB uint16 = 0x198
offC uint16 = 0x168
offD uint16 = 0x1B4
)

func decodeBlock(bits []int) (block, bool) {
if len(bits) != 26 {
return block{}, false
}
var raw uint32
for _, b := range bits {
v = (v << 1) | uint16(b&1)
raw = (raw << 1) | uint32(b&1)
}
data := uint16(raw >> 10)
synd := crcSyndrome(raw)
if synd == 0 {
return block{}, false
}
return v
// use syndrome as offset word
return block{data: data, offset: uint16(synd)}, true
}

func decodePS(bits []int) string {
// naive: take next 64 bits as 8 ASCII chars
if len(bits) < 16+64 {
return ""
func crcSyndrome(raw uint32) uint16 {
// polynomial 0x1B9 (10-bit)
var reg uint32 = raw
poly := uint32(0x1B9)
for i := 25; i >= 10; i-- {
if (reg>>uint(i))&1 == 1 {
reg ^= poly << uint(i-10)
}
}
start := 16
out := make([]rune, 0, 8)
for i := 0; i < 8; i++ {
var c byte
for j := 0; j < 8; j++ {
c = (c << 1) | byte(bits[start+i*8+j]&1)
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 = ' '
}
if c < 32 || c > 126 {
c = ' '
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, rune(c))
out = append(out, r)
}
// trim
for len(out) > 0 && out[len(out)-1] == ' ' {
out = out[:len(out)-1]
return trimRight(out)
}

func trimRight(in []rune) string {
end := len(in)
for end > 0 && in[end-1] == ' ' {
end--
}
return string(out)
return string(in[:end])
}

// BPSKCostas returns a simple carrier-locked version of baseband (placeholder).


+ 3
- 0
internal/recorder/demod.go Ver arquivo

@@ -61,6 +61,9 @@ func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, f
if res.PS != "" {
files["rds_ps"] = res.PS
}
if res.RT != "" {
files["rds_rt"] = res.RT
}
}
}
return nil


Carregando…
Cancelar
Salvar