diff --git a/docs/README.md b/docs/README.md index bf66fb4..5981e5a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,13 @@ - `go run ./cmd/fmrtx --simulate-tx --simulate-output build/sim/simulated-soapy.iqf32 --simulate-duration 250ms` - `go run ./cmd/offline -duration 500ms -output build/offline/composite.iqf32` +### Tone configuration + +The current no-hardware source can be parameterized via config: +- `audio.toneLeftHz` +- `audio.toneRightHz` +- `audio.toneAmplitude` + ### Internal DSP module - `cd internal` - `go test ./...` diff --git a/docs/config.sample.json b/docs/config.sample.json index 4397599..23ecaf4 100644 --- a/docs/config.sample.json +++ b/docs/config.sample.json @@ -2,7 +2,10 @@ "audio": { "inputPath": "", "sampleRate": 48000, - "gain": 1.0 + "gain": 1.0, + "toneLeftHz": 1000, + "toneRightHz": 1600, + "toneAmplitude": 0.4 }, "rds": { "enabled": true, diff --git a/internal/audio/source.go b/internal/audio/source.go index cad96ce..7f95153 100644 --- a/internal/audio/source.go +++ b/internal/audio/source.go @@ -22,6 +22,14 @@ func NewToneSource(sampleRate float64) *ToneSource { } } +func NewConfiguredToneSource(sampleRate, leftHz, rightHz, amplitude float64) *ToneSource { + src := NewToneSource(sampleRate) + src.LeftFreq = leftHz + src.RightFreq = rightHz + src.Amplitude = amplitude + return src +} + func (s *ToneSource) NextFrame() Frame { left := s.Amplitude * math.Sin(2*math.Pi*s.LeftPhase) right := s.Amplitude * math.Sin(2*math.Pi*s.RightPhase) diff --git a/internal/config/config.go b/internal/config/config.go index ff3b799..a300a12 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,9 +15,12 @@ type Config struct { } type AudioConfig struct { - InputPath string `json:"inputPath"` - SampleRate int `json:"sampleRate"` - Gain float64 `json:"gain"` + 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 { @@ -50,7 +53,7 @@ type ControlConfig struct { func Default() Config { return Config{ - Audio: AudioConfig{SampleRate: 48000, Gain: 1.0}, + 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"}, @@ -80,6 +83,12 @@ func (c Config) Validate() error { 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") } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index aae9e11..f2d6fd2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -16,15 +16,15 @@ func TestDefaultValidate(t *testing.T) { func TestLoadAndValidate(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.json") - if err := os.WriteFile(path, []byte(`{"fm":{"frequencyMHz":99.9},"backend":{"kind":"file","outputPath":"out.f32"},"control":{"listenAddress":"127.0.0.1:8088"}}`), 0o644); err != nil { + if err := os.WriteFile(path, []byte(`{"audio":{"toneLeftHz":900,"toneRightHz":1700,"toneAmplitude":0.3},"fm":{"frequencyMHz":99.9},"backend":{"kind":"file","outputPath":"out.f32"},"control":{"listenAddress":"127.0.0.1:8088"}}`), 0o644); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("load config: %v", err) } - if cfg.FM.FrequencyMHz != 99.9 { - t.Fatalf("unexpected frequency: %v", cfg.FM.FrequencyMHz) + if cfg.Audio.ToneLeftHz != 900 { + t.Fatalf("unexpected left tone: %v", cfg.Audio.ToneLeftHz) } } diff --git a/internal/offline/generator.go b/internal/offline/generator.go index 99e5ef7..d150bd3 100644 --- a/internal/offline/generator.go +++ b/internal/offline/generator.go @@ -55,7 +55,7 @@ func (g *Generator) GenerateFrame(duration time.Duration) *output.CompositeFrame }) rdsSamples := rdsEnc.Generate(samples) - source := audio.NewToneSource(sampleRate) + source := audio.NewConfiguredToneSource(sampleRate, g.cfg.Audio.ToneLeftHz, g.cfg.Audio.ToneRightHz, g.cfg.Audio.ToneAmplitude) for i := 0; i < samples; i++ { t := float64(i) / sampleRate @@ -108,5 +108,5 @@ func (g *Generator) WriteFile(path string, duration time.Duration) error { } func (g *Generator) Summary(duration time.Duration) string { - return fmt.Sprintf("offline frame: freq=%.1fMHz sampleRate=%d duration=%s outputDrive=%.2f stereo=%t rds=%t", g.cfg.FM.FrequencyMHz, g.cfg.FM.CompositeRateHz, duration.String(), g.cfg.FM.OutputDrive, g.cfg.FM.StereoEnabled, g.cfg.RDS.Enabled) + return fmt.Sprintf("offline frame: freq=%.1fMHz sampleRate=%d duration=%s outputDrive=%.2f stereo=%t rds=%t toneL=%.1f toneR=%.1f", g.cfg.FM.FrequencyMHz, g.cfg.FM.CompositeRateHz, duration.String(), g.cfg.FM.OutputDrive, g.cfg.FM.StereoEnabled, g.cfg.RDS.Enabled, g.cfg.Audio.ToneLeftHz, g.cfg.Audio.ToneRightHz) }