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.

356 Zeilen
8.7KB

  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. bestMetric := -1.0
  91. var bestCorrs [watermark.PayloadBits]float64
  92. bestCycleOff := 0
  93. bestRepOff := 0
  94. for cycleOff := 0; cycleOff < framesPerWM; cycleOff += timeRep {
  95. for repOff := 0; repOff < timeRep; repOff++ {
  96. var testCorrs [watermark.PayloadBits]float64
  97. for f := 0; f < nFrames; f++ {
  98. wmFrame := ((f - cycleOff - repOff) % framesPerWM + framesPerWM) % framesPerWM
  99. if wmFrame%timeRep != centerRep {
  100. continue
  101. }
  102. g := wmFrame / timeRep
  103. if g >= totalGroups {
  104. continue
  105. }
  106. var corr float64
  107. for b := 0; b < numBins; b++ {
  108. corr += frameMags[f][binLow+b] * float64(det.PNChipAt(g, b))
  109. }
  110. testCorrs[det.GroupBit(g)] += corr
  111. }
  112. var metric float64
  113. for _, c := range testCorrs {
  114. metric += c * c
  115. }
  116. if metric > bestMetric {
  117. bestMetric = metric
  118. bestCorrs = testCorrs
  119. bestCycleOff = cycleOff
  120. bestRepOff = repOff
  121. }
  122. }
  123. }
  124. var sumAbs float64
  125. for _, c := range bestCorrs {
  126. sumAbs += math.Abs(c)
  127. }
  128. fmt.Printf("Sync: cycleOff=%d repOff=%d (%.1fs into recording)\n",
  129. bestCycleOff, bestRepOff, float64(bestCycleOff*hop)/wmRate)
  130. fmt.Printf("Corrs: avg|c|=%.1f\n", sumAbs/128)
  131. // Step 4: RS decode — extract fingerprint
  132. var recv [watermark.RsTotalBytes]byte
  133. confs := make([]float64, watermark.PayloadBits)
  134. for i := 0; i < watermark.PayloadBits; i++ {
  135. confs[i] = math.Abs(bestCorrs[i])
  136. if bestCorrs[i] < 0 {
  137. recv[i/8] |= 1 << uint(7-(i%8))
  138. }
  139. }
  140. type bc struct{ idx int; conf float64 }
  141. byteConfs := make([]bc, watermark.RsTotalBytes)
  142. for b := 0; b < watermark.RsTotalBytes; b++ {
  143. minC := confs[b*8]
  144. for bit := 1; bit < 8; bit++ {
  145. if confs[b*8+bit] < minC {
  146. minC = confs[b*8+bit]
  147. }
  148. }
  149. byteConfs[b] = bc{b, minC}
  150. }
  151. sort.Slice(byteConfs, func(a, b int) bool { return byteConfs[a].conf < byteConfs[b].conf })
  152. var payload [watermark.RsDataBytes]byte
  153. decoded := false
  154. nErasures := 0
  155. for nErase := 0; nErase <= watermark.RsCheckBytes; nErase++ {
  156. if nErase == 0 {
  157. p, ok := watermark.RSDecode(recv, nil)
  158. if ok {
  159. payload = p
  160. decoded = true
  161. break
  162. }
  163. continue
  164. }
  165. erasePos := make([]int, nErase)
  166. for i := 0; i < nErase; i++ {
  167. erasePos[i] = byteConfs[i].idx
  168. }
  169. sort.Ints(erasePos)
  170. p, ok := watermark.RSDecode(recv, erasePos)
  171. if ok {
  172. payload = p
  173. nErasures = nErase
  174. decoded = true
  175. break
  176. }
  177. }
  178. if !decoded {
  179. fmt.Printf("\nWatermark: NOT DETECTED\n")
  180. fmt.Printf("Done in %v\n", time.Since(t0).Round(time.Millisecond))
  181. os.Exit(1)
  182. }
  183. fmt.Printf("\nWatermark: DETECTED (%d erasures)\n", nErasures)
  184. fmt.Printf("Fingerprint: %x\n", payload)
  185. // Optional: verify against supplied keys
  186. keys := os.Args[2:]
  187. if len(keys) > 0 {
  188. fmt.Println()
  189. for _, key := range keys {
  190. if watermark.KeyMatchesPayload(key, payload) {
  191. fmt.Printf(" ✓ MATCH: %q\n", key)
  192. } else {
  193. fmt.Printf(" ✗ %q\n", key)
  194. }
  195. }
  196. }
  197. fmt.Printf("\nDone in %v\n", time.Since(t0).Round(time.Millisecond))
  198. }
  199. // --- Cepstrum filter ---
  200. func cepstrumFilter(magDB []float64, nCeps int) {
  201. n := len(magDB)
  202. if n < nCeps*2 {
  203. return
  204. }
  205. ceps := make([]float64, n)
  206. for k := 0; k < n; k++ {
  207. var sum float64
  208. for i := 0; i < n; i++ {
  209. sum += magDB[i] * math.Cos(math.Pi*float64(k)*(float64(i)+0.5)/float64(n))
  210. }
  211. ceps[k] = sum
  212. }
  213. for k := 0; k < nCeps; k++ {
  214. ceps[k] = 0
  215. }
  216. for i := 0; i < n; i++ {
  217. var sum float64
  218. for k := 0; k < n; k++ {
  219. w := 1.0
  220. if k == 0 {
  221. w = 0.5
  222. }
  223. sum += w * ceps[k] * math.Cos(math.Pi*float64(k)*(float64(i)+0.5)/float64(n))
  224. }
  225. magDB[i] = sum * 2.0 / float64(n)
  226. }
  227. }
  228. // --- LPF ---
  229. type biquad struct{ b0, b1, b2, a1, a2 float64 }
  230. type iirCoeffs []biquad
  231. func designLPF8(cutoffHz, sampleRate float64) iirCoeffs {
  232. angles := []float64{math.Pi / 16, 3 * math.Pi / 16, 5 * math.Pi / 16, 7 * math.Pi / 16}
  233. coeffs := make(iirCoeffs, 4)
  234. for i, angle := range angles {
  235. q := 1.0 / (2 * math.Cos(angle))
  236. omega := 2 * math.Pi * cutoffHz / sampleRate
  237. cosW := math.Cos(omega)
  238. sinW := math.Sin(omega)
  239. alpha := sinW / (2 * q)
  240. a0 := 1 + alpha
  241. coeffs[i] = biquad{
  242. b0: (1 - cosW) / 2 / a0, b1: (1 - cosW) / a0, b2: (1 - cosW) / 2 / a0,
  243. a1: (-2 * cosW) / a0, a2: (1 - alpha) / a0,
  244. }
  245. }
  246. return coeffs
  247. }
  248. func applyIIR(samples []float64, coeffs iirCoeffs) []float64 {
  249. out := make([]float64, len(samples))
  250. copy(out, samples)
  251. for _, bq := range coeffs {
  252. var z1, z2 float64
  253. for i, x := range out {
  254. y := bq.b0*x + z1
  255. z1 = bq.b1*x - bq.a1*y + z2
  256. z2 = bq.b2*x - bq.a2*y
  257. out[i] = y
  258. }
  259. }
  260. return out
  261. }
  262. // --- WAV reader ---
  263. func rmsLevel(s []float64) float64 {
  264. var acc float64
  265. for _, v := range s {
  266. acc += v * v
  267. }
  268. return math.Sqrt(acc / float64(len(s)))
  269. }
  270. func readMonoWAV(path string) ([]float64, float64, error) {
  271. data, err := os.ReadFile(path)
  272. if err != nil {
  273. return nil, 0, err
  274. }
  275. if len(data) < 44 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
  276. return nil, 0, fmt.Errorf("not a RIFF/WAVE file")
  277. }
  278. var channels, bitsPerSample uint16
  279. var sampleRate uint32
  280. var dataStart, dataLen int
  281. i := 12
  282. for i+8 <= len(data) {
  283. id := string(data[i : i+4])
  284. sz := int(binary.LittleEndian.Uint32(data[i+4 : i+8]))
  285. i += 8
  286. switch id {
  287. case "fmt ":
  288. if sz >= 16 {
  289. channels = binary.LittleEndian.Uint16(data[i+2 : i+4])
  290. sampleRate = binary.LittleEndian.Uint32(data[i+4 : i+8])
  291. bitsPerSample = binary.LittleEndian.Uint16(data[i+14 : i+16])
  292. }
  293. case "data":
  294. dataStart, dataLen = i, sz
  295. }
  296. i += sz
  297. if sz%2 != 0 {
  298. i++
  299. }
  300. if dataStart > 0 && channels > 0 {
  301. break
  302. }
  303. }
  304. if dataStart == 0 || bitsPerSample != 16 || channels == 0 {
  305. return nil, 0, fmt.Errorf("unsupported WAV")
  306. }
  307. if dataStart+dataLen > len(data) {
  308. dataLen = len(data) - dataStart
  309. }
  310. step := int(channels) * 2
  311. nFrames := dataLen / step
  312. out := make([]float64, nFrames)
  313. for j := 0; j < nFrames; j++ {
  314. off := dataStart + j*step
  315. l := float64(int16(binary.LittleEndian.Uint16(data[off : off+2])))
  316. r := l
  317. if channels >= 2 {
  318. r = float64(int16(binary.LittleEndian.Uint16(data[off+2 : off+4])))
  319. }
  320. out[j] = (l + r) / 2.0 / 32768.0
  321. }
  322. return out, float64(sampleRate), nil
  323. }