Przeglądaj źródła

fix(engine): reset DSP state before restart

Reset generator and upsampler state on engine start, add a full generator Reset path for stateful DSP/runtime components, and cover deterministic first-frame recovery after reset.
main
Jan 1 miesiąc temu
rodzic
commit
f83d32320a
3 zmienionych plików z 117 dodań i 9 usunięć
  1. +4
    -0
      internal/app/engine.go
  2. +90
    -9
      internal/offline/generator.go
  3. +23
    -0
      internal/offline/generator_test.go

+ 4
- 0
internal/app/engine.go Wyświetl plik

@@ -440,6 +440,10 @@ func (e *Engine) Start(ctx context.Context) error {
e.mu.Unlock()
return fmt.Errorf("driver start: %w", err)
}
e.generator.Reset()
if e.upsampler != nil {
e.upsampler.Reset()
}

runCtx, cancel := context.WithCancel(ctx)
e.cancel = cancel


+ 90
- 9
internal/offline/generator.go Wyświetl plik

@@ -205,6 +205,95 @@ func (g *Generator) RDSEncoder() *rds.Encoder {
return g.rdsEnc
}

func (g *Generator) resetSource() {
rawSource, _ := g.sourceFor(g.sampleRate)
g.source = NewPreEmphasizedSource(rawSource, g.cfg.FM.PreEmphasisTauUS, g.sampleRate, g.cfg.Audio.Gain)
}

func (g *Generator) resetRDS2Encoder() {
if !g.cfg.RDS.Enabled || !g.cfg.RDS.RDS2Enabled {
g.rds2Enc = nil
return
}
g.rds2Enc = rds.NewRDS2Encoder(g.sampleRate)
g.rds2Enc.Enable(true)
if g.cfg.RDS.StationLogoPath != "" {
if err := g.rds2Enc.LoadLogo(g.cfg.RDS.StationLogoPath); err != nil {
log.Printf("rds2: failed to load station logo: %v", err)
}
}
}

// Reset clears stateful DSP/runtime state so the next run starts from a clean baseline
// without changing the current live parameters or feature enablement.
func (g *Generator) Reset() {
if !g.initialized {
return
}

g.resetSource()

mode := g.appliedStereoMode
if lp := g.liveParams.Load(); lp != nil {
mode = canonicalStereoMode(lp.StereoMode)
}
g.stereoEncoder = stereo.NewStereoEncoder(g.sampleRate)
g.stereoEncoder.SetMode(stereo.ParseMode(mode), g.sampleRate)
g.appliedStereoMode = mode

if g.rdsEnc != nil {
g.rdsEnc.Reset()
}
g.resetRDS2Encoder()

if g.audioLPF_L != nil {
g.audioLPF_L.Reset()
}
if g.audioLPF_R != nil {
g.audioLPF_R.Reset()
}
if g.pilotNotchL != nil {
g.pilotNotchL.Reset()
}
if g.pilotNotchR != nil {
g.pilotNotchR.Reset()
}
if g.limiter != nil {
g.limiter.Reset()
}
if g.cleanupLPF_L != nil {
g.cleanupLPF_L.Reset()
}
if g.cleanupLPF_R != nil {
g.cleanupLPF_R.Reset()
}
if g.mpxNotch19 != nil {
g.mpxNotch19.Reset()
}
if g.mpxNotch57 != nil {
g.mpxNotch57.Reset()
}
if g.bs412 != nil {
g.bs412.Reset()
}
if g.compositeClip != nil {
g.compositeClip.Reset()
}
if g.fmMod != nil {
g.fmMod.Reset()
}

if g.watermarkEnabled {
g.stftEmbedder = watermark.NewSTFTEmbedder(g.watermarkKey)
g.wmDecimLPF = dsp.NewLPF4(5500, g.sampleRate)
g.wmInterpLPF = dsp.NewLPF4(5500, g.sampleRate)
} else {
g.stftEmbedder = nil
g.wmDecimLPF = nil
g.wmInterpLPF = nil
}
}

func (g *Generator) init() {
if g.initialized {
return
@@ -268,15 +357,7 @@ func (g *Generator) init() {
})

// RDS2: additional subcarriers (66.5, 71.25, 76 kHz)
if g.cfg.RDS.RDS2Enabled {
g.rds2Enc = rds.NewRDS2Encoder(g.sampleRate)
g.rds2Enc.Enable(true)
if g.cfg.RDS.StationLogoPath != "" {
if err := g.rds2Enc.LoadLogo(g.cfg.RDS.StationLogoPath); err != nil {
log.Printf("rds2: failed to load station logo: %v", err)
}
}
}
g.resetRDS2Encoder()
}
ceiling := g.cfg.FM.LimiterCeiling
if ceiling <= 0 {


+ 23
- 0
internal/offline/generator_test.go Wyświetl plik

@@ -211,3 +211,26 @@ func TestConfigureWatermarkExplicitOptIn(t *testing.T) {
t.Fatal("expected watermark embedder after explicit opt-in")
}
}

func TestGeneratorResetRestoresDeterministicFirstFrame(t *testing.T) {
cfg := cfgpkg.Default()
cfg.RDS.Enabled = false
cfg.FM.FMModulationEnabled = true
g := NewGenerator(cfg)

first := g.GenerateFrame(10 * time.Millisecond)
_ = g.GenerateFrame(10 * time.Millisecond)
g.Reset()
afterReset := g.GenerateFrame(10 * time.Millisecond)

if len(first.Samples) != len(afterReset.Samples) {
t.Fatalf("sample length mismatch: %d vs %d", len(first.Samples), len(afterReset.Samples))
}
for i := range first.Samples {
a := first.Samples[i]
b := afterReset.Samples[i]
if a != b {
t.Fatalf("sample %d differs after reset: first=(%v,%v) reset=(%v,%v)", i, a.I, a.Q, b.I, b.Q)
}
}
}

Ładowanie…
Anuluj
Zapisz