package config import ( "encoding/json" "fmt" "os" ) type Config struct { Audio AudioConfig `json:"audio"` RDS RDSConfig `json:"rds"` FM FMConfig `json:"fm"` Backend BackendConfig `json:"backend"` Control ControlConfig `json:"control"` } type AudioConfig struct { InputPath string `json:"inputPath"` SampleRate int `json:"sampleRate"` Gain float64 `json:"gain"` ToneLeftHz float64 `json:"toneLeftHz"` ToneRightHz float64 `json:"toneRightHz"` ToneAmplitude float64 `json:"toneAmplitude"` } type RDSConfig struct { Enabled bool `json:"enabled"` PI string `json:"pi"` PS string `json:"ps"` RadioText string `json:"radioText"` PTY int `json:"pty"` } type FMConfig struct { FrequencyMHz float64 `json:"frequencyMHz"` StereoEnabled bool `json:"stereoEnabled"` PilotLevel float64 `json:"pilotLevel"` RDSInjection float64 `json:"rdsInjection"` PreEmphasisUS bool `json:"preEmphasisUS"` OutputDrive float64 `json:"outputDrive"` CompositeRateHz int `json:"compositeRateHz"` } type BackendConfig struct { Kind string `json:"kind"` Device string `json:"device"` OutputPath string `json:"outputPath"` } type ControlConfig struct { ListenAddress string `json:"listenAddress"` } func Default() Config { return Config{ Audio: AudioConfig{SampleRate: 48000, Gain: 1.0, ToneLeftHz: 1000, ToneRightHz: 1600, ToneAmplitude: 0.4}, RDS: RDSConfig{Enabled: true, PI: "1234", PS: "FMRTX", RadioText: "fm-rds-tx", PTY: 0}, FM: FMConfig{FrequencyMHz: 100.0, StereoEnabled: true, PilotLevel: 0.1, RDSInjection: 0.03, OutputDrive: 0.5, CompositeRateHz: 228000}, Backend: BackendConfig{Kind: "file", OutputPath: "build/out/composite.f32"}, Control: ControlConfig{ListenAddress: "127.0.0.1:8088"}, } } func Load(path string) (Config, error) { cfg := Default() if path == "" { return cfg, cfg.Validate() } data, err := os.ReadFile(path) if err != nil { return Config{}, err } if err := json.Unmarshal(data, &cfg); err != nil { return Config{}, err } return cfg, cfg.Validate() } func (c Config) Validate() error { if c.Audio.SampleRate < 8000 || c.Audio.SampleRate > 384000 { return fmt.Errorf("audio.sampleRate out of range") } if c.Audio.Gain < 0 || c.Audio.Gain > 4 { return fmt.Errorf("audio.gain out of range") } if c.Audio.ToneLeftHz <= 0 || c.Audio.ToneRightHz <= 0 { return fmt.Errorf("audio tone frequencies must be positive") } if c.Audio.ToneAmplitude < 0 || c.Audio.ToneAmplitude > 1 { return fmt.Errorf("audio.toneAmplitude out of range") } if c.FM.FrequencyMHz < 65 || c.FM.FrequencyMHz > 110 { return fmt.Errorf("fm.frequencyMHz out of range") } if c.FM.PilotLevel < 0 || c.FM.PilotLevel > 0.2 { return fmt.Errorf("fm.pilotLevel out of range") } if c.FM.RDSInjection < 0 || c.FM.RDSInjection > 0.1 { return fmt.Errorf("fm.rdsInjection out of range") } if c.FM.OutputDrive < 0 || c.FM.OutputDrive > 1 { return fmt.Errorf("fm.outputDrive out of range") } if c.FM.CompositeRateHz < 96000 || c.FM.CompositeRateHz > 1520000 { return fmt.Errorf("fm.compositeRateHz out of range") } if c.Backend.Kind == "" { return fmt.Errorf("backend.kind is required") } if c.Control.ListenAddress == "" { return fmt.Errorf("control.listenAddress is required") } return nil }