|
- package offline
-
- import (
- "context"
- "encoding/binary"
- "fmt"
- "math"
- "path/filepath"
- "time"
-
- "github.com/jan/fm-rds-tx/internal/audio"
- cfgpkg "github.com/jan/fm-rds-tx/internal/config"
- "github.com/jan/fm-rds-tx/internal/mpx"
- "github.com/jan/fm-rds-tx/internal/output"
- "github.com/jan/fm-rds-tx/internal/rds"
- "github.com/jan/fm-rds-tx/internal/stereo"
- )
-
- 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,
- }
-
- stereoEncoder := stereo.NewStereoEncoder(sampleRate)
- combiner := mpx.NewDefaultCombiner()
- combiner.PilotGain = g.cfg.FM.PilotLevel
- combiner.RDSGain = g.cfg.FM.RDSInjection
-
- rdsEnc, _ := rds.NewEncoder(rds.RDSConfig{
- PI: 0x1234,
- PS: g.cfg.RDS.PS,
- RT: g.cfg.RDS.RadioText,
- PTY: uint8(g.cfg.RDS.PTY),
- SampleRate: sampleRate,
- })
- rdsSamples := rdsEnc.Generate(samples)
-
- 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
- in := source.NextFrame()
- comps := stereoEncoder.Encode(in)
- stereoDSB := comps.Stereo * math.Sin(2*math.Pi*38000.0*t)
- rdsValue := 0.0
- if g.cfg.RDS.Enabled && i < len(rdsSamples) {
- rdsValue = rdsSamples[i]
- }
- composite := combiner.Combine(comps.Mono, stereoDSB, comps.Pilot, rdsValue) * 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 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)
- }
|