feat: stop a QSO recording and play it back on the air

The situation, in the operator's words: working VP2MAA, recording, and he asks
to hear his own signal. So: stop, play, and the recording is still saved with
the QSO.

Stopping FREEZES the take rather than ending it — nothing more is captured,
nothing is discarded, and logging writes the file exactly as before. Playback
reuses the voice keyer's path: same output device, same PTT keying and release
with its generation guard, so a replay cannot be cut short by a stale unkey.

Playing is refused while still recording. The transmitted audio would feed back
into the same take on any rig monitoring its own transmission, and stopping is
one click the operator has already made — it is their statement that the take is
finished, not a hoop.

The elapsed clock freezes with the recording and resumes from where it stopped,
because it now shows the LENGTH OF THE FILE rather than the time since the QSO
began. Only a fresh take returns it to zero.
This commit is contained in:
2026-07-30 21:36:42 +02:00
parent f221c0ee1e
commit 069728ac68
7 changed files with 226 additions and 16 deletions
+72
View File
@@ -7606,6 +7606,78 @@ func (a *App) stopManualQSORecorder() {
applog.Printf("qso-rec: manual recording finished — sound devices released")
}
// QSOAudioStop freezes the recording without ending it, so it can be played
// back and still be saved with the QSO afterwards.
func (a *App) QSOAudioStop() bool {
if a.qsoRec == nil {
return false
}
return a.qsoRec.PauseQSO()
}
// QSOAudioResume continues a frozen recording, appending to what is there.
func (a *App) QSOAudioResume() bool {
if a.qsoRec == nil {
return false
}
return a.qsoRec.ResumeQSO()
}
// QSOAudioPlayOnAir transmits the recording made so far, through the DVK's
// output device and PTT.
//
// The situation this is for: you are working a station, you have been recording,
// and they ask to hear it. You stop, you send it to them, and the recording is
// still saved with the QSO.
//
// It goes out over the AIR. So it refuses unless the take has been stopped
// first: playing while still recording would feed the transmitted audio back
// into the same take, and on a rig monitoring its own transmission that is a
// loop. Stopping is the operator's statement that the take is finished, and it
// is one click they have already made.
func (a *App) QSOAudioPlayOnAir() error {
if a.qsoRec == nil || a.audioMgr == nil {
return fmt.Errorf("audio not initialized")
}
if !a.qsoRec.Paused() {
return fmt.Errorf("stop the recording before playing it on the air")
}
pcm, err := a.qsoRec.PeekQSO()
if err != nil {
return err
}
// A plain WAV in the data dir, overwritten each time: the player takes a
// path, and MP3 encoding would add seconds to something the operator is
// waiting on with the transmitter keyed.
path := filepath.Join(a.dataDir, "qso_playback.wav")
if err := audio.WritePCM(path, pcm); err != nil {
return fmt.Errorf("prepare playback: %w", err)
}
cfg, _ := a.GetAudioSettings()
if err := a.pttKey(cfg); err != nil {
applog.Printf("qso-rec: PTT on failed before playback: %v", err)
// Keep going — the audio still reaches the rig and the operator may use VOX.
} else if cfg.PTTMethod != "" && cfg.PTTMethod != "none" {
a.pttMu.Lock()
a.dvkPttKeyed = true
a.pttMu.Unlock()
}
if err := a.audioMgr.Play(cfg.ToRadio, path, cfg.TXGain); err != nil {
a.pttMu.Lock()
keyed := a.dvkPttKeyed
gen := a.pttGen
a.dvkPttKeyed = false
a.pttMu.Unlock()
if keyed {
go a.dvkUnkeyPTT(gen)
}
return err
}
applog.Printf("qso-rec: playing the recording on the air (%d bytes)", len(pcm))
return nil
}
// QSOAudioCancel drops the in-progress recording (callsign cleared, QSO
// abandoned without logging).
func (a *App) QSOAudioCancel() {