| @@ -1,7 +1,6 @@ | |||
| { | |||
| "audio": { | |||
| "inputPath": "", | |||
| "inputSampleRate": 48000, | |||
| "gain": 1.0, | |||
| "toneLeftHz": 1000, | |||
| "toneRightHz": 1600, | |||
| @@ -17,12 +17,11 @@ type Config struct { | |||
| } | |||
| type AudioConfig struct { | |||
| InputPath string `json:"inputPath"` | |||
| InputSampleRate int `json:"inputSampleRate"` // sample rate for WAV/tone source | |||
| Gain float64 `json:"gain"` | |||
| ToneLeftHz float64 `json:"toneLeftHz"` | |||
| ToneRightHz float64 `json:"toneRightHz"` | |||
| ToneAmplitude float64 `json:"toneAmplitude"` | |||
| InputPath string `json:"inputPath"` | |||
| Gain float64 `json:"gain"` | |||
| ToneLeftHz float64 `json:"toneLeftHz"` | |||
| ToneRightHz float64 `json:"toneRightHz"` | |||
| ToneAmplitude float64 `json:"toneAmplitude"` | |||
| } | |||
| type RDSConfig struct { | |||
| @@ -60,7 +59,7 @@ type ControlConfig struct { | |||
| func Default() Config { | |||
| return Config{ | |||
| Audio: AudioConfig{InputSampleRate: 48000, Gain: 1.0, ToneLeftHz: 1000, ToneRightHz: 1600, ToneAmplitude: 0.4}, | |||
| Audio: AudioConfig{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, | |||
| @@ -111,9 +110,6 @@ func Load(path string) (Config, error) { | |||
| } | |||
| func (c Config) Validate() error { | |||
| if c.Audio.InputSampleRate < 8000 || c.Audio.InputSampleRate > 384000 { | |||
| return fmt.Errorf("audio.inputSampleRate out of range") | |||
| } | |||
| if c.Audio.Gain < 0 || c.Audio.Gain > 4 { | |||
| return fmt.Errorf("audio.gain out of range") | |||
| } | |||
| @@ -41,12 +41,8 @@ func TestDefaultFMModulation(t *testing.T) { | |||
| func TestParsePI(t *testing.T) { | |||
| tests := []struct{ in string; want uint16; ok bool }{ | |||
| {"1234", 0x1234, true}, | |||
| {"0xBEEF", 0xBEEF, true}, | |||
| {"0XCAFE", 0xCAFE, true}, | |||
| {" 0x2345 ", 0x2345, true}, | |||
| {"", 0, false}, | |||
| {"nope", 0, false}, | |||
| {"1234", 0x1234, true}, {"0xBEEF", 0xBEEF, true}, {"0XCAFE", 0xCAFE, true}, | |||
| {" 0x2345 ", 0x2345, true}, {"", 0, false}, {"nope", 0, false}, | |||
| } | |||
| for _, tt := range tests { | |||
| got, err := ParsePI(tt.in) | |||
| @@ -58,17 +54,17 @@ func TestParsePI(t *testing.T) { | |||
| func TestValidateRejectsInvalidPI(t *testing.T) { | |||
| cfg := Default(); cfg.RDS.PI = "nope" | |||
| if err := cfg.Validate(); err == nil { t.Fatal("expected error for invalid PI") } | |||
| if err := cfg.Validate(); err == nil { t.Fatal("expected error") } | |||
| } | |||
| func TestValidateRejectsEmptyPI(t *testing.T) { | |||
| cfg := Default(); cfg.RDS.PI = "" | |||
| if err := cfg.Validate(); err == nil { t.Fatal("expected error for empty PI") } | |||
| if err := cfg.Validate(); err == nil { t.Fatal("expected error") } | |||
| } | |||
| func TestEffectiveDeviceRate(t *testing.T) { | |||
| cfg := Default() | |||
| if cfg.EffectiveDeviceRate() != float64(cfg.FM.CompositeRateHz) { t.Fatal("expected composite rate when device rate is 0") } | |||
| if cfg.EffectiveDeviceRate() != float64(cfg.FM.CompositeRateHz) { t.Fatal("expected composite rate") } | |||
| cfg.Backend.DeviceSampleRateHz = 912000 | |||
| if cfg.EffectiveDeviceRate() != 912000 { t.Fatal("expected 912000") } | |||
| } | |||
| @@ -10,22 +10,21 @@ import ( | |||
| ) | |||
| type FrameSummary struct { | |||
| Mode string `json:"mode"` | |||
| FrequencyMHz float64 `json:"frequencyMHz"` | |||
| StereoEnabled bool `json:"stereoEnabled"` | |||
| RDSEnabled bool `json:"rdsEnabled"` | |||
| InputSampleRateHz int `json:"inputSampleRateHz"` | |||
| CompositeRate int `json:"compositeRateHz"` | |||
| DeviceRate float64 `json:"deviceSampleRateHz"` | |||
| PilotLevel float64 `json:"pilotLevel"` | |||
| RDSInjection float64 `json:"rdsInjection"` | |||
| OutputDrive float64 `json:"outputDrive"` | |||
| PreEmphasisTauUS float64 `json:"preEmphasisTauUS"` | |||
| MaxDeviationHz float64 `json:"maxDeviationHz"` | |||
| LimiterEnabled bool `json:"limiterEnabled"` | |||
| FMModulation bool `json:"fmModulationEnabled"` | |||
| Source string `json:"source"` | |||
| PreviewSamples []float64 `json:"previewSamples"` | |||
| Mode string `json:"mode"` | |||
| FrequencyMHz float64 `json:"frequencyMHz"` | |||
| StereoEnabled bool `json:"stereoEnabled"` | |||
| RDSEnabled bool `json:"rdsEnabled"` | |||
| CompositeRate int `json:"compositeRateHz"` | |||
| DeviceRate float64 `json:"deviceSampleRateHz"` | |||
| PilotLevel float64 `json:"pilotLevel"` | |||
| RDSInjection float64 `json:"rdsInjection"` | |||
| OutputDrive float64 `json:"outputDrive"` | |||
| PreEmphasisTauUS float64 `json:"preEmphasisTauUS"` | |||
| MaxDeviationHz float64 `json:"maxDeviationHz"` | |||
| LimiterEnabled bool `json:"limiterEnabled"` | |||
| FMModulation bool `json:"fmModulationEnabled"` | |||
| Source string `json:"source"` | |||
| PreviewSamples []float64 `json:"previewSamples"` | |||
| } | |||
| func Generate(cfg cfgpkg.Config) FrameSummary { | |||
| @@ -43,22 +42,21 @@ func Generate(cfg cfgpkg.Config) FrameSummary { | |||
| source = cfg.Audio.InputPath | |||
| } | |||
| return FrameSummary{ | |||
| Mode: "dry-run", | |||
| FrequencyMHz: cfg.FM.FrequencyMHz, | |||
| StereoEnabled: cfg.FM.StereoEnabled, | |||
| RDSEnabled: cfg.RDS.Enabled, | |||
| InputSampleRateHz: cfg.Audio.InputSampleRate, | |||
| CompositeRate: cfg.FM.CompositeRateHz, | |||
| DeviceRate: cfg.EffectiveDeviceRate(), | |||
| PilotLevel: cfg.FM.PilotLevel, | |||
| RDSInjection: cfg.FM.RDSInjection, | |||
| OutputDrive: cfg.FM.OutputDrive, | |||
| PreEmphasisTauUS: cfg.FM.PreEmphasisTauUS, | |||
| MaxDeviationHz: cfg.FM.MaxDeviationHz, | |||
| LimiterEnabled: cfg.FM.LimiterEnabled, | |||
| FMModulation: cfg.FM.FMModulationEnabled, | |||
| Source: source, | |||
| PreviewSamples: preview, | |||
| Mode: "dry-run", | |||
| FrequencyMHz: cfg.FM.FrequencyMHz, | |||
| StereoEnabled: cfg.FM.StereoEnabled, | |||
| RDSEnabled: cfg.RDS.Enabled, | |||
| CompositeRate: cfg.FM.CompositeRateHz, | |||
| DeviceRate: cfg.EffectiveDeviceRate(), | |||
| PilotLevel: cfg.FM.PilotLevel, | |||
| RDSInjection: cfg.FM.RDSInjection, | |||
| OutputDrive: cfg.FM.OutputDrive, | |||
| PreEmphasisTauUS: cfg.FM.PreEmphasisTauUS, | |||
| MaxDeviationHz: cfg.FM.MaxDeviationHz, | |||
| LimiterEnabled: cfg.FM.LimiterEnabled, | |||
| FMModulation: cfg.FM.FMModulationEnabled, | |||
| Source: source, | |||
| PreviewSamples: preview, | |||
| } | |||
| } | |||
| @@ -11,17 +11,17 @@ import ( | |||
| func TestGenerate(t *testing.T) { | |||
| cfg := cfgpkg.Default() | |||
| frame := Generate(cfg) | |||
| if frame.Mode != "dry-run" { t.Fatalf("unexpected mode: %s", frame.Mode) } | |||
| if len(frame.PreviewSamples) != 16 { t.Fatalf("unexpected preview length: %d", len(frame.PreviewSamples)) } | |||
| if frame.Source != "tones" { t.Fatalf("unexpected source: %s", frame.Source) } | |||
| if frame.PreEmphasisTauUS != 50 { t.Fatalf("unexpected preEmphasisTauUS: %.0f", frame.PreEmphasisTauUS) } | |||
| if !frame.FMModulation { t.Fatal("expected fmModulationEnabled=true") } | |||
| if frame.DeviceRate != float64(cfg.FM.CompositeRateHz) { t.Fatalf("expected deviceRate=%d, got %.0f", cfg.FM.CompositeRateHz, frame.DeviceRate) } | |||
| if frame.Mode != "dry-run" { t.Fatalf("mode: %s", frame.Mode) } | |||
| if len(frame.PreviewSamples) != 16 { t.Fatal("preview length") } | |||
| if frame.Source != "tones" { t.Fatal("source") } | |||
| if frame.PreEmphasisTauUS != 50 { t.Fatal("preemph") } | |||
| if !frame.FMModulation { t.Fatal("fm mod") } | |||
| if frame.DeviceRate != float64(cfg.FM.CompositeRateHz) { t.Fatal("device rate") } | |||
| } | |||
| func TestWriteJSONFile(t *testing.T) { | |||
| dir := t.TempDir() | |||
| out := filepath.Join(dir, "frame.json") | |||
| if err := WriteJSON(out, Generate(cfgpkg.Default())); err != nil { t.Fatalf("WriteJSON: %v", err) } | |||
| if _, err := os.Stat(out); err != nil { t.Fatalf("expected output: %v", err) } | |||
| if err := WriteJSON(out, Generate(cfgpkg.Default())); err != nil { t.Fatal(err) } | |||
| if _, err := os.Stat(out); err != nil { t.Fatal(err) } | |||
| } | |||