feat(audio): раздельная громкость RX и TX-монитора + фикс синка слайдера

Две сохраняемые громкости: FVolume (RX) и FTxMonVolume (self-monitor
даунлинка на передаче, дефолт 40). Контекст монитора = TX + DUP + RX MUTE
off (тот же гейт, что аудио в движке). В этом контексте слайдер/SetVolume
правят FTxMonVolume, иначе FVolume; ActiveVolume — активная под контекст.
Автопереключение громкости RX<->TX-монитор на T/R (SetMOX) и при смене
RX MUTE / DUP во время передачи (ApplyActiveVolume). Обе персистятся
(tx_mon_volume в конфиге). Веб выровнен на ActiveVolume.

Фикс: слайдер громкости не синхронизировался с загруженным значением после
подключения (в rfDevice синкался только drive, volume пропущен) → слайдер
оставался на 70, движок брал сохранённое → «прыжок» при первом касании.
Добавлен TrkVolume.Position := ActiveVolume в rfDevice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:24:57 +03:00
co-authored by Claude Opus 4.8
parent ff2082ab04
commit e13d610a23
4 changed files with 49 additions and 10 deletions
+3 -2
View File
@@ -1313,7 +1313,7 @@ begin
rfAGCMode: rfAGCMode:
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode); for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
rfVolume: rfVolume:
TrkVolume.Position := FController.FVolume; TrkVolume.Position := FController.ActiveVolume; // RX или TX-монитор
rfRxGain: rfRxGain:
UpdateRxGainUI; UpdateRxGainUI;
rfDrive: rfDrive:
@@ -1457,6 +1457,7 @@ begin
RebuildXvtrButtons; RebuildXvtrButtons;
PushXvtrToWeb; PushXvtrToWeb;
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive% TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
TrkVolume.Position := FController.ActiveVolume; // слайдер ← загруженная громкость (был баг: не синхронизировался)
UpdateDriveLabel; // переключаем единицу %/дБ под бэкенд UpdateDriveLabel; // переключаем единицу %/дБ под бэкенд
if Assigned(FSampleRateOverlay) then if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate); FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
@@ -5372,7 +5373,7 @@ procedure TMainForm.PushVfoOverlayExtState;
begin begin
if not (Assigned(FVfoOverlay) and FVfoOverlay.Visible) then Exit; if not (Assigned(FVfoOverlay) and FVfoOverlay.Visible) then Exit;
// TxSel: этот слайс выбран для передачи. Пока слайс один → всегда True. // TxSel: этот слайс выбран для передачи. Пока слайс один → всегда True.
FVfoOverlay.SetExtState(FController.FVolume, FController.FMuted, FVfoOverlay.SetExtState(FController.ActiveVolume, FController.FMuted,
FController.FSplitTxB, FController.FTransmitting, True); FController.FSplitTxB, FController.FTransmitting, True);
end; end;
+41 -5
View File
@@ -237,7 +237,8 @@ type
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
// ---- Аудио / TX ---- // ---- Аудио / TX ----
FVolume: Integer; FVolume: Integer; // громкость RX
FTxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off, DUP)
FMuted: Boolean; FMuted: Boolean;
FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины) FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины)
FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte) FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte)
@@ -495,6 +496,15 @@ type
// Громкость / mute / drive / аттенюатор // Громкость / mute / drive / аттенюатор
procedure SetVolume(V: Integer); procedure SetVolume(V: Integer);
procedure VolumeBy(Delta: Integer); procedure VolumeBy(Delta: Integer);
// True — сейчас звучит self-monitor даунлинка на передаче (TX + DUP + RX MUTE
// off). В этом контексте SetVolume/слайдер правят FTxMonVolume, иначе FVolume.
function MonitoringTX: Boolean;
// Активная громкость под текущий контекст (RX или TX-монитор) — то, что видит
// слайдер (rfVolume) и получает движок.
function ActiveVolume: Integer;
// Применить активную громкость к движку + синхронизировать слайдер (rfVolume).
// Зовётся на T/R-переходах и смене RX MUTE / DUP.
procedure ApplyActiveVolume;
procedure SetMute(On_: Boolean); procedure SetMute(On_: Boolean);
procedure ToggleMute; procedure ToggleMute;
procedure SetDrive(V: Integer); procedure SetDrive(V: Integer);
@@ -694,7 +704,7 @@ begin
FAGCMode := 1; FAGCTop := 90; FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX) FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0; FVolume := 70; FTxMonVolume := 40; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000; FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FNextSliceId := 1; // мультислайсы: генератор id FNextSliceId := 1; // мультислайсы: генератор id
FillChar(FSlices, SizeOf(FSlices), 0); FillChar(FSlices, SizeOf(FSlices), 0);
@@ -1881,6 +1891,7 @@ begin
Result := FLoadedGlobal; // несёт не-радио поля (тема/FPS/digits/CAT) round-trip Result := FLoadedGlobal; // несёт не-радио поля (тема/FPS/digits/CAT) round-trip
Result.Volume := FVolume; Result.Volume := FVolume;
Result.TxMonVolume := FTxMonVolume;
Result.DriveLevel := FDrivePercent; Result.DriveLevel := FDrivePercent;
Result.PAMaxPower := FPAMaxPower; Result.PAMaxPower := FPAMaxPower;
Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb; Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb;
@@ -2470,16 +2481,37 @@ end;
procedure TRadioController.RxGainBy(Delta: Integer); procedure TRadioController.RxGainBy(Delta: Integer);
begin SetRxGain(FRxGainDb + Delta); end; begin SetRxGain(FRxGainDb + Delta); end;
function TRadioController.MonitoringTX: Boolean;
begin
// Звук даунлинка на передаче идёт только в full-duplex (DUP) при выключенном
// RX MUTE — тот же гейт, что в движке (FKeepRXDuringTX and FMonitorRXDuringTX).
Result := FTransmitting and FDisplayDuplex and (not FRxMuteOnTx);
end;
function TRadioController.ActiveVolume: Integer;
begin
if MonitoringTX then Result := FTxMonVolume else Result := FVolume;
end;
procedure TRadioController.ApplyActiveVolume;
begin
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetVolume(ActiveVolume / 100.0);
Changed(rfVolume); // слайдер/оверлей ← ActiveVolume
end;
procedure TRadioController.SetVolume(V: Integer); procedure TRadioController.SetVolume(V: Integer);
begin begin
if V < 0 then V := 0 else if V > 100 then V := 100; if V < 0 then V := 0 else if V > 100 then V := 100;
FVolume := V; // Правим ту громкость, что сейчас активна: на TX-мониторе — FTxMonVolume,
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(FVolume / 100.0); // иначе FVolume. Обе персистятся; слайдер редактирует активную.
if MonitoringTX then FTxMonVolume := V else FVolume := V;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(ActiveVolume / 100.0);
Changed(rfVolume); Changed(rfVolume);
end; end;
procedure TRadioController.VolumeBy(Delta: Integer); procedure TRadioController.VolumeBy(Delta: Integer);
begin SetVolume(FVolume + Delta); end; begin SetVolume(ActiveVolume + Delta); end;
procedure TRadioController.SetMute(On_: Boolean); procedure TRadioController.SetMute(On_: Boolean);
begin begin
@@ -2906,6 +2938,7 @@ begin
// но здесь — чтобы тумблер DUP действовал и в момент передачи. // но здесь — чтобы тумблер DUP действовал и в момент передачи.
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.KeepRXDuringTX := FDisplayDuplex; FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
ApplyActiveVolume; // смена DUP на передаче может включить/выключить TX-монитор
Changed(rfDuplex); Changed(rfDuplex);
end; end;
@@ -2921,6 +2954,7 @@ procedure TRadioController.SetRxMuteOnTx(On_: Boolean);
begin begin
FRxMuteOnTx := On_; FRxMuteOnTx := On_;
ApplyRxMuteToEngine; ApplyRxMuteToEngine;
ApplyActiveVolume; // на передаче тумблер монитора меняет активную громкость
Changed(rfRxMuteOnTx); Changed(rfRxMuteOnTx);
end; end;
@@ -3150,6 +3184,7 @@ begin
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX) FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
end; end;
ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме
ApplyActiveVolume; // T/R: переключаем громкость RX ↔ TX-монитор (если monitoring)
Changed(rfTransmitting); Changed(rfTransmitting);
end; end;
@@ -3502,6 +3537,7 @@ begin
if FTXSettings.TXSpecGridStep > 0 then if FTXSettings.TXSpecGridStep > 0 then
FTXSpecGridStep := FTXSettings.TXSpecGridStep; FTXSpecGridStep := FTXSettings.TXSpecGridStep;
FVolume := FLoadedGlobal.Volume; FVolume := FLoadedGlobal.Volume;
FTxMonVolume := EnsureRange(FLoadedGlobal.TxMonVolume, 0, 100);
FActiveVfo := EnsureRange(FLoadedGlobal.ActiveVfo, 0, 1); // битый конфиг не должен дать невалидный VFO FActiveVfo := EnsureRange(FLoadedGlobal.ActiveVfo, 0, 1); // битый конфиг не должен дать невалидный VFO
FPAMaxPower := FLoadedGlobal.PAMaxPower; FPAMaxPower := FLoadedGlobal.PAMaxPower;
FPlutoTxMaxAttDb := EnsureRange(FLoadedGlobal.PlutoTxMaxAttDb, -50.0, 0.0); FPlutoTxMaxAttDb := EnsureRange(FLoadedGlobal.PlutoTxMaxAttDb, -50.0, 0.0);
+4 -2
View File
@@ -219,6 +219,7 @@ type
TGlobalSettings = record TGlobalSettings = record
Volume: Integer; Volume: Integer;
TxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off)
DriveLevel: Integer; DriveLevel: Integer;
ActiveVfo: Integer; ActiveVfo: Integer;
NRMode: Integer; NRMode: Integer;
@@ -424,7 +425,7 @@ class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
var i: Integer; var i: Integer;
begin begin
FillChar(G, SizeOf(G), 0); FillChar(G, SizeOf(G), 0);
G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0; G.Volume := 70; G.TxMonVolume := 40; G.DriveLevel := 50; G.ActiveVfo := 0;
G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False; G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False;
G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5; G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5;
G.LastXvtr := -1; G.LastXvtr := -1;
@@ -623,6 +624,7 @@ begin
GObj := GetGlobalObj(DevObj); GObj := GetGlobalObj(DevObj);
G.Volume := JI(GObj,'volume',70); G.Volume := JI(GObj,'volume',70);
G.TxMonVolume := JI(GObj,'tx_mon_volume',40);
G.DriveLevel := JI(GObj,'drive_level',50); G.DriveLevel := JI(GObj,'drive_level',50);
G.ActiveVfo := JI(GObj,'active_vfo',0); G.ActiveVfo := JI(GObj,'active_vfo',0);
G.NRMode := JI(GObj,'nr_mode',Ord(JB(GObj,'nr_enabled',False))); G.NRMode := JI(GObj,'nr_mode',Ord(JB(GObj,'nr_enabled',False)));
@@ -759,7 +761,7 @@ procedure TSettingsManager.SaveGlobal(const MAC: array of Byte;
var O: TJSONObject; i: Integer; var O: TJSONObject; i: Integer;
begin begin
O := GetGlobalObj(GetDevObj(MacToStr(MAC))); O := GetGlobalObj(GetDevObj(MacToStr(MAC)));
JW(O,'volume',G.Volume); JW(O,'drive_level',G.DriveLevel); JW(O,'volume',G.Volume); JW(O,'tx_mon_volume',G.TxMonVolume); JW(O,'drive_level',G.DriveLevel);
JW(O,'active_vfo',G.ActiveVfo); JW(O,'active_vfo',G.ActiveVfo);
JW(O,'nr_mode',G.NRMode); JW(O,'nb_mode',G.NBMode); JW(O,'nr_mode',G.NRMode); JW(O,'nb_mode',G.NBMode);
JW(O,'nr_enabled',G.NRMode <> 0); JW(O,'nb_enabled',G.NBMode <> 0); JW(O,'nr_enabled',G.NRMode <> 0); JW(O,'nb_enabled',G.NBMode <> 0);
+1 -1
View File
@@ -563,7 +563,7 @@ begin
FController.FWaterfallBuf, FController.FWaterfallBuf,
FController.FLastSMeter, FController.FLastSMeter,
FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop, FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop,
ViewSpan, FController.FVolume, ViewSpan, FController.ActiveVolume,
FController.FWfAGCEnabled, FController.FWfNFEnabled, FController.FWfAGCEnabled, FController.FWfNFEnabled,
FController.FCurrentBand, FController.FCurrentBand,
FController.FRunning and FController.FNetwork.Connected, FController.FRunning and FController.FNetwork.Connected,