fix(icom): create the audio manager before the CAT link comes up

reloadCAT builds the network audio sink gated on audioMgr, and startup created
the manager AFTER the first reloadCAT: every fresh launch connected with audio
silently off however the option was set, and the operator had to
untick/save/retick it to hear anything. The manager is idle-until-used, so
creating it earlier costs nothing.
This commit is contained in:
2026-08-29 16:47:42 +02:00
parent 490941f506
commit 1fba7d2d57
2 changed files with 47 additions and 40 deletions
+43 -38
View File
@@ -1252,6 +1252,49 @@ func (a *App) startup(ctx context.Context) {
go a.applyRelayAuto(s.FreqHz, s.Band)
}
})
// Digital Voice Keyer + QSO recorder (WASAPI). Idle until used. Created
// BEFORE the CAT link comes up, and it matters: a network Icom with RX
// audio enabled builds its audio sink inside reloadCAT, gated on audioMgr —
// when the manager was created later, every fresh launch connected with
// audio silently off, and the operator had to untick/save/retick the option
// to hear anything.
a.audioMgr = audio.NewManager(func() {
st := a.dvkStatus()
// When a voice message finishes (or is stopped), drop the PTT we keyed
// for it — but tag the release with the current key generation so it
// can't cut a transmission a newer message already started.
if !st.Playing {
a.pttMu.Lock()
keyed := a.dvkPttKeyed
gen := a.pttGen
if keyed {
a.dvkPttKeyed = false
}
a.pttMu.Unlock()
if keyed {
go a.dvkUnkeyPTT(gen)
}
// And give the microphone back, if we took it (see raiseFlexDVKDax).
// Off the callback's goroutine: this talks to the radio, and the
// audio manager is reporting a state change, not waiting on us.
go a.lowerFlexDVKDax()
}
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "audio:status", st)
}
})
a.qsoRec = audio.NewRecorder()
if a.audioMgr != nil {
// A running monitor picks the new level up immediately: the operator is
// listening while they move the slider, and asking them to stop and
// restart it to hear the change is how a working control gets reported
// as broken.
if cfg, err := a.GetAudioSettings(); err == nil {
a.audioMgr.SetMonitorGain(cfg.FromGain)
}
}
a.startQSORecorderIfEnabled()
a.reloadCAT()
// The QSO logbook lives where the ACTIVE PROFILE points it: the local SQLite
@@ -1479,44 +1522,6 @@ func (a *App) startup(ctx context.Context) {
})
a.solar.Start()
// Digital Voice Keyer + QSO recorder (WASAPI). Idle until used.
a.audioMgr = audio.NewManager(func() {
st := a.dvkStatus()
// When a voice message finishes (or is stopped), drop the PTT we keyed
// for it — but tag the release with the current key generation so it
// can't cut a transmission a newer message already started.
if !st.Playing {
a.pttMu.Lock()
keyed := a.dvkPttKeyed
gen := a.pttGen
if keyed {
a.dvkPttKeyed = false
}
a.pttMu.Unlock()
if keyed {
go a.dvkUnkeyPTT(gen)
}
// And give the microphone back, if we took it (see raiseFlexDVKDax).
// Off the callback's goroutine: this talks to the radio, and the
// audio manager is reporting a state change, not waiting on us.
go a.lowerFlexDVKDax()
}
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "audio:status", st)
}
})
a.qsoRec = audio.NewRecorder()
if a.audioMgr != nil {
// A running monitor picks the new level up immediately: the operator is
// listening while they move the slider, and asking them to stop and
// restart it to hear the change is how a working control gets reported
// as broken.
if cfg, err := a.GetAudioSettings(); err == nil {
a.audioMgr.SetMonitorGain(cfg.FromGain)
}
}
a.startQSORecorderIfEnabled()
// NET Control store (global JSON, shared across logbooks).
if ns, err := netctl.Open(filepath.Join(a.dataDir, "nets.json")); err != nil {
applog.Printf("netctl: open failed: %v", err)