Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

364 Zeilen
9.1KB

  1. // cmd/wmdecode — STFT-domain spread-spectrum watermark decoder.
  2. //
  3. // Decodes watermark from FM broadcast recordings following
  4. // Kirovski & Malvar (IEEE TSP 2003) architecture.
  5. //
  6. // Usage:
  7. //
  8. // wmdecode <file.wav> [key ...]
  9. package main
  10. import (
  11. "encoding/binary"
  12. "fmt"
  13. "math"
  14. "math/cmplx"
  15. "os"
  16. "sort"
  17. "time"
  18. "github.com/jan/fm-rds-tx/internal/dsp"
  19. "github.com/jan/fm-rds-tx/internal/watermark"
  20. )
  21. func main() {
  22. if len(os.Args) < 2 {
  23. fmt.Fprintln(os.Stderr, "usage: wmdecode <file.wav> [key ...]")
  24. os.Exit(1)
  25. }
  26. t0 := time.Now()
  27. samples, recRate, err := readMonoWAV(os.Args[1])
  28. if err != nil {
  29. fmt.Fprintf(os.Stderr, "read WAV: %v\n", err)
  30. os.Exit(1)
  31. }
  32. rms := rmsLevel(samples)
  33. fmt.Printf("WAV: %d samples @ %.0f Hz = %.2fs, RMS %.1f dBFS\n",
  34. len(samples), recRate, float64(len(samples))/recRate, 20*math.Log10(rms+1e-9))
  35. // Step 1: Decimate to WMRate (12 kHz)
  36. wmRate := float64(watermark.WMRate)
  37. decimFactor := int(recRate / wmRate)
  38. if decimFactor < 1 {
  39. decimFactor = 1
  40. }
  41. actualRate := recRate / float64(decimFactor)
  42. fmt.Printf("Downsample: %d:1 (%.0f Hz → %.0f Hz)\n", decimFactor, recRate, actualRate)
  43. lpfCoeffs := designLPF8(5500, recRate)
  44. filtered := applyIIR(samples, lpfCoeffs)
  45. nDown := len(filtered) / decimFactor
  46. down := make([]float64, nDown)
  47. for i := 0; i < nDown; i++ {
  48. down[i] = filtered[i*decimFactor]
  49. }
  50. fmt.Printf("Downsampled: %d samples, %.1fs\n", nDown, float64(nDown)/wmRate)
  51. // Step 2: Compute ALL STFT frames with cepstrum filtering
  52. fftSize := watermark.FFTSize
  53. hop := watermark.FFTHop
  54. nFrames := (nDown - fftSize) / hop
  55. if nFrames <= 0 {
  56. fmt.Fprintln(os.Stderr, "Recording too short")
  57. os.Exit(1)
  58. }
  59. var window [watermark.FFTSize]float64
  60. dsp.HannWindow(window[:])
  61. fmt.Printf("STFT: %d frames (%d-point, hop=%d)\n", nFrames, fftSize, hop)
  62. type stftMag [watermark.FFTSize / 2]float64
  63. frameMags := make([]stftMag, nFrames)
  64. for f := 0; f < nFrames; f++ {
  65. offset := f * hop
  66. var buf [watermark.FFTSize]complex128
  67. for i := 0; i < fftSize; i++ {
  68. buf[i] = complex(down[offset+i]*window[i], 0)
  69. }
  70. dsp.FFT(buf[:])
  71. for bin := 0; bin < fftSize/2; bin++ {
  72. mag := cmplx.Abs(buf[bin])
  73. if mag < 1e-12 {
  74. mag = 1e-12
  75. }
  76. frameMags[f][bin] = 20 * math.Log10(mag)
  77. }
  78. cepstrumFilter(frameMags[f][:], 8)
  79. }
  80. // Step 3: For each key, search cycle offset + rep offset
  81. keys := os.Args[2:]
  82. if len(keys) == 0 {
  83. fmt.Println("No keys supplied.")
  84. os.Exit(1)
  85. }
  86. for _, key := range keys {
  87. fmt.Printf("\nKey: %q\n", key)
  88. det := watermark.NewSTFTDetector(key)
  89. totalGroups := watermark.TotalGroups
  90. timeRep := watermark.TimeRep
  91. framesPerWM := watermark.FramesPerWM
  92. numBins := watermark.NumBins
  93. binLow := watermark.BinLow
  94. centerRep := timeRep / 2
  95. bestMetric := -1.0
  96. var bestCorrs [watermark.PayloadBits]float64
  97. bestCycleOff := 0
  98. bestRepOff := 0
  99. nCandidates := 0
  100. for cycleOff := 0; cycleOff < framesPerWM; cycleOff += timeRep {
  101. for repOff := 0; repOff < timeRep; repOff++ {
  102. var testCorrs [watermark.PayloadBits]float64
  103. for f := 0; f < nFrames; f++ {
  104. wmFrame := ((f - cycleOff - repOff) % framesPerWM + framesPerWM) % framesPerWM
  105. if wmFrame%timeRep != centerRep {
  106. continue
  107. }
  108. g := wmFrame / timeRep
  109. if g >= totalGroups {
  110. continue
  111. }
  112. var corr float64
  113. for b := 0; b < numBins; b++ {
  114. corr += frameMags[f][binLow+b] * float64(det.PNChipAt(g, b))
  115. }
  116. testCorrs[det.GroupBit(g)] += corr
  117. }
  118. var metric float64
  119. for _, c := range testCorrs {
  120. metric += c * c
  121. }
  122. if metric > bestMetric {
  123. bestMetric = metric
  124. bestCorrs = testCorrs
  125. bestCycleOff = cycleOff
  126. bestRepOff = repOff
  127. }
  128. nCandidates++
  129. }
  130. }
  131. fmt.Printf("Searched %d candidates in %v\n", nCandidates, time.Since(t0).Round(time.Millisecond))
  132. fmt.Printf("Best: cycleOff=%d, repOff=%d, metric=%.0f\n", bestCycleOff, bestRepOff, bestMetric)
  133. var sumAbs float64
  134. for _, c := range bestCorrs {
  135. sumAbs += math.Abs(c)
  136. }
  137. fmt.Printf("Corrs: avg|c|=%.1f\n", sumAbs/128)
  138. // BER diagnostic against known key
  139. knownPayload := watermark.KeyToPayload(key)
  140. knownCW := watermark.RSEncode(knownPayload)
  141. var knownBits [watermark.PayloadBits]int
  142. for i := 0; i < watermark.PayloadBits; i++ {
  143. knownBits[i] = int((knownCW[i/8] >> uint(7-(i%8))) & 1)
  144. }
  145. nerr := 0
  146. for i := 0; i < watermark.PayloadBits; i++ {
  147. hard := 0
  148. if bestCorrs[i] < 0 {
  149. hard = 1
  150. }
  151. if hard != knownBits[i] {
  152. nerr++
  153. }
  154. }
  155. fmt.Printf("BER: %d/128 (%.1f%%)\n", nerr, 100*float64(nerr)/128)
  156. // Show recv vs expected
  157. var recv [watermark.RsTotalBytes]byte
  158. confs := make([]float64, watermark.PayloadBits)
  159. for i := 0; i < watermark.PayloadBits; i++ {
  160. confs[i] = math.Abs(bestCorrs[i])
  161. if bestCorrs[i] < 0 {
  162. recv[i/8] |= 1 << uint(7-(i%8))
  163. }
  164. }
  165. fmt.Printf("recv: %x\nwant: %x\n", recv, knownCW)
  166. // Confidence-based erasure (MIN bit confidence per byte)
  167. type bc struct{ idx int; conf float64 }
  168. byteConfs := make([]bc, watermark.RsTotalBytes)
  169. for b := 0; b < watermark.RsTotalBytes; b++ {
  170. minC := confs[b*8]
  171. for bit := 1; bit < 8; bit++ {
  172. if confs[b*8+bit] < minC {
  173. minC = confs[b*8+bit]
  174. }
  175. }
  176. byteConfs[b] = bc{b, minC}
  177. }
  178. sort.Slice(byteConfs, func(a, b int) bool { return byteConfs[a].conf < byteConfs[b].conf })
  179. decoded := false
  180. for nErase := 0; nErase <= watermark.RsCheckBytes; nErase++ {
  181. if nErase == 0 {
  182. p, ok := watermark.RSDecode(recv, nil)
  183. if ok && watermark.KeyMatchesPayload(key, p) {
  184. fmt.Printf(" ✓ MATCH (0 erasures), payload=%x\n", p)
  185. decoded = true
  186. break
  187. }
  188. continue
  189. }
  190. erasePos := make([]int, nErase)
  191. for i := 0; i < nErase; i++ {
  192. erasePos[i] = byteConfs[i].idx
  193. }
  194. sort.Ints(erasePos)
  195. p, ok := watermark.RSDecode(recv, erasePos)
  196. if ok && watermark.KeyMatchesPayload(key, p) {
  197. fmt.Printf(" ✓ MATCH (%d erasures), payload=%x\n", nErase, p)
  198. decoded = true
  199. break
  200. }
  201. }
  202. if !decoded {
  203. fmt.Println(" ✗ NOT FOUND")
  204. }
  205. }
  206. fmt.Printf("\nDone in %v\n", time.Since(t0).Round(time.Millisecond))
  207. }
  208. func cepstrumFilter(magDB []float64, nCeps int) {
  209. n := len(magDB)
  210. if n < nCeps*2 {
  211. return
  212. }
  213. ceps := make([]float64, n)
  214. for k := 0; k < n; k++ {
  215. var sum float64
  216. for i := 0; i < n; i++ {
  217. sum += magDB[i] * math.Cos(math.Pi*float64(k)*(float64(i)+0.5)/float64(n))
  218. }
  219. ceps[k] = sum
  220. }
  221. for k := 0; k < nCeps; k++ {
  222. ceps[k] = 0
  223. }
  224. for i := 0; i < n; i++ {
  225. var sum float64
  226. for k := 0; k < n; k++ {
  227. w := 1.0
  228. if k == 0 {
  229. w = 0.5
  230. }
  231. sum += w * ceps[k] * math.Cos(math.Pi*float64(k)*(float64(i)+0.5)/float64(n))
  232. }
  233. magDB[i] = sum * 2.0 / float64(n)
  234. }
  235. }
  236. type biquad struct{ b0, b1, b2, a1, a2 float64 }
  237. type iirCoeffs []biquad
  238. func designLPF8(cutoffHz, sampleRate float64) iirCoeffs {
  239. angles := []float64{math.Pi / 16, 3 * math.Pi / 16, 5 * math.Pi / 16, 7 * math.Pi / 16}
  240. coeffs := make(iirCoeffs, 4)
  241. for i, angle := range angles {
  242. q := 1.0 / (2 * math.Cos(angle))
  243. omega := 2 * math.Pi * cutoffHz / sampleRate
  244. cosW := math.Cos(omega)
  245. sinW := math.Sin(omega)
  246. alpha := sinW / (2 * q)
  247. a0 := 1 + alpha
  248. coeffs[i] = biquad{
  249. b0: (1 - cosW) / 2 / a0, b1: (1 - cosW) / a0, b2: (1 - cosW) / 2 / a0,
  250. a1: (-2 * cosW) / a0, a2: (1 - alpha) / a0,
  251. }
  252. }
  253. return coeffs
  254. }
  255. func applyIIR(samples []float64, coeffs iirCoeffs) []float64 {
  256. out := make([]float64, len(samples))
  257. copy(out, samples)
  258. for _, bq := range coeffs {
  259. var z1, z2 float64
  260. for i, x := range out {
  261. y := bq.b0*x + z1
  262. z1 = bq.b1*x - bq.a1*y + z2
  263. z2 = bq.b2*x - bq.a2*y
  264. out[i] = y
  265. }
  266. }
  267. return out
  268. }
  269. func rmsLevel(s []float64) float64 {
  270. var acc float64
  271. for _, v := range s {
  272. acc += v * v
  273. }
  274. return math.Sqrt(acc / float64(len(s)))
  275. }
  276. func readMonoWAV(path string) ([]float64, float64, error) {
  277. data, err := os.ReadFile(path)
  278. if err != nil {
  279. return nil, 0, err
  280. }
  281. if len(data) < 44 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
  282. return nil, 0, fmt.Errorf("not a RIFF/WAVE file")
  283. }
  284. var channels, bitsPerSample uint16
  285. var sampleRate uint32
  286. var dataStart, dataLen int
  287. i := 12
  288. for i+8 <= len(data) {
  289. id := string(data[i : i+4])
  290. sz := int(binary.LittleEndian.Uint32(data[i+4 : i+8]))
  291. i += 8
  292. switch id {
  293. case "fmt ":
  294. if sz >= 16 {
  295. channels = binary.LittleEndian.Uint16(data[i+2 : i+4])
  296. sampleRate = binary.LittleEndian.Uint32(data[i+4 : i+8])
  297. bitsPerSample = binary.LittleEndian.Uint16(data[i+14 : i+16])
  298. }
  299. case "data":
  300. dataStart, dataLen = i, sz
  301. }
  302. i += sz
  303. if sz%2 != 0 {
  304. i++
  305. }
  306. if dataStart > 0 && channels > 0 {
  307. break
  308. }
  309. }
  310. if dataStart == 0 || bitsPerSample != 16 || channels == 0 {
  311. return nil, 0, fmt.Errorf("unsupported WAV")
  312. }
  313. if dataStart+dataLen > len(data) {
  314. dataLen = len(data) - dataStart
  315. }
  316. step := int(channels) * 2
  317. nFrames := dataLen / step
  318. out := make([]float64, nFrames)
  319. for j := 0; j < nFrames; j++ {
  320. off := dataStart + j*step
  321. l := float64(int16(binary.LittleEndian.Uint16(data[off : off+2])))
  322. r := l
  323. if channels >= 2 {
  324. r = float64(int16(binary.LittleEndian.Uint16(data[off+2 : off+4])))
  325. }
  326. out[j] = (l + r) / 2.0 / 32768.0
  327. }
  328. return out, float64(sampleRate), nil
  329. }