Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

97 lignes
2.8KB

  1. package offline
  2. import (
  3. "context"
  4. "encoding/binary"
  5. "fmt"
  6. "math"
  7. "path/filepath"
  8. "time"
  9. cfgpkg "github.com/jan/fm-rds-tx/internal/config"
  10. "github.com/jan/fm-rds-tx/internal/output"
  11. )
  12. type Generator struct {
  13. cfg cfgpkg.Config
  14. }
  15. func NewGenerator(cfg cfgpkg.Config) *Generator {
  16. return &Generator{cfg: cfg}
  17. }
  18. func (g *Generator) GenerateFrame(duration time.Duration) *output.CompositeFrame {
  19. sampleRate := float64(g.cfg.FM.CompositeRateHz)
  20. if sampleRate <= 0 {
  21. sampleRate = 228000
  22. }
  23. samples := int(duration.Seconds() * sampleRate)
  24. if samples <= 0 {
  25. samples = int(sampleRate / 10)
  26. }
  27. frame := &output.CompositeFrame{
  28. Samples: make([]output.IQSample, samples),
  29. SampleRateHz: sampleRate,
  30. Timestamp: time.Now().UTC(),
  31. Sequence: 1,
  32. }
  33. leftFreq := 1000.0
  34. rightFreq := 1600.0
  35. pilotFreq := 19000.0
  36. rdsFreq := 57000.0
  37. for i := 0; i < samples; i++ {
  38. t := float64(i) / sampleRate
  39. left := 0.4 * math.Sin(2*math.Pi*leftFreq*t)
  40. right := 0.4 * math.Sin(2*math.Pi*rightFreq*t+math.Pi/3)
  41. mono := (left + right) / 2
  42. stereo := (left - right) / 2 * 0.8 * math.Sin(2*math.Pi*38000*t)
  43. pilot := g.cfg.FM.PilotLevel * math.Sin(2*math.Pi*pilotFreq*t)
  44. rds := g.cfg.FM.RDSInjection * math.Sin(2*math.Pi*rdsFreq*t)
  45. composite := (mono + stereo + pilot + rds) * g.cfg.FM.OutputDrive
  46. frame.Samples[i] = output.IQSample{I: float32(composite), Q: 0}
  47. }
  48. return frame
  49. }
  50. func (g *Generator) WriteFile(path string, duration time.Duration) error {
  51. if path == "" {
  52. path = g.cfg.Backend.OutputPath
  53. }
  54. if path == "" {
  55. path = filepath.Join("build", "offline", "composite.iqf32")
  56. }
  57. backend, err := output.NewFileBackend(path, binary.LittleEndian, output.BackendInfo{
  58. Name: "offline-file",
  59. Description: "offline composite file backend",
  60. })
  61. if err != nil {
  62. return err
  63. }
  64. defer backend.Close(context.Background())
  65. if err := backend.Configure(context.Background(), output.BackendConfig{
  66. SampleRateHz: float64(g.cfg.FM.CompositeRateHz),
  67. Channels: 2,
  68. IQLevel: float32(g.cfg.FM.OutputDrive),
  69. }); err != nil {
  70. return err
  71. }
  72. frame := g.GenerateFrame(duration)
  73. if _, err := backend.Write(context.Background(), frame); err != nil {
  74. return err
  75. }
  76. if err := backend.Flush(context.Background()); err != nil {
  77. return err
  78. }
  79. return nil
  80. }
  81. func (g *Generator) Summary(duration time.Duration) string {
  82. return fmt.Sprintf("offline frame: freq=%.1fMHz sampleRate=%d duration=%s outputDrive=%.2f", g.cfg.FM.FrequencyMHz, g.cfg.FM.CompositeRateHz, duration.String(), g.cfg.FM.OutputDrive)
  83. }