Bläddra i källkod

fix(stereo): apply mode changes at chunk boundaries

Stop reconfiguring the stereo encoder from UpdateLive, stage canonical stereoMode values as desired state, and apply SetMode only from the DSP thread at the start of GenerateFrame. Add tests covering deferred mode application and mode-name canonicalization.
main
Jan 1 månad sedan
förälder
incheckning
740e7f5f98
1 ändrade filer med 25 tillägg och 17 borttagningar
  1. +25
    -17
      internal/offline/generator.go

+ 25
- 17
internal/offline/generator.go Visa fil

@@ -85,15 +85,16 @@ type Generator struct {
cfg cfgpkg.Config

// Persistent DSP state across GenerateFrame calls
source *PreEmphasizedSource
stereoEncoder stereo.StereoEncoder
rdsEnc *rds.Encoder
rds2Enc *rds.RDS2Encoder
combiner mpx.DefaultCombiner
fmMod *dsp.FMModulator
sampleRate float64
initialized bool
frameSeq uint64
source *PreEmphasizedSource
stereoEncoder stereo.StereoEncoder
appliedStereoMode string // canonical mode currently applied to stereoEncoder; DSP goroutine only
rdsEnc *rds.Encoder
rds2Enc *rds.RDS2Encoder
combiner mpx.DefaultCombiner
fmMod *dsp.FMModulator
sampleRate float64
initialized bool
frameSeq uint64

// Broadcast-standard clip-filter-clip chain (per channel L/R):
//
@@ -178,13 +179,10 @@ func (g *Generator) SetExternalSource(src frameSource) error {
return nil
}

// UpdateLive hot-swaps DSP parameters. Thread-safe — called from control API,
// applied at the next chunk boundary by the DSP goroutine.
// UpdateLive hot-swaps DSP parameters. Thread-safe — called from control API.
// The DSP goroutine applies mode changes at the next chunk boundary.
func (g *Generator) UpdateLive(p LiveParams) {
// Detect stereo mode change: requires reconfiguring the encoder's Hilbert filter.
if old := g.liveParams.Load(); old != nil && old.StereoMode != p.StereoMode {
g.stereoEncoder.SetMode(stereo.ParseMode(p.StereoMode), g.sampleRate)
}
p.StereoMode = canonicalStereoMode(p.StereoMode)
g.liveParams.Store(&p)
}

@@ -197,6 +195,10 @@ func (g *Generator) CurrentLiveParams() LiveParams {
return LiveParams{OutputDrive: 1.0, LimiterCeiling: 1.0, MpxGain: 1.0}
}

func canonicalStereoMode(mode string) string {
return stereo.ParseMode(mode).String()
}

// RDSEncoder returns the live RDS encoder, or nil if RDS is disabled.
// Used by the Engine to forward text updates.
func (g *Generator) RDSEncoder() *rds.Encoder {
@@ -223,7 +225,8 @@ func (g *Generator) init() {
rawSource, _ := g.sourceFor(g.sampleRate)
g.source = NewPreEmphasizedSource(rawSource, g.cfg.FM.PreEmphasisTauUS, g.sampleRate, g.cfg.Audio.Gain)
g.stereoEncoder = stereo.NewStereoEncoder(g.sampleRate)
g.stereoEncoder.SetMode(stereo.ParseMode(g.cfg.FM.StereoMode), g.sampleRate)
g.appliedStereoMode = canonicalStereoMode(g.cfg.FM.StereoMode)
g.stereoEncoder.SetMode(stereo.ParseMode(g.appliedStereoMode), g.sampleRate)
g.combiner = mpx.DefaultCombiner{
MonoGain: 1.0, StereoGain: 1.0,
PilotGain: g.cfg.FM.PilotLevel, RDSGain: g.cfg.FM.RDSInjection,
@@ -335,7 +338,7 @@ func (g *Generator) init() {
g.liveParams.Store(&LiveParams{
OutputDrive: g.cfg.FM.OutputDrive,
StereoEnabled: g.cfg.FM.StereoEnabled,
StereoMode: g.cfg.FM.StereoMode,
StereoMode: g.appliedStereoMode,
PilotLevel: g.cfg.FM.PilotLevel,
RDSInjection: g.cfg.FM.RDSInjection,
RDSEnabled: g.cfg.RDS.Enabled,
@@ -418,6 +421,11 @@ func (g *Generator) GenerateFrame(duration time.Duration) *output.CompositeFrame
lp = &LiveParams{OutputDrive: 1.0, LimiterCeiling: 1.0, MpxGain: 1.0}
}

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

// Apply live tone and gain updates each chunk. GenerateFrame runs on a
// single goroutine so these field writes are safe without additional locking.
if g.toneSource != nil {


Laddar…
Avbryt
Spara