Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

418 行
11KB

  1. // cmd/wmdecode — fm-rds-tx spread-spectrum watermark recovery tool.
  2. //
  3. // Approach: downsample to chip rate (12 kHz), correlate at 1 sample/chip.
  4. // No fractional stepping, no clock drift issues. FFT-free phase search.
  5. package main
  6. import (
  7. "encoding/binary"
  8. "fmt"
  9. "math"
  10. "os"
  11. "sort"
  12. "github.com/jan/fm-rds-tx/internal/watermark"
  13. )
  14. func main() {
  15. if len(os.Args) < 2 {
  16. fmt.Fprintln(os.Stderr, "usage: wmdecode <file.wav> [key ...]")
  17. os.Exit(1)
  18. }
  19. samples, recRate, err := readMonoWAV(os.Args[1])
  20. if err != nil {
  21. fmt.Fprintf(os.Stderr, "read WAV: %v\n", err)
  22. os.Exit(1)
  23. }
  24. rms := rmsLevel(samples)
  25. fmt.Printf("WAV: %d samples @ %.0f Hz = %.2fs, RMS %.1f dBFS\n",
  26. len(samples), recRate, float64(len(samples))/recRate, 20*math.Log10(rms+1e-9))
  27. chipRate := float64(watermark.ChipRate) // 12000
  28. pnChips := watermark.PnChips // 2048
  29. // Step 1: LPF at ChipRate/2 then downsample to ChipRate.
  30. // At chip rate: 1 sample = 1 chip. No fractional stepping.
  31. decimFactor := int(recRate / chipRate) // 192000/12000 = 16
  32. if decimFactor < 1 {
  33. decimFactor = 1
  34. }
  35. actualChipRate := recRate / float64(decimFactor) // should be exactly chipRate
  36. fmt.Printf("Downsample: %d:1 (%.0f Hz → %.0f Hz)\n", decimFactor, recRate, actualChipRate)
  37. // Anti-alias LPF (8th-order IIR at 5.5 kHz)
  38. lpfCoeffs := designLPF8(5500, recRate)
  39. filtered := applyIIR(samples, lpfCoeffs)
  40. // Decimate
  41. nDown := len(filtered) / decimFactor
  42. down := make([]float64, nDown)
  43. for i := 0; i < nDown; i++ {
  44. down[i] = filtered[i*decimFactor]
  45. }
  46. rmsDown := rmsLevel(down)
  47. fmt.Printf("Downsampled: %d samples @ %.0f Hz, RMS %.1f dBFS\n",
  48. nDown, actualChipRate, 20*math.Log10(rmsDown+1e-9))
  49. // Step 2: Phase search — slide 1-bit PN template across [0, pnChips).
  50. // At chip rate this is a simple 2048-element dot product per offset.
  51. // Test all 2048 phases, accumulate energy over many bits.
  52. fmt.Printf("Phase search: %d candidates\n", pnChips)
  53. nSearchBits := nDown / pnChips
  54. if nSearchBits > 500 {
  55. nSearchBits = 500
  56. }
  57. bestPhase := 0
  58. bestEnergy := 0.0
  59. for phase := 0; phase < pnChips; phase++ {
  60. var energy float64
  61. for b := 0; b < nSearchBits; b++ {
  62. start := phase + b*pnChips
  63. if start+pnChips > nDown {
  64. break
  65. }
  66. c := corrChipRate(down, start, pnChips)
  67. energy += c * c
  68. }
  69. if energy > bestEnergy {
  70. bestEnergy = energy
  71. bestPhase = phase
  72. }
  73. }
  74. fmt.Printf("Phase: offset=%d (%.2fms), energy=%.0f\n",
  75. bestPhase, float64(bestPhase)/actualChipRate*1000, bestEnergy)
  76. // Step 3: Per-bit correlation with ±4 sample sliding (handles residual drift).
  77. // At chip rate, ±4 samples = ±0.33ms — covers ~±40 ppm over 22s frame.
  78. nCompleteBits := (nDown - bestPhase) / pnChips
  79. nFrames := nCompleteBits / watermark.PayloadBits
  80. if nFrames < 1 {
  81. nFrames = 1
  82. }
  83. fmt.Printf("Sync: %d complete bits, %d frames\n", nCompleteBits, nFrames)
  84. const slideWindow = 200 // ±200 chips — handles phase errors + drift
  85. corrs := make([]float64, watermark.PayloadBits)
  86. for i := 0; i < watermark.PayloadBits; i++ {
  87. // For each offset, sum correlation across ALL frames first.
  88. // Signal adds coherently (×nFrames), noise adds as √nFrames.
  89. // Then pick the offset with maximum |sum|.
  90. bestAbs := 0.0
  91. bestVal := 0.0
  92. for off := -slideWindow; off <= slideWindow; off++ {
  93. var sum float64
  94. for f := 0; f < nFrames; f++ {
  95. nominal := bestPhase + (f*watermark.PayloadBits+i)*pnChips + off
  96. if nominal < 0 || nominal+pnChips > nDown {
  97. continue
  98. }
  99. sum += corrChipRate(down, nominal, pnChips)
  100. }
  101. if math.Abs(sum) > bestAbs {
  102. bestAbs = math.Abs(sum)
  103. bestVal = sum
  104. }
  105. }
  106. corrs[i] = bestVal
  107. }
  108. // Diagnostics
  109. var corrMin, corrMax, sumAbs float64
  110. var nStrong, nDead int
  111. for i, c := range corrs {
  112. ac := math.Abs(c)
  113. sumAbs += ac
  114. if i == 0 || ac < corrMin {
  115. corrMin = ac
  116. }
  117. if ac > corrMax {
  118. corrMax = ac
  119. }
  120. if ac > sumAbs/float64(i+1)*2 {
  121. nStrong++
  122. }
  123. if ac < 3 {
  124. nDead++
  125. }
  126. }
  127. avgCorr := sumAbs / 128
  128. nStrong = 0
  129. for _, c := range corrs {
  130. if math.Abs(c) > avgCorr*0.5 {
  131. nStrong++
  132. }
  133. }
  134. fmt.Printf("Corrs: min|c|=%.1f, max|c|=%.1f, avg|c|=%.1f (strong=%d, dead=%d)\n",
  135. corrMin, corrMax, avgCorr, nStrong, nDead)
  136. // Step 4: Frame sync — 128 rotations × byte-level erasure + bit-flipping.
  137. // Verbose: compute BER at each rotation against the known key (if supplied)
  138. knownPayload := [watermark.RsDataBytes]byte{}
  139. hasKnown := false
  140. if len(os.Args) >= 3 {
  141. hasKnown = true
  142. knownPayload = watermark.KeyToPayload(os.Args[2])
  143. knownCW := watermark.RSEncode(knownPayload)
  144. var knownBits [watermark.PayloadBits]int
  145. for i := 0; i < watermark.PayloadBits; i++ {
  146. knownBits[i] = int((knownCW[i/8] >> uint(7-(i%8))) & 1)
  147. }
  148. fmt.Println("\nRotation sweep (top 10 by BER):")
  149. type rotBER struct{ rot, ber int }
  150. var results []rotBER
  151. for rot := 0; rot < watermark.PayloadBits; rot++ {
  152. nerr := 0
  153. for i := 0; i < watermark.PayloadBits; i++ {
  154. srcBit := (i + rot) % watermark.PayloadBits
  155. hard := 0
  156. if corrs[srcBit] < 0 {
  157. hard = 1
  158. }
  159. if hard != knownBits[i] {
  160. nerr++
  161. }
  162. }
  163. results = append(results, rotBER{rot, nerr})
  164. }
  165. sort.Slice(results, func(a, b int) bool { return results[a].ber < results[b].ber })
  166. for j := 0; j < 10 && j < len(results); j++ {
  167. r := results[j]
  168. fmt.Printf(" rot=%3d: BER=%d/128 (%4.1f%%)\n", r.rot, r.ber, 100*float64(r.ber)/128)
  169. }
  170. // Show byte error pattern at best rotation
  171. bestRot := results[0].rot
  172. fmt.Printf("\nByte errors at rot=%d:\n ", bestRot)
  173. for b := 0; b < watermark.RsTotalBytes; b++ {
  174. nerr := 0
  175. for bit := 0; bit < 8; bit++ {
  176. srcBit := (b*8 + bit + bestRot) % watermark.PayloadBits
  177. hard := 0
  178. if corrs[srcBit] < 0 {
  179. hard = 1
  180. }
  181. if hard != knownBits[b*8+bit] {
  182. nerr++
  183. }
  184. }
  185. fmt.Printf("B%d:%d ", b, nerr)
  186. }
  187. fmt.Println()
  188. // Show received vs expected codeword at best rotation
  189. var recv [watermark.RsTotalBytes]byte
  190. for i := 0; i < watermark.PayloadBits; i++ {
  191. srcBit := (i + bestRot) % watermark.PayloadBits
  192. if corrs[srcBit] < 0 {
  193. recv[i/8] |= 1 << uint(7-(i%8))
  194. }
  195. }
  196. fmt.Printf(" recv: %x\n", recv)
  197. fmt.Printf(" want: %x\n", knownCW)
  198. }
  199. _ = hasKnown
  200. _ = knownPayload
  201. type decodeResult struct {
  202. rotation int
  203. payload [watermark.RsDataBytes]byte
  204. flips int
  205. }
  206. var best *decodeResult
  207. for rot := 0; rot < watermark.PayloadBits; rot++ {
  208. var recv [watermark.RsTotalBytes]byte
  209. confs := make([]float64, watermark.PayloadBits)
  210. for i := 0; i < watermark.PayloadBits; i++ {
  211. srcBit := (i + rot) % watermark.PayloadBits
  212. c := corrs[srcBit]
  213. confs[i] = math.Abs(c)
  214. if c < 0 {
  215. recv[i/8] |= 1 << uint(7-(i%8))
  216. }
  217. }
  218. // Brute-force RS decode: try ALL possible erasure subsets of size 1..8.
  219. // With sliding correlation, confidence values are unreliable for erasure
  220. // selection (all bits look "strong"). Instead, let RS tell us which
  221. // subsets produce a valid codeword. This is fast: sum(C(16,k), k=1..8)
  222. // = ~39k RS decodes per rotation, ~5M total. Each takes <1µs.
  223. decoded := false
  224. for nErase := 1; nErase <= watermark.RsCheckBytes; nErase++ {
  225. if decoded { break }
  226. indices := make([]int, nErase)
  227. for i := range indices { indices[i] = i }
  228. for {
  229. erasePos := make([]int, nErase)
  230. copy(erasePos, indices)
  231. payload, ok := watermark.RSDecode(recv, erasePos)
  232. if ok {
  233. if best == nil {
  234. best = &decodeResult{rot, payload, nErase}
  235. }
  236. decoded = true
  237. break
  238. }
  239. // Next combination
  240. i := nErase - 1
  241. for i >= 0 && indices[i] == watermark.RsTotalBytes-nErase+i {
  242. i--
  243. }
  244. if i < 0 { break }
  245. indices[i]++
  246. for j := i + 1; j < nErase; j++ {
  247. indices[j] = indices[j-1] + 1
  248. }
  249. }
  250. }
  251. if decoded && best != nil && best.flips <= 4 {
  252. break // clean decode with few erasures — stop early
  253. }
  254. }
  255. if best == nil {
  256. fmt.Println("RS decode: FAILED — no valid frame alignment found.")
  257. fmt.Println("Watermark may not be present, or recording is too noisy/short.")
  258. os.Exit(1)
  259. }
  260. fmt.Printf("\nFrame sync: rotation=%d, %d byte erasures\n", best.rotation, best.flips)
  261. fmt.Printf("Payload: %x\n\n", best.payload)
  262. keys := os.Args[2:]
  263. if len(keys) == 0 {
  264. fmt.Println("No keys supplied — payload shown above.")
  265. return
  266. }
  267. fmt.Println("Key check:")
  268. matched := false
  269. for _, key := range keys {
  270. if watermark.KeyMatchesPayload(key, best.payload) {
  271. fmt.Printf(" ✓ MATCH: %q\n", key)
  272. matched = true
  273. } else {
  274. fmt.Printf(" ✗ : %q\n", key)
  275. }
  276. }
  277. if !matched {
  278. fmt.Println("\nNo key matched.")
  279. }
  280. }
  281. // corrChipRate correlates at chip rate (1 sample = 1 chip).
  282. func corrChipRate(down []float64, start, pnChips int) float64 {
  283. var acc float64
  284. for i := 0; i < pnChips; i++ {
  285. acc += down[start+i] * float64(watermark.PNSequence[i])
  286. }
  287. return acc
  288. }
  289. // --- 8th-order Butterworth LPF (4 cascaded biquads) ---
  290. type biquad struct{ b0, b1, b2, a1, a2 float64 }
  291. type iirCoeffs []biquad
  292. func designLPF8(cutoffHz, sampleRate float64) iirCoeffs {
  293. // 8th-order Butterworth = 4 biquad sections
  294. angles := []float64{math.Pi / 16, 3 * math.Pi / 16, 5 * math.Pi / 16, 7 * math.Pi / 16}
  295. coeffs := make(iirCoeffs, 4)
  296. for i, angle := range angles {
  297. q := 1.0 / (2 * math.Cos(angle))
  298. omega := 2 * math.Pi * cutoffHz / sampleRate
  299. cosW := math.Cos(omega)
  300. sinW := math.Sin(omega)
  301. alpha := sinW / (2 * q)
  302. a0 := 1 + alpha
  303. coeffs[i] = biquad{
  304. b0: (1 - cosW) / 2 / a0,
  305. b1: (1 - cosW) / a0,
  306. b2: (1 - cosW) / 2 / a0,
  307. a1: (-2 * cosW) / a0,
  308. a2: (1 - alpha) / a0,
  309. }
  310. }
  311. return coeffs
  312. }
  313. func applyIIR(samples []float64, coeffs iirCoeffs) []float64 {
  314. out := make([]float64, len(samples))
  315. copy(out, samples)
  316. for _, bq := range coeffs {
  317. var z1, z2 float64
  318. for i, x := range out {
  319. y := bq.b0*x + z1
  320. z1 = bq.b1*x - bq.a1*y + z2
  321. z2 = bq.b2*x - bq.a2*y
  322. out[i] = y
  323. }
  324. }
  325. return out
  326. }
  327. func rmsLevel(s []float64) float64 {
  328. var acc float64
  329. for _, v := range s {
  330. acc += v * v
  331. }
  332. return math.Sqrt(acc / float64(len(s)))
  333. }
  334. func readMonoWAV(path string) ([]float64, float64, error) {
  335. data, err := os.ReadFile(path)
  336. if err != nil {
  337. return nil, 0, err
  338. }
  339. if len(data) < 44 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
  340. return nil, 0, fmt.Errorf("not a RIFF/WAVE file")
  341. }
  342. var channels, bitsPerSample uint16
  343. var sampleRate uint32
  344. var dataStart, dataLen int
  345. i := 12
  346. for i+8 <= len(data) {
  347. id := string(data[i : i+4])
  348. sz := int(binary.LittleEndian.Uint32(data[i+4 : i+8]))
  349. i += 8
  350. switch id {
  351. case "fmt ":
  352. if sz >= 16 {
  353. channels = binary.LittleEndian.Uint16(data[i+2 : i+4])
  354. sampleRate = binary.LittleEndian.Uint32(data[i+4 : i+8])
  355. bitsPerSample = binary.LittleEndian.Uint16(data[i+14 : i+16])
  356. }
  357. case "data":
  358. dataStart, dataLen = i, sz
  359. }
  360. i += sz
  361. if sz%2 != 0 {
  362. i++
  363. }
  364. if dataStart > 0 && channels > 0 {
  365. break
  366. }
  367. }
  368. if dataStart == 0 || bitsPerSample != 16 || channels == 0 {
  369. return nil, 0, fmt.Errorf("unsupported WAV (need 16-bit PCM, got bits=%d ch=%d)", bitsPerSample, channels)
  370. }
  371. if dataStart+dataLen > len(data) {
  372. dataLen = len(data) - dataStart
  373. }
  374. step := int(channels) * 2
  375. nFrames := dataLen / step
  376. out := make([]float64, nFrames)
  377. for j := 0; j < nFrames; j++ {
  378. off := dataStart + j*step
  379. l := float64(int16(binary.LittleEndian.Uint16(data[off : off+2])))
  380. r := l
  381. if channels >= 2 {
  382. r = float64(int16(binary.LittleEndian.Uint16(data[off+2 : off+4])))
  383. }
  384. out[j] = (l + r) / 2.0 / 32768.0
  385. }
  386. return out, float64(sampleRate), nil
  387. }