Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

140 líneas
4.3KB

  1. package config
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. )
  7. type Config struct {
  8. Audio AudioConfig `json:"audio"`
  9. RDS RDSConfig `json:"rds"`
  10. FM FMConfig `json:"fm"`
  11. Backend BackendConfig `json:"backend"`
  12. Control ControlConfig `json:"control"`
  13. }
  14. type AudioConfig struct {
  15. InputPath string `json:"inputPath"`
  16. SampleRate int `json:"sampleRate"`
  17. Gain float64 `json:"gain"`
  18. ToneLeftHz float64 `json:"toneLeftHz"`
  19. ToneRightHz float64 `json:"toneRightHz"`
  20. ToneAmplitude float64 `json:"toneAmplitude"`
  21. }
  22. type RDSConfig struct {
  23. Enabled bool `json:"enabled"`
  24. PI string `json:"pi"`
  25. PS string `json:"ps"`
  26. RadioText string `json:"radioText"`
  27. PTY int `json:"pty"`
  28. }
  29. type FMConfig struct {
  30. FrequencyMHz float64 `json:"frequencyMHz"`
  31. StereoEnabled bool `json:"stereoEnabled"`
  32. PilotLevel float64 `json:"pilotLevel"`
  33. RDSInjection float64 `json:"rdsInjection"`
  34. PreEmphasisUS float64 `json:"preEmphasisUS"` // time constant in µs: 50 (EU) or 75 (US), 0=off
  35. OutputDrive float64 `json:"outputDrive"`
  36. CompositeRateHz int `json:"compositeRateHz"`
  37. MaxDeviationHz float64 `json:"maxDeviationHz"` // FM deviation in Hz, default 75000
  38. LimiterEnabled bool `json:"limiterEnabled"`
  39. LimiterCeiling float64 `json:"limiterCeiling"` // composite ceiling, default 1.0
  40. FMModulationEnabled bool `json:"fmModulationEnabled"` // true = output FM IQ, false = raw composite
  41. }
  42. type BackendConfig struct {
  43. Kind string `json:"kind"`
  44. Device string `json:"device"`
  45. OutputPath string `json:"outputPath"`
  46. }
  47. type ControlConfig struct {
  48. ListenAddress string `json:"listenAddress"`
  49. }
  50. func Default() Config {
  51. return Config{
  52. Audio: AudioConfig{SampleRate: 48000, Gain: 1.0, ToneLeftHz: 1000, ToneRightHz: 1600, ToneAmplitude: 0.4},
  53. RDS: RDSConfig{Enabled: true, PI: "1234", PS: "FMRTX", RadioText: "fm-rds-tx", PTY: 0},
  54. FM: FMConfig{
  55. FrequencyMHz: 100.0,
  56. StereoEnabled: true,
  57. PilotLevel: 0.1,
  58. RDSInjection: 0.05,
  59. PreEmphasisUS: 50, // European default
  60. OutputDrive: 0.5,
  61. CompositeRateHz: 228000,
  62. MaxDeviationHz: 75000,
  63. LimiterEnabled: true,
  64. LimiterCeiling: 1.0,
  65. FMModulationEnabled: true,
  66. },
  67. Backend: BackendConfig{Kind: "file", OutputPath: "build/out/composite.f32"},
  68. Control: ControlConfig{ListenAddress: "127.0.0.1:8088"},
  69. }
  70. }
  71. func Load(path string) (Config, error) {
  72. cfg := Default()
  73. if path == "" {
  74. return cfg, cfg.Validate()
  75. }
  76. data, err := os.ReadFile(path)
  77. if err != nil {
  78. return Config{}, err
  79. }
  80. if err := json.Unmarshal(data, &cfg); err != nil {
  81. return Config{}, err
  82. }
  83. return cfg, cfg.Validate()
  84. }
  85. func (c Config) Validate() error {
  86. if c.Audio.SampleRate < 8000 || c.Audio.SampleRate > 384000 {
  87. return fmt.Errorf("audio.sampleRate out of range")
  88. }
  89. if c.Audio.Gain < 0 || c.Audio.Gain > 4 {
  90. return fmt.Errorf("audio.gain out of range")
  91. }
  92. if c.Audio.ToneLeftHz <= 0 || c.Audio.ToneRightHz <= 0 {
  93. return fmt.Errorf("audio tone frequencies must be positive")
  94. }
  95. if c.Audio.ToneAmplitude < 0 || c.Audio.ToneAmplitude > 1 {
  96. return fmt.Errorf("audio.toneAmplitude out of range")
  97. }
  98. if c.FM.FrequencyMHz < 65 || c.FM.FrequencyMHz > 110 {
  99. return fmt.Errorf("fm.frequencyMHz out of range")
  100. }
  101. if c.FM.PilotLevel < 0 || c.FM.PilotLevel > 0.2 {
  102. return fmt.Errorf("fm.pilotLevel out of range")
  103. }
  104. if c.FM.RDSInjection < 0 || c.FM.RDSInjection > 0.15 {
  105. return fmt.Errorf("fm.rdsInjection out of range")
  106. }
  107. if c.FM.OutputDrive < 0 || c.FM.OutputDrive > 1 {
  108. return fmt.Errorf("fm.outputDrive out of range")
  109. }
  110. if c.FM.CompositeRateHz < 96000 || c.FM.CompositeRateHz > 1520000 {
  111. return fmt.Errorf("fm.compositeRateHz out of range")
  112. }
  113. if c.FM.PreEmphasisUS < 0 || c.FM.PreEmphasisUS > 100 {
  114. return fmt.Errorf("fm.preEmphasisUS out of range (0=off, 50=EU, 75=US)")
  115. }
  116. if c.FM.MaxDeviationHz < 0 || c.FM.MaxDeviationHz > 150000 {
  117. return fmt.Errorf("fm.maxDeviationHz out of range")
  118. }
  119. if c.FM.LimiterCeiling < 0 || c.FM.LimiterCeiling > 2 {
  120. return fmt.Errorf("fm.limiterCeiling out of range")
  121. }
  122. if c.Backend.Kind == "" {
  123. return fmt.Errorf("backend.kind is required")
  124. }
  125. if c.Control.ListenAddress == "" {
  126. return fmt.Errorf("control.listenAddress is required")
  127. }
  128. return nil
  129. }