Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

410 wiersze
9.8KB

  1. // cmd/wmdecode — STFT-domain spread-spectrum watermark decoder.
  2. //
  3. // Extracts the embedded fingerprint from an FM broadcast recording
  4. // without needing to know the license key. Optionally verifies
  5. // against supplied keys.
  6. //
  7. // Usage:
  8. //
  9. // wmdecode <file.wav> Extract fingerprint
  10. // wmdecode <file.wav> [key ...] Extract + verify keys
  11. package main
  12. import (
  13. "encoding/binary"
  14. "fmt"
  15. "math"
  16. "math/cmplx"
  17. "os"
  18. "sort"
  19. "time"
  20. "github.com/jan/fm-rds-tx/internal/dsp"
  21. "github.com/jan/fm-rds-tx/internal/watermark"
  22. )
  23. func main() {
  24. if len(os.Args) < 2 {
  25. fmt.Fprintln(os.Stderr, "usage: wmdecode <file.wav> [key ...]")
  26. os.Exit(1)
  27. }
  28. t0 := time.Now()
  29. samples, recRate, err := readMonoWAV(os.Args[1])
  30. if err != nil {
  31. fmt.Fprintf(os.Stderr, "read WAV: %v\n", err)
  32. os.Exit(1)
  33. }
  34. rms := rmsLevel(samples)
  35. fmt.Printf("WAV: %d samples @ %.0f Hz = %.2fs, RMS %.1f dBFS\n",
  36. len(samples), recRate, float64(len(samples))/recRate, 20*math.Log10(rms+1e-9))
  37. // Step 1: Decimate to WMRate (12 kHz)
  38. wmRate := float64(watermark.WMRate)
  39. decimFactor := int(recRate / wmRate)
  40. if decimFactor < 1 {
  41. decimFactor = 1
  42. }
  43. actualRate := recRate / float64(decimFactor)
  44. fmt.Printf("Downsample: %d:1 (%.0f Hz → %.0f Hz)\n", decimFactor, recRate, actualRate)
  45. lpfCoeffs := designLPF8(5500, recRate)
  46. filtered := applyIIR(samples, lpfCoeffs)
  47. nDown := len(filtered) / decimFactor
  48. down := make([]float64, nDown)
  49. for i := 0; i < nDown; i++ {
  50. down[i] = filtered[i*decimFactor]
  51. }
  52. // Step 2: STFT + cepstrum filtering
  53. fftSize := watermark.FFTSize
  54. hop := watermark.FFTHop
  55. nFrames := (nDown - fftSize) / hop
  56. if nFrames <= 0 {
  57. fmt.Fprintln(os.Stderr, "Recording too short")
  58. os.Exit(1)
  59. }
  60. var window [watermark.FFTSize]float64
  61. dsp.HannWindow(window[:])
  62. fmt.Printf("STFT: %d frames (%.1fs required, %.1fs available)\n",
  63. nFrames, float64(watermark.SamplesPerWM)/wmRate, float64(nDown)/wmRate)
  64. type stftMag [watermark.FFTSize / 2]float64
  65. frameMags := make([]stftMag, nFrames)
  66. for f := 0; f < nFrames; f++ {
  67. offset := f * hop
  68. var buf [watermark.FFTSize]complex128
  69. for i := 0; i < fftSize; i++ {
  70. buf[i] = complex(down[offset+i]*window[i], 0)
  71. }
  72. dsp.FFT(buf[:])
  73. for bin := 0; bin < fftSize/2; bin++ {
  74. mag := cmplx.Abs(buf[bin])
  75. if mag < 1e-12 {
  76. mag = 1e-12
  77. }
  78. frameMags[f][bin] = 20 * math.Log10(mag)
  79. }
  80. cepstrumFilter(frameMags[f][:], 8)
  81. }
  82. // Step 3: Cycle-offset search (key-independent — fixed PN)
  83. det := watermark.NewSTFTDetector()
  84. totalGroups := watermark.TotalGroups
  85. timeRep := watermark.TimeRep
  86. framesPerWM := watermark.FramesPerWM
  87. numBins := watermark.NumBins
  88. binLow := watermark.BinLow
  89. centerRep := timeRep / 2
  90. // Precompute for speed: float64 PN chips + group-to-bit + center frame lists
  91. pnF := det.PNChipsFloat()
  92. g2b := det.GroupToBit()
  93. // Center frame indices per repOff (avoids per-frame modulo + branch)
  94. var centerFrames [5][]int
  95. for f := 0; f < nFrames; f++ {
  96. r := f % timeRep
  97. centerFrames[r] = append(centerFrames[r], f)
  98. }
  99. bestMetric := -1.0
  100. var bestCorrs [watermark.PayloadBits]float64
  101. bestCycleOff := 0
  102. bestRepOff := 0
  103. // Parallel search: 5 goroutines (one per rep offset), each searches 1280 cycle offsets.
  104. type searchResult struct {
  105. corrs [watermark.PayloadBits]float64
  106. metric float64
  107. cycleOff int
  108. repOff int
  109. }
  110. results := make(chan searchResult, timeRep)
  111. for repOff := 0; repOff < timeRep; repOff++ {
  112. go func(repOff int) {
  113. cfIdx := (repOff + centerRep) % timeRep
  114. cfs := centerFrames[cfIdx]
  115. var best searchResult
  116. best.repOff = repOff
  117. for cycleOff := 0; cycleOff < framesPerWM; cycleOff += timeRep {
  118. var testCorrs [watermark.PayloadBits]float64
  119. for _, f := range cfs {
  120. wmFrame := ((f - cycleOff - repOff) % framesPerWM + framesPerWM) % framesPerWM
  121. g := wmFrame / timeRep
  122. if g >= totalGroups {
  123. continue
  124. }
  125. var corr float64
  126. for b := 0; b < numBins; b++ {
  127. corr += frameMags[f][binLow+b] * pnF[g][b]
  128. }
  129. testCorrs[g2b[g]] += corr
  130. }
  131. var metric float64
  132. for _, c := range testCorrs {
  133. metric += c * c
  134. }
  135. if metric > best.metric {
  136. best.metric = metric
  137. best.corrs = testCorrs
  138. best.cycleOff = cycleOff
  139. }
  140. }
  141. results <- best
  142. }(repOff)
  143. }
  144. for i := 0; i < timeRep; i++ {
  145. r := <-results
  146. if r.metric > bestMetric {
  147. bestMetric = r.metric
  148. bestCorrs = r.corrs
  149. bestCycleOff = r.cycleOff
  150. bestRepOff = r.repOff
  151. }
  152. }
  153. var sumAbs float64
  154. for _, c := range bestCorrs {
  155. sumAbs += math.Abs(c)
  156. }
  157. fmt.Printf("Sync: cycleOff=%d repOff=%d (%.1fs into recording)\n",
  158. bestCycleOff, bestRepOff, float64(bestCycleOff*hop)/wmRate)
  159. fmt.Printf("Corrs: avg|c|=%.1f\n", sumAbs/128)
  160. // Step 4: RS decode — extract fingerprint
  161. var recv [watermark.RsTotalBytes]byte
  162. confs := make([]float64, watermark.PayloadBits)
  163. for i := 0; i < watermark.PayloadBits; i++ {
  164. confs[i] = math.Abs(bestCorrs[i])
  165. if bestCorrs[i] < 0 {
  166. recv[i/8] |= 1 << uint(7-(i%8))
  167. }
  168. }
  169. type bc struct{ idx int; conf float64 }
  170. byteConfs := make([]bc, watermark.RsTotalBytes)
  171. for b := 0; b < watermark.RsTotalBytes; b++ {
  172. minC := confs[b*8]
  173. for bit := 1; bit < 8; bit++ {
  174. if confs[b*8+bit] < minC {
  175. minC = confs[b*8+bit]
  176. }
  177. }
  178. byteConfs[b] = bc{b, minC}
  179. }
  180. sort.Slice(byteConfs, func(a, b int) bool { return byteConfs[a].conf < byteConfs[b].conf })
  181. var payload [watermark.RsDataBytes]byte
  182. decoded := false
  183. nErasures := 0
  184. for nErase := 0; nErase <= watermark.RsCheckBytes; nErase++ {
  185. if nErase == 0 {
  186. p, ok := watermark.RSDecode(recv, nil)
  187. if ok {
  188. payload = p
  189. decoded = true
  190. break
  191. }
  192. continue
  193. }
  194. erasePos := make([]int, nErase)
  195. for i := 0; i < nErase; i++ {
  196. erasePos[i] = byteConfs[i].idx
  197. }
  198. sort.Ints(erasePos)
  199. p, ok := watermark.RSDecode(recv, erasePos)
  200. if ok {
  201. payload = p
  202. nErasures = nErase
  203. decoded = true
  204. break
  205. }
  206. }
  207. if !decoded {
  208. fmt.Printf("\nWatermark: NOT DETECTED\n")
  209. fmt.Printf("Done in %v\n", time.Since(t0).Round(time.Millisecond))
  210. os.Exit(1)
  211. }
  212. fmt.Printf("\nWatermark: DETECTED (%d erasures)\n", nErasures)
  213. fmt.Printf("Fingerprint: %x\n", payload)
  214. // Optional: verify against supplied keys
  215. keys := os.Args[2:]
  216. if len(keys) > 0 {
  217. fmt.Println()
  218. for _, key := range keys {
  219. if watermark.KeyMatchesPayload(key, payload) {
  220. fmt.Printf(" ✓ MATCH: %q\n", key)
  221. } else {
  222. fmt.Printf(" ✗ %q\n", key)
  223. }
  224. }
  225. }
  226. fmt.Printf("\nDone in %v\n", time.Since(t0).Round(time.Millisecond))
  227. }
  228. // --- Cepstrum filter ---
  229. var cosTable [][]float64
  230. func initCosTable(n int) {
  231. cosTable = make([][]float64, n)
  232. for k := 0; k < n; k++ {
  233. cosTable[k] = make([]float64, n)
  234. for i := 0; i < n; i++ {
  235. cosTable[k][i] = math.Cos(math.Pi * float64(k) * (float64(i) + 0.5) / float64(n))
  236. }
  237. }
  238. }
  239. func cepstrumFilter(magDB []float64, nCeps int) {
  240. n := len(magDB)
  241. if n < nCeps*2 {
  242. return
  243. }
  244. if len(cosTable) != n {
  245. initCosTable(n)
  246. }
  247. ceps := make([]float64, n)
  248. for k := 0; k < n; k++ {
  249. var sum float64
  250. row := cosTable[k]
  251. for i := 0; i < n; i++ {
  252. sum += magDB[i] * row[i]
  253. }
  254. ceps[k] = sum
  255. }
  256. for k := 0; k < nCeps; k++ {
  257. ceps[k] = 0
  258. }
  259. scale := 2.0 / float64(n)
  260. for i := 0; i < n; i++ {
  261. var sum float64
  262. for k := 0; k < n; k++ {
  263. w := 1.0
  264. if k == 0 {
  265. w = 0.5
  266. }
  267. sum += w * ceps[k] * cosTable[k][i]
  268. }
  269. magDB[i] = sum * scale
  270. }
  271. }
  272. // --- LPF ---
  273. type biquad struct{ b0, b1, b2, a1, a2 float64 }
  274. type iirCoeffs []biquad
  275. func designLPF8(cutoffHz, sampleRate float64) iirCoeffs {
  276. angles := []float64{math.Pi / 16, 3 * math.Pi / 16, 5 * math.Pi / 16, 7 * math.Pi / 16}
  277. coeffs := make(iirCoeffs, 4)
  278. for i, angle := range angles {
  279. q := 1.0 / (2 * math.Cos(angle))
  280. omega := 2 * math.Pi * cutoffHz / sampleRate
  281. cosW := math.Cos(omega)
  282. sinW := math.Sin(omega)
  283. alpha := sinW / (2 * q)
  284. a0 := 1 + alpha
  285. coeffs[i] = biquad{
  286. b0: (1 - cosW) / 2 / a0, b1: (1 - cosW) / a0, b2: (1 - cosW) / 2 / a0,
  287. a1: (-2 * cosW) / a0, a2: (1 - alpha) / a0,
  288. }
  289. }
  290. return coeffs
  291. }
  292. func applyIIR(samples []float64, coeffs iirCoeffs) []float64 {
  293. out := make([]float64, len(samples))
  294. copy(out, samples)
  295. for _, bq := range coeffs {
  296. var z1, z2 float64
  297. for i, x := range out {
  298. y := bq.b0*x + z1
  299. z1 = bq.b1*x - bq.a1*y + z2
  300. z2 = bq.b2*x - bq.a2*y
  301. out[i] = y
  302. }
  303. }
  304. return out
  305. }
  306. // --- WAV reader ---
  307. func rmsLevel(s []float64) float64 {
  308. var acc float64
  309. for _, v := range s {
  310. acc += v * v
  311. }
  312. return math.Sqrt(acc / float64(len(s)))
  313. }
  314. func readMonoWAV(path string) ([]float64, float64, error) {
  315. data, err := os.ReadFile(path)
  316. if err != nil {
  317. return nil, 0, err
  318. }
  319. if len(data) < 44 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
  320. return nil, 0, fmt.Errorf("not a RIFF/WAVE file")
  321. }
  322. var channels, bitsPerSample uint16
  323. var sampleRate uint32
  324. var dataStart, dataLen int
  325. i := 12
  326. for i+8 <= len(data) {
  327. id := string(data[i : i+4])
  328. sz := int(binary.LittleEndian.Uint32(data[i+4 : i+8]))
  329. i += 8
  330. switch id {
  331. case "fmt ":
  332. if sz >= 16 {
  333. channels = binary.LittleEndian.Uint16(data[i+2 : i+4])
  334. sampleRate = binary.LittleEndian.Uint32(data[i+4 : i+8])
  335. bitsPerSample = binary.LittleEndian.Uint16(data[i+14 : i+16])
  336. }
  337. case "data":
  338. dataStart, dataLen = i, sz
  339. }
  340. i += sz
  341. if sz%2 != 0 {
  342. i++
  343. }
  344. if dataStart > 0 && channels > 0 {
  345. break
  346. }
  347. }
  348. if dataStart == 0 || bitsPerSample != 16 || channels == 0 {
  349. return nil, 0, fmt.Errorf("unsupported WAV")
  350. }
  351. if dataStart+dataLen > len(data) {
  352. dataLen = len(data) - dataStart
  353. }
  354. step := int(channels) * 2
  355. nFrames := dataLen / step
  356. out := make([]float64, nFrames)
  357. for j := 0; j < nFrames; j++ {
  358. off := dataStart + j*step
  359. l := float64(int16(binary.LittleEndian.Uint16(data[off : off+2])))
  360. r := l
  361. if channels >= 2 {
  362. r = float64(int16(binary.LittleEndian.Uint16(data[off+2 : off+4])))
  363. }
  364. out[j] = (l + r) / 2.0 / 32768.0
  365. }
  366. return out, float64(sampleRate), nil
  367. }