package offline import ( "context" "encoding/binary" "fmt" "math" "path/filepath" "time" cfgpkg "github.com/jan/fm-rds-tx/internal/config" "github.com/jan/fm-rds-tx/internal/output" ) type Generator struct { cfg cfgpkg.Config } func NewGenerator(cfg cfgpkg.Config) *Generator { return &Generator{cfg: cfg} } func (g *Generator) GenerateFrame(duration time.Duration) *output.CompositeFrame { sampleRate := float64(g.cfg.FM.CompositeRateHz) if sampleRate <= 0 { sampleRate = 228000 } samples := int(duration.Seconds() * sampleRate) if samples <= 0 { samples = int(sampleRate / 10) } frame := &output.CompositeFrame{ Samples: make([]output.IQSample, samples), SampleRateHz: sampleRate, Timestamp: time.Now().UTC(), Sequence: 1, } leftFreq := 1000.0 rightFreq := 1600.0 pilotFreq := 19000.0 rdsFreq := 57000.0 for i := 0; i < samples; i++ { t := float64(i) / sampleRate left := 0.4 * math.Sin(2*math.Pi*leftFreq*t) right := 0.4 * math.Sin(2*math.Pi*rightFreq*t+math.Pi/3) mono := (left + right) / 2 stereo := (left - right) / 2 * 0.8 * math.Sin(2*math.Pi*38000*t) pilot := g.cfg.FM.PilotLevel * math.Sin(2*math.Pi*pilotFreq*t) rds := g.cfg.FM.RDSInjection * math.Sin(2*math.Pi*rdsFreq*t) composite := (mono + stereo + pilot + rds) * g.cfg.FM.OutputDrive frame.Samples[i] = output.IQSample{I: float32(composite), Q: 0} } return frame } func (g *Generator) WriteFile(path string, duration time.Duration) error { if path == "" { path = g.cfg.Backend.OutputPath } if path == "" { path = filepath.Join("build", "offline", "composite.iqf32") } backend, err := output.NewFileBackend(path, binary.LittleEndian, output.BackendInfo{ Name: "offline-file", Description: "offline composite file backend", }) if err != nil { return err } defer backend.Close(context.Background()) if err := backend.Configure(context.Background(), output.BackendConfig{ SampleRateHz: float64(g.cfg.FM.CompositeRateHz), Channels: 2, IQLevel: float32(g.cfg.FM.OutputDrive), }); err != nil { return err } frame := g.GenerateFrame(duration) if _, err := backend.Write(context.Background(), frame); err != nil { return err } if err := backend.Flush(context.Background()); err != nil { return err } return nil } func (g *Generator) Summary(duration time.Duration) string { return fmt.Sprintf("offline frame: freq=%.1fMHz sampleRate=%d duration=%s outputDrive=%.2f", g.cfg.FM.FrequencyMHz, g.cfg.FM.CompositeRateHz, duration.String(), g.cfg.FM.OutputDrive) }