|
|
|
@@ -0,0 +1,100 @@ |
|
|
|
package rds |
|
|
|
|
|
|
|
import ( |
|
|
|
"math" |
|
|
|
) |
|
|
|
|
|
|
|
// Decoder performs a simple RDS baseband decode (BPSK, 1187.5 bps). |
|
|
|
type Decoder struct { |
|
|
|
lastPS string |
|
|
|
lastPI uint16 |
|
|
|
} |
|
|
|
|
|
|
|
type Result struct { |
|
|
|
PI uint16 `json:"pi"` |
|
|
|
PS string `json:"ps"` |
|
|
|
} |
|
|
|
|
|
|
|
// Decode takes baseband samples at ~2400 Hz and attempts to extract PI/PS. |
|
|
|
func (d *Decoder) Decode(base []float32, sampleRate int) Result { |
|
|
|
if len(base) == 0 || sampleRate <= 0 { |
|
|
|
return 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 { |
|
|
|
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 ps != "" { |
|
|
|
d.lastPS = ps |
|
|
|
} |
|
|
|
return Result{PI: d.lastPI, PS: d.lastPS} |
|
|
|
} |
|
|
|
|
|
|
|
func bitsToU16(bits []int) uint16 { |
|
|
|
var v uint16 |
|
|
|
for _, b := range bits { |
|
|
|
v = (v << 1) | uint16(b&1) |
|
|
|
} |
|
|
|
return v |
|
|
|
} |
|
|
|
|
|
|
|
func decodePS(bits []int) string { |
|
|
|
// naive: take next 64 bits as 8 ASCII chars |
|
|
|
if len(bits) < 16+64 { |
|
|
|
return "" |
|
|
|
} |
|
|
|
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) |
|
|
|
} |
|
|
|
if c < 32 || c > 126 { |
|
|
|
c = ' ' |
|
|
|
} |
|
|
|
out = append(out, rune(c)) |
|
|
|
} |
|
|
|
// trim |
|
|
|
for len(out) > 0 && out[len(out)-1] == ' ' { |
|
|
|
out = out[:len(out)-1] |
|
|
|
} |
|
|
|
return string(out) |
|
|
|
} |
|
|
|
|
|
|
|
// BPSKCostas returns a simple carrier-locked version of baseband (placeholder). |
|
|
|
func BPSKCostas(in []float32) []float32 { |
|
|
|
out := make([]float32, len(in)) |
|
|
|
var phase float64 |
|
|
|
for i, v := range in { |
|
|
|
phase += 0.0001 * float64(v) * math.Sin(phase) |
|
|
|
out[i] = float32(float64(v) * math.Cos(phase)) |
|
|
|
} |
|
|
|
return out |
|
|
|
} |