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

105 行
2.8KB

  1. package offline
  2. import (
  3. "math"
  4. "testing"
  5. "time"
  6. "github.com/jan/fm-rds-tx/internal/dsp"
  7. "github.com/jan/fm-rds-tx/internal/watermark"
  8. )
  9. func TestWatermarkE2EFloat32(t *testing.T) {
  10. if testing.Short() {
  11. t.Skip("skipping long watermark E2E float32 test in -short mode")
  12. }
  13. const key = "test-key-e2e-f32"
  14. const duration = 45 * time.Second
  15. gen := newWatermarkE2EGenerator(t, key)
  16. frame := gen.GenerateFrame(duration)
  17. nSamples := len(frame.Samples)
  18. compositeRate := frame.SampleRateHz
  19. t.Logf("Generated %d samples @ %.0f Hz", nSamples, compositeRate)
  20. t.Run("float32_storage", func(t *testing.T) {
  21. composite := extractCompositeFrame(frame)
  22. payload, _, ok := decodeWatermarkFromComposite(t, composite, compositeRate)
  23. if !ok {
  24. t.Fatal("decode failed after float32 composite storage")
  25. }
  26. if !watermark.KeyMatchesPayload(key, payload) {
  27. t.Fatalf("payload mismatch after float32 storage: %x", payload)
  28. }
  29. })
  30. t.Run("fm_mod_demod", func(t *testing.T) {
  31. maxDev := 75000.0
  32. phases := make([]float64, nSamples)
  33. phaseInc := 2 * math.Pi * maxDev / compositeRate
  34. phase := 0.0
  35. for i, s := range frame.Samples {
  36. phase += float64(s.I) * phaseInc
  37. phases[i] = phase
  38. }
  39. demod := make([]float64, nSamples)
  40. for i := 1; i < nSamples; i++ {
  41. dp := phases[i] - phases[i-1]
  42. demod[i] = dp / phaseInc
  43. }
  44. demod[0] = demod[1]
  45. payload, _, ok := decodeWatermarkFromComposite(t, demod, compositeRate)
  46. if !ok {
  47. t.Fatal("decode failed after FM mod/demod")
  48. }
  49. if !watermark.KeyMatchesPayload(key, payload) {
  50. t.Fatalf("payload mismatch after FM mod/demod: %x", payload)
  51. }
  52. })
  53. t.Run("fm_upsample_downsample", func(t *testing.T) {
  54. maxDev := 75000.0
  55. deviceRate := 2280000.0
  56. upsampler := dsp.NewFMUpsampler(compositeRate, deviceRate, maxDev)
  57. upFrame := upsampler.Process(frame)
  58. t.Logf("Upsampled: %d IQ samples @ %.0f Hz", len(upFrame.Samples), upFrame.SampleRateHz)
  59. nUp := len(upFrame.Samples)
  60. demod := make([]float64, nUp)
  61. prevPhase := 0.0
  62. for i, s := range upFrame.Samples {
  63. p := math.Atan2(float64(s.Q), float64(s.I))
  64. dp := p - prevPhase
  65. for dp > math.Pi {
  66. dp -= 2 * math.Pi
  67. }
  68. for dp < -math.Pi {
  69. dp += 2 * math.Pi
  70. }
  71. demod[i] = dp * deviceRate / (2 * math.Pi * maxDev)
  72. prevPhase = p
  73. }
  74. ratio := int(deviceRate / compositeRate)
  75. nDown := nUp / ratio
  76. downsampled := make([]float64, nDown)
  77. for i := 0; i < nDown; i++ {
  78. sum := 0.0
  79. for j := 0; j < ratio; j++ {
  80. idx := i*ratio + j
  81. if idx < nUp {
  82. sum += demod[idx]
  83. }
  84. }
  85. downsampled[i] = sum / float64(ratio)
  86. }
  87. payload, _, ok := decodeWatermarkFromComposite(t, downsampled, compositeRate)
  88. if !ok {
  89. t.Fatal("decode failed after FM upsample/downsample path")
  90. }
  91. if !watermark.KeyMatchesPayload(key, payload) {
  92. t.Fatalf("payload mismatch after FM upsample/downsample path: %x", payload)
  93. }
  94. })
  95. }