Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

139 lines
4.0KB

  1. // cmd/wmdump — generates a composite WAV with watermark for offline verification.
  2. //
  3. // Usage:
  4. //
  5. // wmdump --key FMRTX-XXX --config config.json --output composite.wav --duration 60s
  6. // wmdecode composite.wav FMRTX-XXX
  7. //
  8. // If wmdecode succeeds on the composite.wav, the watermark code is working.
  9. // If it fails on an air recording, the issue is in the PlutoSDR/air/receiver path.
  10. package main
  11. import (
  12. "encoding/binary"
  13. "flag"
  14. "fmt"
  15. "log"
  16. "math"
  17. "os"
  18. "time"
  19. cfgpkg "github.com/jan/fm-rds-tx/internal/config"
  20. "github.com/jan/fm-rds-tx/internal/license"
  21. offpkg "github.com/jan/fm-rds-tx/internal/offline"
  22. "github.com/jan/fm-rds-tx/internal/watermark"
  23. )
  24. func main() {
  25. configPath := flag.String("config", "", "path to JSON config (uses same as fmrtx)")
  26. key := flag.String("key", "free", "license key to embed")
  27. output := flag.String("output", "wmdump.wav", "output WAV path")
  28. duration := flag.Duration("duration", 60*time.Second, "generation duration")
  29. rate := flag.Int("rate", 192000, "output WAV sample rate (resampled from composite)")
  30. flag.Parse()
  31. cfg, err := cfgpkg.Load(*configPath)
  32. if err != nil {
  33. log.Fatalf("load config: %v", err)
  34. }
  35. // Match real TX: split-rate mode means FMModulationEnabled=false
  36. cfg.FM.FMModulationEnabled = false
  37. gen := offpkg.NewGenerator(cfg)
  38. licState := license.NewState(*key)
  39. gen.SetLicense(licState, *key)
  40. fmt.Printf("Generating composite with watermark...\n")
  41. fmt.Printf(" Key: %s (licensed=%v)\n", *key, licState.Licensed())
  42. fmt.Printf(" Config: %s\n", *configPath)
  43. fmt.Printf(" Duration: %s\n", *duration)
  44. fmt.Printf(" Composite: %d Hz\n", cfg.FM.CompositeRateHz)
  45. fmt.Printf(" Output: %s @ %d Hz\n", *output, *rate)
  46. fmt.Printf(" ChipRate: %d Hz (PN bandwidth 0-%d Hz)\n", watermark.ChipRate, watermark.ChipRate/2)
  47. fmt.Println()
  48. frame := gen.GenerateFrame(*duration)
  49. if frame == nil {
  50. log.Fatal("GenerateFrame returned nil")
  51. }
  52. fmt.Printf("Generated %d composite samples @ %.0f Hz\n", len(frame.Samples), frame.SampleRateHz)
  53. // Extract composite (I channel in non-FM mode)
  54. compRate := frame.SampleRateHz
  55. nComp := len(frame.Samples)
  56. // RMS check
  57. var rmsAcc float64
  58. for _, s := range frame.Samples {
  59. rmsAcc += float64(s.I) * float64(s.I)
  60. }
  61. compRMS := math.Sqrt(rmsAcc / float64(nComp))
  62. fmt.Printf("Composite RMS: %.1f dBFS\n", 20*math.Log10(compRMS+1e-12))
  63. // Resample composite to output rate (linear interpolation)
  64. outRate := float64(*rate)
  65. ratio := outRate / compRate
  66. nOut := int(float64(nComp) * ratio)
  67. samples := make([]float64, nOut)
  68. for i := range samples {
  69. pos := float64(i) / ratio
  70. idx := int(pos)
  71. frac := pos - float64(idx)
  72. if idx+1 < nComp {
  73. samples[i] = float64(frame.Samples[idx].I)*(1-frac) + float64(frame.Samples[idx+1].I)*frac
  74. } else if idx < nComp {
  75. samples[i] = float64(frame.Samples[idx].I)
  76. }
  77. }
  78. // RMS after resample
  79. var rms2 float64
  80. for _, s := range samples {
  81. rms2 += s * s
  82. }
  83. outRMS := math.Sqrt(rms2 / float64(nOut))
  84. fmt.Printf("Output RMS: %.1f dBFS (%d samples @ %.0f Hz)\n", 20*math.Log10(outRMS+1e-12), nOut, outRate)
  85. // Write WAV
  86. if err := writeWAV(*output, samples, *rate); err != nil {
  87. log.Fatalf("write WAV: %v", err)
  88. }
  89. fmt.Printf("\nWritten: %s\n", *output)
  90. fmt.Printf("\nDecode with:\n")
  91. fmt.Printf(" .\\wmdecode.exe %s %q\n", *output, *key)
  92. }
  93. func writeWAV(path string, samples []float64, rate int) error {
  94. f, err := os.Create(path)
  95. if err != nil {
  96. return err
  97. }
  98. defer f.Close()
  99. le := binary.LittleEndian
  100. dataSz := uint32(len(samples) * 2)
  101. f.Write([]byte("RIFF"))
  102. binary.Write(f, le, 36+dataSz)
  103. f.Write([]byte("WAVE"))
  104. f.Write([]byte("fmt "))
  105. binary.Write(f, le, uint32(16))
  106. binary.Write(f, le, uint16(1))
  107. binary.Write(f, le, uint16(1))
  108. binary.Write(f, le, uint32(rate))
  109. binary.Write(f, le, uint32(rate*2))
  110. binary.Write(f, le, uint16(2))
  111. binary.Write(f, le, uint16(16))
  112. f.Write([]byte("data"))
  113. binary.Write(f, le, dataSz)
  114. for _, s := range samples {
  115. v := s * 32767.0
  116. if v > 32767 {
  117. v = 32767
  118. }
  119. if v < -32768 {
  120. v = -32768
  121. }
  122. binary.Write(f, le, int16(v))
  123. }
  124. return nil
  125. }