Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

189 行
5.3KB

  1. package watermark
  2. import (
  3. "math"
  4. "sort"
  5. "testing"
  6. )
  7. // TestRoundTrip verifies the full embed → downsample → phase-search → rotation → RS-decode chain.
  8. func TestRoundTrip(t *testing.T) {
  9. const key = "test-key-42"
  10. const recRate = float64(RecordingRate) // 48000
  11. const compRate = float64(CompositeRate) // 228000
  12. const duration = 60.0 // seconds — ~2.7 frames at ChipRate=12kHz
  13. nRecSamples := int(duration * recRate)
  14. // === Embed ===
  15. emb := NewEmbedder(key)
  16. // No gate — test pure watermark signal
  17. samples := make([]float64, 0, nRecSamples)
  18. // Drive embedder at CompositeRate, collect at RecordingRate via Bresenham
  19. accum := 0
  20. var last float64
  21. for len(samples) < nRecSamples {
  22. last = emb.NextSample()
  23. accum += RecordingRate
  24. if accum >= CompositeRate {
  25. accum -= CompositeRate
  26. samples = append(samples, last)
  27. }
  28. }
  29. t.Logf("Embedded: %d samples @ %.0f Hz = %.2fs", len(samples), recRate, float64(len(samples))/recRate)
  30. // RMS check
  31. var rmsAcc float64
  32. for _, s := range samples {
  33. rmsAcc += s * s
  34. }
  35. rms := math.Sqrt(rmsAcc / float64(len(samples)))
  36. rmsDBFS := 20 * math.Log10(rms+1e-12)
  37. t.Logf("Watermark RMS: %.1f dBFS (expect ~-48)", rmsDBFS)
  38. if rmsDBFS < -52 || rmsDBFS > -44 {
  39. t.Errorf("RMS %.1f dBFS out of expected range [-52, -44]", rmsDBFS)
  40. }
  41. // === Decode: Phase search ===
  42. samplesPerBit := int(float64(PnChips) * recRate / float64(ChipRate))
  43. t.Logf("samplesPerBit=%d, frameLen=%d", samplesPerBit, samplesPerBit*PayloadBits)
  44. const coarseStep = 8
  45. const syncBits = 64
  46. bestPhase := 0
  47. bestMag := 0.0
  48. for phase := 0; phase < samplesPerBit; phase += coarseStep {
  49. mag := testAvgCorrMag(samples, phase, samplesPerBit, syncBits, recRate)
  50. if mag > bestMag {
  51. bestMag = mag
  52. bestPhase = phase
  53. }
  54. }
  55. fineStart := bestPhase - coarseStep
  56. if fineStart < 0 { fineStart = 0 }
  57. fineEnd := bestPhase + coarseStep
  58. if fineEnd > samplesPerBit { fineEnd = samplesPerBit }
  59. for phase := fineStart; phase < fineEnd; phase++ {
  60. mag := testAvgCorrMag(samples, phase, samplesPerBit, syncBits, recRate)
  61. if mag > bestMag {
  62. bestMag = mag
  63. bestPhase = phase
  64. }
  65. }
  66. t.Logf("Phase search: bestPhase=%d, avgCorr=%.4f", bestPhase, bestMag)
  67. // Phase should be 0 for clean signal starting at sample 0
  68. if bestPhase != 0 {
  69. t.Errorf("expected bestPhase=0, got %d", bestPhase)
  70. }
  71. // === Decode: Extract correlations ===
  72. nCompleteBits := (len(samples) - bestPhase) / samplesPerBit
  73. nFrames := nCompleteBits / PayloadBits
  74. if nFrames == 0 { nFrames = 1 }
  75. t.Logf("Complete bits: %d, frames: %d", nCompleteBits, nFrames)
  76. corrs := make([]float64, PayloadBits)
  77. for i := 0; i < PayloadBits; i++ {
  78. for frame := 0; frame < nFrames; frame++ {
  79. bitGlobal := frame*PayloadBits + i
  80. start := bestPhase + bitGlobal*samplesPerBit
  81. if start+samplesPerBit > len(samples) { break }
  82. corrs[i] += CorrelateAt(samples, start, recRate)
  83. }
  84. }
  85. // Log correlation stats
  86. var minAbs, maxAbs float64
  87. for i, c := range corrs {
  88. ac := math.Abs(c)
  89. if i == 0 || ac < minAbs { minAbs = ac }
  90. if ac > maxAbs { maxAbs = ac }
  91. }
  92. t.Logf("Correlation range: min|c|=%.2f, max|c|=%.2f", minAbs, maxAbs)
  93. // === Decode: Frame sync via rotation ===
  94. type decodeResult struct {
  95. rotation int
  96. payload [RsDataBytes]byte
  97. erasures int
  98. }
  99. var best *decodeResult
  100. for rot := 0; rot < PayloadBits; rot++ {
  101. var recv [RsTotalBytes]byte
  102. confs := make([]float64, PayloadBits)
  103. for i := 0; i < PayloadBits; i++ {
  104. srcBit := (i + rot) % PayloadBits
  105. c := corrs[srcBit]
  106. confs[i] = math.Abs(c)
  107. if c < 0 {
  108. recv[i/8] |= 1 << uint(7-(i%8))
  109. }
  110. }
  111. type bitConf struct { idx int; conf float64 }
  112. ranked := make([]bitConf, PayloadBits)
  113. for i := range ranked { ranked[i] = bitConf{i, confs[i]} }
  114. sort.Slice(ranked, func(a, b int) bool { return ranked[a].conf < ranked[b].conf })
  115. for nErase := 0; nErase <= RsCheckBytes*8; nErase++ {
  116. erasedBytes := map[int]bool{}
  117. for _, bc := range ranked[:nErase] {
  118. erasedBytes[bc.idx/8] = true
  119. }
  120. if len(erasedBytes) > RsCheckBytes { break }
  121. erasePos := make([]int, 0, len(erasedBytes))
  122. for pos := range erasedBytes { erasePos = append(erasePos, pos) }
  123. sort.Ints(erasePos)
  124. payload, ok := RSDecode(recv, erasePos)
  125. if ok {
  126. if best == nil || len(erasePos) < best.erasures {
  127. best = &decodeResult{rotation: rot, payload: payload, erasures: len(erasePos)}
  128. }
  129. break
  130. }
  131. }
  132. if best != nil && best.erasures == 0 { break }
  133. }
  134. if best == nil {
  135. t.Fatal("RS decode FAILED — no valid rotation found")
  136. }
  137. t.Logf("Decoded: rotation=%d, erasures=%d, payload=%x", best.rotation, best.erasures, best.payload)
  138. // Rotation should be 0 for clean signal
  139. if best.rotation != 0 {
  140. t.Errorf("expected rotation=0, got %d", best.rotation)
  141. }
  142. if best.erasures != 0 {
  143. t.Errorf("expected 0 erasures, got %d", best.erasures)
  144. }
  145. // Key match
  146. if !KeyMatchesPayload(key, best.payload) {
  147. t.Errorf("key %q does NOT match decoded payload %x", key, best.payload)
  148. } else {
  149. t.Logf("Key %q MATCHES", key)
  150. }
  151. }
  152. func testAvgCorrMag(samples []float64, phase, samplesPerBit, nBits int, recRate float64) float64 {
  153. var total float64
  154. var count int
  155. for b := 0; b < nBits; b++ {
  156. start := phase + b*samplesPerBit
  157. if start+samplesPerBit > len(samples) { break }
  158. c := CorrelateAt(samples, start, recRate)
  159. total += math.Abs(c)
  160. count++
  161. }
  162. if count == 0 { return 0 }
  163. return total / float64(count)
  164. }