|
|
@@ -85,15 +85,16 @@ type Generator struct { |
|
|
cfg cfgpkg.Config |
|
|
cfg cfgpkg.Config |
|
|
|
|
|
|
|
|
// Persistent DSP state across GenerateFrame calls |
|
|
// 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): |
|
|
// Broadcast-standard clip-filter-clip chain (per channel L/R): |
|
|
// |
|
|
// |
|
|
@@ -178,13 +179,10 @@ func (g *Generator) SetExternalSource(src frameSource) error { |
|
|
return nil |
|
|
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) { |
|
|
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) |
|
|
g.liveParams.Store(&p) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -197,6 +195,10 @@ func (g *Generator) CurrentLiveParams() LiveParams { |
|
|
return LiveParams{OutputDrive: 1.0, LimiterCeiling: 1.0, MpxGain: 1.0} |
|
|
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. |
|
|
// RDSEncoder returns the live RDS encoder, or nil if RDS is disabled. |
|
|
// Used by the Engine to forward text updates. |
|
|
// Used by the Engine to forward text updates. |
|
|
func (g *Generator) RDSEncoder() *rds.Encoder { |
|
|
func (g *Generator) RDSEncoder() *rds.Encoder { |
|
|
@@ -223,7 +225,8 @@ func (g *Generator) init() { |
|
|
rawSource, _ := g.sourceFor(g.sampleRate) |
|
|
rawSource, _ := g.sourceFor(g.sampleRate) |
|
|
g.source = NewPreEmphasizedSource(rawSource, g.cfg.FM.PreEmphasisTauUS, g.sampleRate, g.cfg.Audio.Gain) |
|
|
g.source = NewPreEmphasizedSource(rawSource, g.cfg.FM.PreEmphasisTauUS, g.sampleRate, g.cfg.Audio.Gain) |
|
|
g.stereoEncoder = stereo.NewStereoEncoder(g.sampleRate) |
|
|
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{ |
|
|
g.combiner = mpx.DefaultCombiner{ |
|
|
MonoGain: 1.0, StereoGain: 1.0, |
|
|
MonoGain: 1.0, StereoGain: 1.0, |
|
|
PilotGain: g.cfg.FM.PilotLevel, RDSGain: g.cfg.FM.RDSInjection, |
|
|
PilotGain: g.cfg.FM.PilotLevel, RDSGain: g.cfg.FM.RDSInjection, |
|
|
@@ -335,7 +338,7 @@ func (g *Generator) init() { |
|
|
g.liveParams.Store(&LiveParams{ |
|
|
g.liveParams.Store(&LiveParams{ |
|
|
OutputDrive: g.cfg.FM.OutputDrive, |
|
|
OutputDrive: g.cfg.FM.OutputDrive, |
|
|
StereoEnabled: g.cfg.FM.StereoEnabled, |
|
|
StereoEnabled: g.cfg.FM.StereoEnabled, |
|
|
StereoMode: g.cfg.FM.StereoMode, |
|
|
|
|
|
|
|
|
StereoMode: g.appliedStereoMode, |
|
|
PilotLevel: g.cfg.FM.PilotLevel, |
|
|
PilotLevel: g.cfg.FM.PilotLevel, |
|
|
RDSInjection: g.cfg.FM.RDSInjection, |
|
|
RDSInjection: g.cfg.FM.RDSInjection, |
|
|
RDSEnabled: g.cfg.RDS.Enabled, |
|
|
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} |
|
|
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 |
|
|
// Apply live tone and gain updates each chunk. GenerateFrame runs on a |
|
|
// single goroutine so these field writes are safe without additional locking. |
|
|
// single goroutine so these field writes are safe without additional locking. |
|
|
if g.toneSource != nil { |
|
|
if g.toneSource != nil { |
|
|
|