Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

130 linhas
4.8KB

  1. package offline
  2. import (
  3. "math"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "time"
  9. cfgpkg "github.com/jan/fm-rds-tx/internal/config"
  10. )
  11. func TestGenerateFrame(t *testing.T) {
  12. g := NewGenerator(cfgpkg.Default())
  13. frame := g.GenerateFrame(50 * time.Millisecond)
  14. if frame == nil || len(frame.Samples) == 0 { t.Fatal("expected samples") }
  15. }
  16. func TestGenerateFrameFMIQ(t *testing.T) {
  17. cfg := cfgpkg.Default(); cfg.FM.FMModulationEnabled = true
  18. frame := NewGenerator(cfg).GenerateFrame(10 * time.Millisecond)
  19. for i := 100; i < len(frame.Samples) && i < 200; i++ {
  20. s := frame.Samples[i]
  21. mag := math.Sqrt(float64(s.I)*float64(s.I) + float64(s.Q)*float64(s.Q))
  22. if math.Abs(mag-1.0) > 0.01 { t.Fatalf("sample %d: mag=%.4f", i, mag) }
  23. }
  24. }
  25. func TestGenerateFrameCompositeOnly(t *testing.T) {
  26. cfg := cfgpkg.Default(); cfg.FM.FMModulationEnabled = false
  27. frame := NewGenerator(cfg).GenerateFrame(10 * time.Millisecond)
  28. for i := 0; i < len(frame.Samples) && i < 100; i++ {
  29. if frame.Samples[i].Q != 0 { t.Fatalf("sample %d: Q=%.6f", i, frame.Samples[i].Q) }
  30. }
  31. }
  32. func TestStereoDisabled(t *testing.T) {
  33. cfgS := cfgpkg.Default(); cfgS.FM.FMModulationEnabled = false; cfgS.FM.StereoEnabled = true
  34. cfgM := cfgS; cfgM.FM.StereoEnabled = false
  35. sf := NewGenerator(cfgS).GenerateFrame(20 * time.Millisecond)
  36. mf := NewGenerator(cfgM).GenerateFrame(20 * time.Millisecond)
  37. var diffEnergy float64
  38. for i := range sf.Samples {
  39. d := float64(sf.Samples[i].I - mf.Samples[i].I); diffEnergy += d * d
  40. }
  41. if diffEnergy == 0 { t.Fatal("expected difference") }
  42. }
  43. func TestWriteFile(t *testing.T) {
  44. cfg := cfgpkg.Default()
  45. out := filepath.Join(t.TempDir(), "test.iqf32")
  46. if err := NewGenerator(cfg).WriteFile(out, 20*time.Millisecond); err != nil { t.Fatal(err) }
  47. info, _ := os.Stat(out)
  48. if info.Size() == 0 { t.Fatal("empty file") }
  49. }
  50. func TestSummaryTones(t *testing.T) {
  51. cfg := cfgpkg.Default(); cfg.Audio.InputPath = ""
  52. s := NewGenerator(cfg).Summary(10 * time.Millisecond)
  53. if !strings.Contains(s, "source=tones") { t.Fatalf("unexpected: %s", s) }
  54. }
  55. func TestSummaryToneFallback(t *testing.T) {
  56. cfg := cfgpkg.Default(); cfg.Audio.InputPath = "missing.wav"
  57. s := NewGenerator(cfg).Summary(10 * time.Millisecond)
  58. if !strings.Contains(s, "source=tone-fallback") { t.Fatalf("unexpected: %s", s) }
  59. }
  60. func TestSummaryPreemph(t *testing.T) {
  61. cfg := cfgpkg.Default(); cfg.FM.PreEmphasisTauUS = 50
  62. if !strings.Contains(NewGenerator(cfg).Summary(10*time.Millisecond), "preemph=50µs") { t.Fatal("missing preemph") }
  63. }
  64. func TestSummaryFMIQ(t *testing.T) {
  65. cfg := cfgpkg.Default(); cfg.FM.FMModulationEnabled = true
  66. if !strings.Contains(NewGenerator(cfg).Summary(10*time.Millisecond), "FM-IQ") { t.Fatal("missing FM-IQ") }
  67. }
  68. func TestLimiterPreventsClipping(t *testing.T) {
  69. cfg := cfgpkg.Default()
  70. cfg.FM.LimiterEnabled = true; cfg.FM.LimiterCeiling = 1.0
  71. cfg.FM.FMModulationEnabled = false
  72. cfg.Audio.ToneAmplitude = 0.9; cfg.Audio.Gain = 2.0; cfg.FM.OutputDrive = 1.0
  73. frame := NewGenerator(cfg).GenerateFrame(50 * time.Millisecond)
  74. for i, s := range frame.Samples {
  75. if math.Abs(float64(s.I)) > 1.01 { t.Fatalf("sample %d: %.4f exceeds ceiling", i, s.I) }
  76. }
  77. }
  78. // --- Operator truth tests ---
  79. func TestRDSDisabledSuppressesRDSEnergy(t *testing.T) {
  80. cfgOn := cfgpkg.Default(); cfgOn.FM.FMModulationEnabled = false; cfgOn.RDS.Enabled = true
  81. cfgOff := cfgOn; cfgOff.RDS.Enabled = false
  82. fOn := NewGenerator(cfgOn).GenerateFrame(20 * time.Millisecond)
  83. fOff := NewGenerator(cfgOff).GenerateFrame(20 * time.Millisecond)
  84. var diff float64
  85. for i := range fOn.Samples {
  86. d := float64(fOn.Samples[i].I - fOff.Samples[i].I); diff += d * d
  87. }
  88. if diff == 0 { t.Fatal("rds.enabled=false should produce different output") }
  89. }
  90. func TestFMModDisabledMeansComposite(t *testing.T) {
  91. cfg := cfgpkg.Default(); cfg.FM.FMModulationEnabled = false
  92. frame := NewGenerator(cfg).GenerateFrame(10 * time.Millisecond)
  93. for i := 0; i < 100; i++ {
  94. if frame.Samples[i].Q != 0 { t.Fatal("Q should be 0 when FM mod is off") }
  95. }
  96. }
  97. func TestLimiterDisabledAllowsHigherPeaks(t *testing.T) {
  98. base := cfgpkg.Default()
  99. base.FM.FMModulationEnabled = false
  100. base.Audio.ToneAmplitude = 0.9; base.Audio.Gain = 2.0; base.FM.OutputDrive = 1.0
  101. cfgLim := base; cfgLim.FM.LimiterEnabled = true; cfgLim.FM.LimiterCeiling = 1.0
  102. cfgNoLim := base; cfgNoLim.FM.LimiterEnabled = false
  103. fLim := NewGenerator(cfgLim).GenerateFrame(50 * time.Millisecond)
  104. fNoLim := NewGenerator(cfgNoLim).GenerateFrame(50 * time.Millisecond)
  105. var maxLim, maxNoLim float64
  106. for _, s := range fLim.Samples { if math.Abs(float64(s.I)) > maxLim { maxLim = math.Abs(float64(s.I)) } }
  107. for _, s := range fNoLim.Samples { if math.Abs(float64(s.I)) > maxNoLim { maxNoLim = math.Abs(float64(s.I)) } }
  108. if maxNoLim <= maxLim { t.Fatalf("limiter disabled should allow higher peaks: lim=%.4f nolim=%.4f", maxLim, maxNoLim) }
  109. }