Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

107 řádky
3.0KB

  1. // cmd/wmtest — Ferrite watermark self-test tool.
  2. //
  3. // Generates a mono WAV file containing only the spread-spectrum watermark
  4. // signal (silence + watermark, scaled up for visibility). Run wmdecode on
  5. // the output to verify embedder and decoder work without FM transmission.
  6. //
  7. // Usage:
  8. //
  9. // wmtest --key <license-key> --output test.wav --duration 30s
  10. // wmdecode test.wav <license-key>
  11. package main
  12. import (
  13. "encoding/binary"
  14. "flag"
  15. "fmt"
  16. "math"
  17. "os"
  18. "time"
  19. "github.com/jan/fm-rds-tx/internal/watermark"
  20. )
  21. func main() {
  22. key := flag.String("key", "free", "License key to embed")
  23. output := flag.String("output", "wmtest.wav", "Output WAV file")
  24. duration := flag.Duration("duration", 30*time.Second, "Duration")
  25. flag.Parse()
  26. const compRate = watermark.CompositeRate // 228000
  27. const recRate = watermark.RecordingRate // 48000
  28. nSamples := int(duration.Seconds() * float64(recRate))
  29. fmt.Printf("Ferrite watermark self-test\n")
  30. fmt.Printf(" Key: %s\n", *key)
  31. fmt.Printf(" Duration: %s (%d samples @ %dHz)\n\n", *duration, nSamples, recRate)
  32. embedder := watermark.NewEmbedder(*key)
  33. samples := make([]float64, 0, nSamples)
  34. // Drive embedder at composite rate, collect samples at recording rate.
  35. // Bresenham: accumulate recRate each composite step; when >= compRate,
  36. // emit one recording sample and subtract compRate.
  37. accum := 0
  38. var last float64
  39. for len(samples) < nSamples {
  40. last = embedder.NextSample()
  41. accum += recRate
  42. if accum >= compRate {
  43. accum -= compRate
  44. samples = append(samples, last)
  45. }
  46. }
  47. // RMS
  48. var rmsAcc float64
  49. for _, s := range samples {
  50. rmsAcc += s * s
  51. }
  52. rms := math.Sqrt(rmsAcc / float64(len(samples)))
  53. fmt.Printf("Watermark RMS: %.1f dBFS (nominal -48 dBFS)\n", 20*math.Log10(rms+1e-12))
  54. if err := writeMonoWAV(*output, samples, recRate); err != nil {
  55. fmt.Fprintf(os.Stderr, "write WAV: %v\n", err)
  56. os.Exit(1)
  57. }
  58. fmt.Printf("Written: %s\n\n", *output)
  59. fmt.Printf("Decode with:\n")
  60. fmt.Printf(" .\\wmdecode.exe %s %q\n\n", *output, *key)
  61. fmt.Printf("Expected: RS decode clean + MATCH\n")
  62. }
  63. func writeMonoWAV(path string, samples []float64, rate int) error {
  64. f, err := os.Create(path)
  65. if err != nil {
  66. return err
  67. }
  68. defer f.Close()
  69. le := binary.LittleEndian
  70. dataSz := uint32(len(samples) * 2)
  71. f.Write([]byte("RIFF"))
  72. binary.Write(f, le, 36+dataSz)
  73. f.Write([]byte("WAVE"))
  74. f.Write([]byte("fmt "))
  75. binary.Write(f, le, uint32(16))
  76. binary.Write(f, le, uint16(1)) // PCM
  77. binary.Write(f, le, uint16(1)) // mono
  78. binary.Write(f, le, uint32(rate))
  79. binary.Write(f, le, uint32(rate*2)) // byte rate
  80. binary.Write(f, le, uint16(2)) // block align
  81. binary.Write(f, le, uint16(16)) // bits/sample
  82. f.Write([]byte("data"))
  83. binary.Write(f, le, dataSz)
  84. for _, s := range samples {
  85. // Scale to int16 range — watermark at -48dBFS → ~0.004 amplitude
  86. // Multiply by 32767 to get full 16-bit range
  87. v := s * 32767.0
  88. if v > 32767 { v = 32767 }
  89. if v < -32768 { v = -32768 }
  90. binary.Write(f, le, int16(v))
  91. }
  92. return nil
  93. }