From ed67ed7fe3a13c9b51c1390794f8a43f58228218 Mon Sep 17 00:00:00 2001 From: rouggy Date: Fri, 31 Jul 2026 21:32:55 +0200 Subject: [PATCH] fix: replaying a QSO used the voice keyer's level and overdrove the rig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app.go | 33 +++++++++++++++++------ changelog.json | 6 +++-- frontend/src/components/SettingsModal.tsx | 10 +++++-- frontend/src/lib/i18n.tsx | 4 +-- frontend/wailsjs/go/models.ts | 2 ++ 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/app.go b/app.go index 06253c4..9ebfa15 100644 --- a/app.go +++ b/app.go @@ -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 diff --git a/changelog.json b/changelog.json index 5a8e09d..891530a 100644 --- a/changelog.json +++ b/changelog.json @@ -10,7 +10,8 @@ "The \"From radio\" and mic levels take effect as you drag the slider, and the RX level now applies to what you HEAR through OpsLog, not only to recordings.", "The padlocks on frequency, band, mode and times no longer flicker under the pointer and refuse the click.", "The callsign field is wider, taking the room from the RST pair.", - "MP3 recordings no longer carry a hiss of their own: the 16→32 kHz conversion mirrored the whole spectrum only 7 dB down. It is now 57 dB down." + "MP3 recordings no longer carry a hiss of their own: the 16→32 kHz conversion mirrored the whole spectrum only 7 dB down. It is now 57 dB down.", + "Replaying a QSO recording on the air has its own level, separate from the voice keyer: a recording comes from a receiver line output and needs far less gain than a message spoken into a microphone." ], "fr": [ "Les filtres du cluster gagnent NEW POTA et NEW COUNTY, à côté des pastilles de statut existantes.", @@ -20,7 +21,8 @@ "Les niveaux « From radio » et micro agissent pendant que vous déplacez le curseur, et le niveau RX s'applique désormais à ce que vous ENTENDEZ dans OpsLog, pas seulement aux enregistrements.", "Les cadenas de fréquence, bande, mode et heures ne scintillent plus sous le curseur et acceptent le clic.", "Le champ indicatif est plus large, la place venant de la paire RST.", - "Les enregistrements MP3 ne portent plus un souffle qui leur est propre : la conversion 16→32 kHz recopiait tout le spectre à seulement 7 dB en dessous. Il est désormais à 57 dB." + "Les enregistrements MP3 ne portent plus un souffle qui leur est propre : la conversion 16→32 kHz recopiait tout le spectre à seulement 7 dB en dessous. Il est désormais à 57 dB.", + "La relecture d'un enregistrement de QSO a son propre niveau, distinct du manipulateur vocal : une prise vient de la sortie ligne d'un récepteur et demande bien moins de gain qu'un message dit au micro." ] }, { diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 28cb4bf..f1be114 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1198,13 +1198,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan from_radio: string; to_radio: string; recording_device: string; listening_device: string; qso_record: boolean; qso_dir: string; preroll_seconds: number; ptt_method: 'none' | 'cat' | 'rts' | 'dtr'; ptt_port: string; format: 'wav' | 'mp3'; - from_gain: number; mic_gain: number; tx_gain: number; + from_gain: number; mic_gain: number; tx_gain: number; qso_play_gain: number; }; type AudioDev = { id: string; name: string; default: boolean }; const [audioCfg, setAudioCfg] = useState({ from_radio: '', to_radio: '', recording_device: '', listening_device: '', qso_record: false, qso_dir: '', preroll_seconds: 8, ptt_method: 'none', ptt_port: '', format: 'wav', - from_gain: 100, mic_gain: 100, tx_gain: 100, + from_gain: 100, mic_gain: 100, tx_gain: 100, qso_play_gain: 100, }); const [audioInputs, setAudioInputs] = useState([]); const [audioOutputs, setAudioOutputs] = useState([]); @@ -5118,6 +5118,12 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan onChange={(e) => { const v = parseInt(e.target.value, 10); setAudioField({ from_gain: v }); AudioApplyLevels(v, audioCfg.mic_gain); }} className="w-48 accent-primary" /> {audioCfg.from_gain}% + +
+ setAudioField({ qso_play_gain: parseInt(e.target.value, 10) })} className="w-48 accent-primary" /> + {audioCfg.qso_play_gain}% +