Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

164 line
4.0KB

  1. package watermark
  2. import (
  3. "math"
  4. "testing"
  5. )
  6. func TestSTFTRoundTrip(t *testing.T) {
  7. const key = "test-stft-key"
  8. const duration = 150.0 // seconds — need > 136.5s for one full WM cycle
  9. nSamples := int(duration * WMRate)
  10. t.Logf("Generating %d samples @ %d Hz (%.1fs)", nSamples, WMRate, duration)
  11. t.Logf("WM cycle: %d STFT frames, %.1fs", FramesPerWM, float64(SamplesPerWM)/WMRate)
  12. // Generate test signal: broadband noise (the multiplicative watermark
  13. // needs energy in all frequency bins to work — a pure tone only has
  14. // energy in one bin and the watermark has no effect on silent bins)
  15. audio := make([]float64, nSamples)
  16. // Simple LCG pseudo-random for reproducibility
  17. var lcg uint64 = 12345
  18. for i := range audio {
  19. lcg = lcg*6364136223846793005 + 1442695040888963407
  20. audio[i] = 0.3 * (float64(int32(lcg>>33))/float64(1<<31))
  21. }
  22. rmsIn := rmsF64(audio)
  23. t.Logf("Input RMS: %.1f dBFS", 20*math.Log10(rmsIn+1e-12))
  24. // Embed watermark
  25. embedder := NewSTFTEmbedder(key)
  26. watermarked := embedder.ProcessBlock(audio)
  27. rmsOut := rmsF64(watermarked)
  28. t.Logf("Output RMS: %.1f dBFS", 20*math.Log10(rmsOut+1e-12))
  29. t.Logf("RMS change: %.2f dB", 20*math.Log10(rmsOut/rmsIn))
  30. // Detect watermark
  31. detector := NewSTFTDetector(key)
  32. corrs, offset := detector.Detect(watermarked)
  33. t.Logf("Detection offset: %d", offset)
  34. // Check correlations
  35. var nPositive, nNegative int
  36. var sumAbs float64
  37. for _, c := range corrs {
  38. sumAbs += math.Abs(c)
  39. if c > 0 {
  40. nPositive++
  41. } else {
  42. nNegative++
  43. }
  44. }
  45. avgAbs := sumAbs / float64(payloadBits)
  46. t.Logf("Correlations: avg|c|=%.1f, positive=%d, negative=%d", avgAbs, nPositive, nNegative)
  47. if avgAbs < 1.0 {
  48. t.Errorf("avg|c| too low: %.1f (expected >> 1.0)", avgAbs)
  49. }
  50. // Check against known payload
  51. payload := KeyToPayload(key)
  52. codeword := RSEncode(payload)
  53. var expectedBits [payloadBits]int
  54. for i := 0; i < payloadBits; i++ {
  55. expectedBits[i] = int((codeword[i/8] >> uint(7-(i%8))) & 1)
  56. }
  57. nerr := 0
  58. for i := 0; i < payloadBits; i++ {
  59. hard := 0
  60. if corrs[i] < 0 {
  61. hard = 1
  62. }
  63. if hard != expectedBits[i] {
  64. nerr++
  65. }
  66. }
  67. t.Logf("BER: %d/%d (%.1f%%)", nerr, payloadBits, 100*float64(nerr)/float64(payloadBits))
  68. if nerr > 20 {
  69. t.Errorf("BER too high: %d/%d", nerr, payloadBits)
  70. }
  71. // Try RS decode
  72. var recv [rsTotalBytes]byte
  73. for i := 0; i < payloadBits; i++ {
  74. if corrs[i] < 0 {
  75. recv[i/8] |= 1 << uint(7-(i%8))
  76. }
  77. }
  78. // Try with erasures if needed
  79. decoded := false
  80. for nErase := 0; nErase <= rsCheckBytes; nErase++ {
  81. if nErase == 0 {
  82. // Try zero erasures (valid if BER=0)
  83. p, ok := RSDecode(recv, nil)
  84. if ok {
  85. if KeyMatchesPayload(key, p) {
  86. t.Logf("Decoded with 0 erasures: MATCH ✓")
  87. decoded = true
  88. break
  89. }
  90. }
  91. continue
  92. }
  93. // Erase weakest bytes by |correlation|
  94. type bc struct{ idx int; conf float64 }
  95. byteConfs := make([]bc, rsTotalBytes)
  96. for b := 0; b < rsTotalBytes; b++ {
  97. minC := math.Abs(corrs[b*8])
  98. for bit := 1; bit < 8; bit++ {
  99. c := math.Abs(corrs[b*8+bit])
  100. if c < minC {
  101. minC = c
  102. }
  103. }
  104. byteConfs[b] = bc{b, minC}
  105. }
  106. // Sort by confidence (weakest first)
  107. for i := 0; i < len(byteConfs); i++ {
  108. for j := i + 1; j < len(byteConfs); j++ {
  109. if byteConfs[j].conf < byteConfs[i].conf {
  110. byteConfs[i], byteConfs[j] = byteConfs[j], byteConfs[i]
  111. }
  112. }
  113. }
  114. erasePos := make([]int, nErase)
  115. for i := 0; i < nErase; i++ {
  116. erasePos[i] = byteConfs[i].idx
  117. }
  118. // Sort positions
  119. for i := 0; i < len(erasePos); i++ {
  120. for j := i + 1; j < len(erasePos); j++ {
  121. if erasePos[j] < erasePos[i] {
  122. erasePos[i], erasePos[j] = erasePos[j], erasePos[i]
  123. }
  124. }
  125. }
  126. p, ok := RSDecode(recv, erasePos)
  127. if ok {
  128. if KeyMatchesPayload(key, p) {
  129. t.Logf("Decoded with %d erasures: MATCH ✓", nErase)
  130. decoded = true
  131. break
  132. }
  133. }
  134. }
  135. if !decoded {
  136. t.Errorf("RS decode FAILED")
  137. }
  138. }
  139. func rmsF64(s []float64) float64 {
  140. var acc float64
  141. for _, v := range s {
  142. acc += v * v
  143. }
  144. return math.Sqrt(acc / float64(len(s)))
  145. }