fix: replaying a QSO used the voice keyer's level and overdrove the rig

A panadapter shot settles it: answering an S5 station, the replayed recording
goes out several times wider and taller than the station being answered — in WAV
as well as MP3, so this is not the encoder.

One setting served two sources that have nothing in common. A voice-keyer
message is a microphone at speaking distance and legitimately wants 195%; a QSO
recording is a receiver's line output, which is far hotter. The same 195% put it
into the transmitter flat out, ALC pinned, noise and voice alike.

The QSO playback now has its own level, defaulting to 100%. The voice keyer
keeps the setting it had, so nobody's F-key messages change.
This commit is contained in:
2026-07-31 21:32:55 +02:00
parent fddb3c45c4
commit ed67ed7fe3
5 changed files with 41 additions and 14 deletions
+25 -8
View File
@@ -141,11 +141,20 @@ const (
keyAudioQSODir = "audio.qso_dir" // folder for QSO recordings
keyAudioPreroll = "audio.preroll_seconds" // rolling-buffer pre-roll length
keyAudioTXGain = "audio.tx_gain" // voice-keyer playback level % (100 = as recorded)
keyAudioPTTMethod = "audio.ptt_method" // "none" (VOX) | "rts" | "dtr"
keyAudioPTTPort = "audio.ptt_port" // COM port for serial PTT
keyAudioFormat = "audio.qso_format" // "wav" | "mp3"
keyAudioFromGain = "audio.from_gain" // From Radio (RX) mix level, percent
keyAudioMicGain = "audio.mic_gain" // mic mix level, percent
// Replaying a QSO recording needs its OWN level, and by a wide margin.
//
// A voice-keyer message is a microphone at speaking distance; a QSO
// recording is a receiver's line output, which is far hotter. Sharing one
// setting meant the 195% an operator needs for their F-key messages was
// also applied to a recording, which then hit the transmitter flat out —
// visible on the panadapter as a signal several times the size of the S5
// station being answered.
keyAudioQSOPlayGain = "audio.qso_play_gain" // QSO-recording playback level %
keyAudioPTTMethod = "audio.ptt_method" // "none" (VOX) | "rts" | "dtr"
keyAudioPTTPort = "audio.ptt_port" // COM port for serial PTT
keyAudioFormat = "audio.qso_format" // "wav" | "mp3"
keyAudioFromGain = "audio.from_gain" // From Radio (RX) mix level, percent
keyAudioMicGain = "audio.mic_gain" // mic mix level, percent
keyAwardDefs = "awards.defs" // JSON array of award definitions (editable)
keyAwardRefsUpdated = "awards.refs.updated." // + CODE → last list-update timestamp
@@ -6958,6 +6967,7 @@ type AudioSettings struct {
FromGain int `json:"from_gain"` // From Radio (RX) mix level %, default 100
MicGain int `json:"mic_gain"` // mic mix level %, default 100
TXGain int `json:"tx_gain"` // voice-keyer playback level %, default 100
QSOPlayGain int `json:"qso_play_gain"` // QSO-recording playback level %, default 100
}
// ListAudioInputDevices / ListAudioOutputDevices enumerate WASAPI endpoints
@@ -6967,14 +6977,14 @@ func (a *App) ListAudioOutputDevices() ([]audio.Device, error) { return audio.Li
// GetAudioSettings returns the stored audio config (preroll defaults to 8s).
func (a *App) GetAudioSettings() (AudioSettings, error) {
out := AudioSettings{PrerollSeconds: 8, PTTMethod: "none", Format: "wav", FromGain: 100, MicGain: 100, TXGain: 100}
out := AudioSettings{PrerollSeconds: 8, PTTMethod: "none", Format: "wav", FromGain: 100, MicGain: 100, TXGain: 100, QSOPlayGain: 100}
if a.settings == nil {
return out, nil
}
m, err := a.settings.GetMany(a.ctx,
keyAudioFromRadio, keyAudioToRadio, keyAudioRecDevice, keyAudioListenDevice,
keyAudioQSORecord, keyAudioQSODir, keyAudioPreroll, keyAudioPTTMethod, keyAudioPTTPort, keyAudioFormat,
keyAudioFromGain, keyAudioMicGain, keyAudioTXGain)
keyAudioFromGain, keyAudioMicGain, keyAudioTXGain, keyAudioQSOPlayGain)
if err != nil {
return out, err
}
@@ -7005,6 +7015,9 @@ func (a *App) GetAudioSettings() (AudioSettings, error) {
if n, _ := strconv.Atoi(m[keyAudioTXGain]); n > 0 && n <= 400 {
out.TXGain = n
}
if n, _ := strconv.Atoi(m[keyAudioQSOPlayGain]); n > 0 && n <= 400 {
out.QSOPlayGain = n
}
return out, nil
}
@@ -7036,6 +7049,9 @@ func (a *App) SaveAudioSettings(s AudioSettings) error {
}
// Up to 400 %: a mic recorded quietly needs real amplification, and the
// clamp in the player keeps it from wrapping into noise.
if s.QSOPlayGain <= 0 || s.QSOPlayGain > 400 {
s.QSOPlayGain = 100
}
if s.TXGain <= 0 || s.TXGain > 400 {
s.TXGain = 100
}
@@ -7053,6 +7069,7 @@ func (a *App) SaveAudioSettings(s AudioSettings) error {
keyAudioFromGain: strconv.Itoa(s.FromGain),
keyAudioMicGain: strconv.Itoa(s.MicGain),
keyAudioTXGain: strconv.Itoa(s.TXGain),
keyAudioQSOPlayGain: strconv.Itoa(s.QSOPlayGain),
} {
if err := a.settings.Set(a.ctx, k, v); err != nil {
return err
@@ -7777,7 +7794,7 @@ func (a *App) QSOAudioPlayOnAir() error {
a.dvkPttKeyed = true
a.pttMu.Unlock()
}
if err := a.audioMgr.Play(cfg.ToRadio, path, cfg.TXGain); err != nil {
if err := a.audioMgr.Play(cfg.ToRadio, path, cfg.QSOPlayGain); err != nil {
applog.Printf("qso-rec: playback on %q failed: %v", cfg.ToRadio, err)
a.pttMu.Lock()
keyed := a.dvkPttKeyed