| @@ -1,7 +1,6 @@ | |||||
| { | { | ||||
| "audio": { | "audio": { | ||||
| "inputPath": "", | "inputPath": "", | ||||
| "inputSampleRate": 48000, | |||||
| "gain": 1.0, | "gain": 1.0, | ||||
| "toneLeftHz": 1000, | "toneLeftHz": 1000, | ||||
| "toneRightHz": 1600, | "toneRightHz": 1600, | ||||
| @@ -17,12 +17,11 @@ type Config struct { | |||||
| } | } | ||||
| type AudioConfig 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 { | type RDSConfig struct { | ||||
| @@ -60,7 +59,7 @@ type ControlConfig struct { | |||||
| func Default() Config { | func Default() Config { | ||||
| return 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}, | RDS: RDSConfig{Enabled: true, PI: "1234", PS: "FMRTX", RadioText: "fm-rds-tx", PTY: 0}, | ||||
| FM: FMConfig{ | FM: FMConfig{ | ||||
| FrequencyMHz: 100.0, | FrequencyMHz: 100.0, | ||||
| @@ -111,9 +110,6 @@ func Load(path string) (Config, error) { | |||||
| } | } | ||||
| func (c Config) Validate() 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 { | if c.Audio.Gain < 0 || c.Audio.Gain > 4 { | ||||
| return fmt.Errorf("audio.gain out of range") | return fmt.Errorf("audio.gain out of range") | ||||
| } | } | ||||
| @@ -41,12 +41,8 @@ func TestDefaultFMModulation(t *testing.T) { | |||||
| func TestParsePI(t *testing.T) { | func TestParsePI(t *testing.T) { | ||||
| tests := []struct{ in string; want uint16; ok bool }{ | 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 { | for _, tt := range tests { | ||||
| got, err := ParsePI(tt.in) | got, err := ParsePI(tt.in) | ||||
| @@ -58,17 +54,17 @@ func TestParsePI(t *testing.T) { | |||||
| func TestValidateRejectsInvalidPI(t *testing.T) { | func TestValidateRejectsInvalidPI(t *testing.T) { | ||||
| cfg := Default(); cfg.RDS.PI = "nope" | 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) { | func TestValidateRejectsEmptyPI(t *testing.T) { | ||||
| cfg := Default(); cfg.RDS.PI = "" | 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) { | func TestEffectiveDeviceRate(t *testing.T) { | ||||
| cfg := Default() | 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 | cfg.Backend.DeviceSampleRateHz = 912000 | ||||
| if cfg.EffectiveDeviceRate() != 912000 { t.Fatal("expected 912000") } | if cfg.EffectiveDeviceRate() != 912000 { t.Fatal("expected 912000") } | ||||
| } | } | ||||
| @@ -10,22 +10,21 @@ import ( | |||||
| ) | ) | ||||
| type FrameSummary struct { | 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 { | func Generate(cfg cfgpkg.Config) FrameSummary { | ||||
| @@ -43,22 +42,21 @@ func Generate(cfg cfgpkg.Config) FrameSummary { | |||||
| source = cfg.Audio.InputPath | source = cfg.Audio.InputPath | ||||
| } | } | ||||
| return FrameSummary{ | 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) { | func TestGenerate(t *testing.T) { | ||||
| cfg := cfgpkg.Default() | cfg := cfgpkg.Default() | ||||
| frame := Generate(cfg) | 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) { | func TestWriteJSONFile(t *testing.T) { | ||||
| dir := t.TempDir() | dir := t.TempDir() | ||||
| out := filepath.Join(dir, "frame.json") | 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) } | |||||
| } | } | ||||