mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(slices): per-pan tuning wheel and per-slice RX mute on TX
Mouse wheel over an extra panadapter now retunes that pan's primary slice (first created on that receiver) instead of the main VFO; a slice flag under the cursor still wins, pan 0 keeps driving the VFO. Slice flags get a clickable RXM badge: RX audio of the slice is muted while transmitting (default), so the operator no longer hears himself on every slice in full duplex. State persists with the workspace. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -579,6 +579,7 @@ type
|
|||||||
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
procedure OnVfoOverlaySplit(Sender: TObject);
|
procedure OnVfoOverlaySplit(Sender: TObject);
|
||||||
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
||||||
|
procedure OnVfoOverlayRxMuteTx(Sender: TObject); // RXM — приём слайса на TX
|
||||||
procedure RefreshTxIndicators; // TX-бейджи всех флагов
|
procedure RefreshTxIndicators; // TX-бейджи всех флагов
|
||||||
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
|
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
|
||||||
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
|
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
|
||||||
@@ -5457,6 +5458,17 @@ begin
|
|||||||
FController.SetTxSlice(CurSliceId); // → Changed(rfTransmitting) → RefreshTxIndicators
|
FController.SetTxSlice(CurSliceId); // → Changed(rfTransmitting) → RefreshTxIndicators
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnVfoOverlayRxMuteTx(Sender: TObject);
|
||||||
|
// Бейдж RXM на флаге слайса: слышим ли этот слайс, пока сами передаём.
|
||||||
|
// У главного приёмника то же делает кнопка RX MUTE на левой панели.
|
||||||
|
var S: TCtrlSlice;
|
||||||
|
begin
|
||||||
|
if CurSliceId = 0 then Exit; // у флага A бейджа RXM нет (страховка)
|
||||||
|
if not FController.GetSlice(CurSliceId, S) then Exit;
|
||||||
|
FController.SetSliceRxMuteOnTx(CurSliceId, not S.RxMuteOnTx);
|
||||||
|
PushFlagStateAllPans(CurSliceId);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.RefreshTxIndicators;
|
procedure TMainForm.RefreshTxIndicators;
|
||||||
// Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены
|
// Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены
|
||||||
// TX-слайса / состояния передачи.
|
// TX-слайса / состояния передачи.
|
||||||
@@ -5539,6 +5551,7 @@ begin
|
|||||||
O.OnAudioDevice := OnVfoOverlayAudioDevice;
|
O.OnAudioDevice := OnVfoOverlayAudioDevice;
|
||||||
O.OnSplit := OnVfoOverlaySplit;
|
O.OnSplit := OnVfoOverlaySplit;
|
||||||
O.OnTxSelect := OnVfoOverlayTxSelect;
|
O.OnTxSelect := OnVfoOverlayTxSelect;
|
||||||
|
O.OnRxMuteTx := OnVfoOverlayRxMuteTx;
|
||||||
O.OnSliceSelect := OnVfoOverlaySliceSelect;
|
O.OnSliceSelect := OnVfoOverlaySliceSelect;
|
||||||
O.OnClose := OnVfoOverlayClose;
|
O.OnClose := OnVfoOverlayClose;
|
||||||
O.OnInvalidate := OnVfoOverlayInvalidate;
|
O.OnInvalidate := OnVfoOverlayInvalidate;
|
||||||
@@ -6477,6 +6490,7 @@ begin
|
|||||||
Cfg.Pans[i].Slices[n].AGC := Ord(FController.FSlices[s].AGC);
|
Cfg.Pans[i].Slices[n].AGC := Ord(FController.FSlices[s].AGC);
|
||||||
Cfg.Pans[i].Slices[n].Volume := FController.FSlices[s].Volume;
|
Cfg.Pans[i].Slices[n].Volume := FController.FSlices[s].Volume;
|
||||||
Cfg.Pans[i].Slices[n].Muted := FController.FSlices[s].Muted;
|
Cfg.Pans[i].Slices[n].Muted := FController.FSlices[s].Muted;
|
||||||
|
Cfg.Pans[i].Slices[n].RxMuteOnTx := FController.FSlices[s].RxMuteOnTx;
|
||||||
Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn;
|
Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn;
|
||||||
Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel;
|
Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel;
|
||||||
Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName;
|
Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName;
|
||||||
@@ -6501,6 +6515,7 @@ begin
|
|||||||
EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId);
|
EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId);
|
||||||
if Id < 0 then Exit;
|
if Id < 0 then Exit;
|
||||||
if SC.Muted then FController.SetSliceMute(Id, True);
|
if SC.Muted then FController.SetSliceMute(Id, True);
|
||||||
|
FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx);
|
||||||
if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel);
|
if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel);
|
||||||
P.CreateSliceFlag(Id);
|
P.CreateSliceFlag(Id);
|
||||||
FActiveSliceId := Id;
|
FActiveSliceId := Id;
|
||||||
@@ -7604,6 +7619,16 @@ begin
|
|||||||
SliceId := FPans[i].SliceUnderCursor;
|
SliceId := FPans[i].SliceUnderCursor;
|
||||||
if SliceId <> 0 then begin SlicePan := FPans[i]; Break; end;
|
if SliceId <> 0 then begin SlicePan := FPans[i]; Break; end;
|
||||||
end;
|
end;
|
||||||
|
// Иначе колесо над спектром/водопадом ДОП. пана крутит его главный слайс
|
||||||
|
// (первый созданный на этом приёмнике), а не общий VFO. Пан 0 — общий VFO.
|
||||||
|
if SliceId = 0 then
|
||||||
|
for i := 1 to MAX_PANS - 1 do
|
||||||
|
if (FPans[i] <> nil) and FPans[i].CursorOverDisplay then
|
||||||
|
begin
|
||||||
|
SliceId := FController.PanPrimarySliceId(i);
|
||||||
|
if SliceId <> 0 then SlicePan := FPans[i];
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
||||||
begin
|
begin
|
||||||
if (Sl.Mode = MODE_DMR) and
|
if (Sl.Mode = MODE_DMR) and
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ type
|
|||||||
function SliceAtPixel(X: Integer): Integer;
|
function SliceAtPixel(X: Integer): Integer;
|
||||||
// SliceId флага под курсором мыши (прямоугольник оверлея; 0 = нет).
|
// SliceId флага под курсором мыши (прямоугольник оверлея; 0 = нет).
|
||||||
function SliceUnderCursor: Integer;
|
function SliceUnderCursor: Integer;
|
||||||
|
function CursorOverDisplay: Boolean; // курсор над спектром/водопадом пана
|
||||||
// Единый packing всех флагов (A+B+) в один ряд, без наездов.
|
// Единый packing всех флагов (A+B+) в один ряд, без наездов.
|
||||||
procedure LayoutFlags;
|
procedure LayoutFlags;
|
||||||
// Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+.
|
// Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+.
|
||||||
@@ -1142,6 +1143,22 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Курсор над спектром/водопадом этого пана? (колесо крутит «его» частоту)
|
||||||
|
function TPanafallPanel.CursorOverDisplay: Boolean;
|
||||||
|
|
||||||
|
function Over(C: TControl): Boolean;
|
||||||
|
var P: TPoint;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if (C = nil) or (not C.Visible) then Exit;
|
||||||
|
P := C.ScreenToClient(Mouse.CursorPos);
|
||||||
|
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
|
||||||
|
end;
|
||||||
|
|
||||||
|
begin
|
||||||
|
Result := Over(FPbSpectrum) or Over(FPbWaterfall);
|
||||||
|
end;
|
||||||
|
|
||||||
// Флаг слайса под курсором мыши (в клиентских координатах спектра)?
|
// Флаг слайса под курсором мыши (в клиентских координатах спектра)?
|
||||||
function TPanafallPanel.SliceUnderCursor: Integer;
|
function TPanafallPanel.SliceUnderCursor: Integer;
|
||||||
var P: TPoint; i: Integer; O: TVfoOverlay;
|
var P: TPoint; i: Integer; O: TVfoOverlay;
|
||||||
@@ -1321,6 +1338,7 @@ begin
|
|||||||
O.SetExtState(Round(S.Volume * 100), S.Muted, False,
|
O.SetExtState(Round(S.Volume * 100), S.Muted, False,
|
||||||
FController.FTransmitting and (FController.FTxSliceId = SliceId),
|
FController.FTransmitting and (FController.FTxSliceId = SliceId),
|
||||||
FController.FTxSliceId = SliceId);
|
FController.FTxSliceId = SliceId);
|
||||||
|
O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM
|
||||||
// Список аудио-устройств (общий PA-список) + текущее устройство слайса.
|
// Список аудио-устройств (общий PA-список) + текущее устройство слайса.
|
||||||
if Assigned(FController.FAudioOut) then
|
if Assigned(FController.FAudioOut) then
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ type
|
|||||||
AGC: TWDSPAGCMode;
|
AGC: TWDSPAGCMode;
|
||||||
Volume: Double; // 0..1
|
Volume: Double; // 0..1
|
||||||
Muted: Boolean;
|
Muted: Boolean;
|
||||||
|
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
|
||||||
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
|
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
|
||||||
FMSQLevel: Integer; // порог squelch 0..100
|
FMSQLevel: Integer; // порог squelch 0..100
|
||||||
DeviceIndex: Integer; // PortAudio-индекс (-1 = default)
|
DeviceIndex: Integer; // PortAudio-индекс (-1 = default)
|
||||||
@@ -187,6 +188,7 @@ type
|
|||||||
const Left, Right: array of Single; Count: Integer);
|
const Left, Right: array of Single; Count: Integer);
|
||||||
procedure OnSliceDMRAudioReady(SliceId: Integer;
|
procedure OnSliceDMRAudioReady(SliceId: Integer;
|
||||||
const PCM: array of Single; Count: Integer);
|
const PCM: array of Single; Count: Integer);
|
||||||
|
function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче?
|
||||||
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
|
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
|
||||||
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
|
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
|
||||||
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
|
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
|
||||||
@@ -592,6 +594,8 @@ type
|
|||||||
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||||||
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
|
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
|
||||||
procedure SetSliceMute(Id: Integer; Mute: Boolean);
|
procedure SetSliceMute(Id: Integer; Mute: Boolean);
|
||||||
|
procedure SetSliceRxMuteOnTx(Id: Integer; On_: Boolean); // self-monitor слайса на TX
|
||||||
|
function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана
|
||||||
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||||
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
|
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
|
||||||
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
|
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
|
||||||
@@ -1334,10 +1338,19 @@ begin
|
|||||||
idx := FindSliceIndex(SliceId);
|
idx := FindSliceIndex(SliceId);
|
||||||
if idx < 0 then Exit;
|
if idx < 0 then Exit;
|
||||||
if FSlices[idx].Mode = MODE_DMR then Exit;
|
if FSlices[idx].Mode = MODE_DMR then Exit;
|
||||||
|
if SliceMutedByTx(idx) then Exit;
|
||||||
if FSlices[idx].Audio <> nil then
|
if FSlices[idx].Audio <> nil then
|
||||||
FSlices[idx].Audio.Write(Left, Right, Count);
|
FSlices[idx].Audio.Write(Left, Right, Count);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Слайс глушится на передаче? В full-duplex (Pluto/QO-100) RX-тракт живёт и во
|
||||||
|
// время TX, поэтому иначе оператор слышит сам себя на КАЖДОМ слайсе. У главного
|
||||||
|
// приёмника за это отвечает FRxMuteOnTx (кнопка RX MUTE), у слайса — свой флаг.
|
||||||
|
function TRadioController.SliceMutedByTx(Idx: Integer): Boolean;
|
||||||
|
begin
|
||||||
|
Result := FTransmitting and FSlices[Idx].RxMuteOnTx;
|
||||||
|
end;
|
||||||
|
|
||||||
// DSP thread: flat, volume-independent discriminator of the corresponding
|
// DSP thread: flat, volume-independent discriminator of the corresponding
|
||||||
// WDSP slice channel. FeedAudio only copies it into that decoder's ring.
|
// WDSP slice channel. FeedAudio only copies it into that decoder's ring.
|
||||||
procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
|
procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
|
||||||
@@ -1362,6 +1375,7 @@ begin
|
|||||||
idx := FindSliceIndex(SliceId);
|
idx := FindSliceIndex(SliceId);
|
||||||
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or
|
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or
|
||||||
FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit;
|
FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit;
|
||||||
|
if SliceMutedByTx(idx) then Exit;
|
||||||
N := Min(Count, Length(PCM));
|
N := Min(Count, Length(PCM));
|
||||||
SetLength(Left, N * 6);
|
SetLength(Left, N * 6);
|
||||||
SetLength(Right, N * 6);
|
SetLength(Right, N * 6);
|
||||||
@@ -1783,6 +1797,7 @@ begin
|
|||||||
FSlices[slot].AGC := AGC;
|
FSlices[slot].AGC := AGC;
|
||||||
FSlices[slot].Volume := Vol;
|
FSlices[slot].Volume := Vol;
|
||||||
FSlices[slot].Muted := False;
|
FSlices[slot].Muted := False;
|
||||||
|
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
|
||||||
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
|
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
|
||||||
FSlices[slot].FMSQLevel := 40;
|
FSlices[slot].FMSQLevel := 40;
|
||||||
FSlices[slot].DeviceIndex := DevIndex;
|
FSlices[slot].DeviceIndex := DevIndex;
|
||||||
@@ -1878,6 +1893,26 @@ begin
|
|||||||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute);
|
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.SetSliceRxMuteOnTx(Id: Integer; On_: Boolean);
|
||||||
|
var idx: Integer;
|
||||||
|
begin
|
||||||
|
idx := FindSliceIndex(Id);
|
||||||
|
if idx < 0 then Exit;
|
||||||
|
FSlices[idx].RxMuteOnTx := On_;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// «Главный» слайс пана = созданный на нём первым. Id монотонно растёт
|
||||||
|
// (FNextSliceId), поэтому первый = слайс с минимальным Id. 0 = слайсов нет.
|
||||||
|
function TRadioController.PanPrimarySliceId(PanId: Integer): Integer;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
for i := 0 to MAX_SLICES - 1 do
|
||||||
|
if FSlices[i].Active and (FSlices[i].PanId = PanId) and
|
||||||
|
((Result = 0) or (FSlices[i].Id < Result)) then
|
||||||
|
Result := FSlices[i].Id;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer;
|
procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer;
|
||||||
SNBOn, ANFOn: Boolean);
|
SNBOn, ANFOn: Boolean);
|
||||||
var idx: Integer;
|
var idx: Integer;
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ type
|
|||||||
AGC: Integer; // Ord(TWDSPAGCMode)
|
AGC: Integer; // Ord(TWDSPAGCMode)
|
||||||
Volume: Double; // 0..1
|
Volume: Double; // 0..1
|
||||||
Muted: Boolean;
|
Muted: Boolean;
|
||||||
|
RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл)
|
||||||
FMSQOn: Boolean;
|
FMSQOn: Boolean;
|
||||||
FMSQLevel: Integer;
|
FMSQLevel: Integer;
|
||||||
DevName: string; // имя PortAudio-устройства ('' = default)
|
DevName: string; // имя PortAudio-устройства ('' = default)
|
||||||
@@ -1783,6 +1784,7 @@ begin
|
|||||||
SliceObj.Add('agc', P.Pans[i].Slices[j].AGC);
|
SliceObj.Add('agc', P.Pans[i].Slices[j].AGC);
|
||||||
SliceObj.Add('volume', P.Pans[i].Slices[j].Volume);
|
SliceObj.Add('volume', P.Pans[i].Slices[j].Volume);
|
||||||
SliceObj.Add('muted', P.Pans[i].Slices[j].Muted);
|
SliceObj.Add('muted', P.Pans[i].Slices[j].Muted);
|
||||||
|
SliceObj.Add('rx_mute_on_tx', P.Pans[i].Slices[j].RxMuteOnTx);
|
||||||
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
|
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
|
||||||
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
|
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
|
||||||
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
|
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
|
||||||
@@ -1860,6 +1862,7 @@ begin
|
|||||||
P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1);
|
P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1);
|
||||||
P.Pans[id].Slices[n].Volume := EnsureRange(JD(SliceObj, 'volume', 0.7), 0.0, 1.0);
|
P.Pans[id].Slices[n].Volume := EnsureRange(JD(SliceObj, 'volume', 0.7), 0.0, 1.0);
|
||||||
P.Pans[id].Slices[n].Muted := JB(SliceObj, 'muted', False);
|
P.Pans[id].Slices[n].Muted := JB(SliceObj, 'muted', False);
|
||||||
|
P.Pans[id].Slices[n].RxMuteOnTx := JB(SliceObj, 'rx_mute_on_tx', True);
|
||||||
P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False);
|
P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False);
|
||||||
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
|
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
|
||||||
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
|
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
|
||||||
|
|||||||
+31
-8
@@ -77,6 +77,7 @@ type
|
|||||||
FSplit: Boolean;
|
FSplit: Boolean;
|
||||||
FTx: Boolean; // передача идёт (красный)
|
FTx: Boolean; // передача идёт (красный)
|
||||||
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
|
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
|
||||||
|
FRxMuteTx: Boolean; // RXM: приём слайса глушится на передаче (умолч. True)
|
||||||
FSlice: Char; // буква слайса ('A')
|
FSlice: Char; // буква слайса ('A')
|
||||||
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
|
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
|
||||||
FDMRText: string; // стабильный краткий статус рядом с полосой
|
FDMRText: string; // стабильный краткий статус рядом с полосой
|
||||||
@@ -105,6 +106,7 @@ type
|
|||||||
FOnAudioDevice: TAudioDeviceEvent;
|
FOnAudioDevice: TAudioDeviceEvent;
|
||||||
FOnSplit: TNotifyEvent;
|
FOnSplit: TNotifyEvent;
|
||||||
FOnTxSelect: TNotifyEvent;
|
FOnTxSelect: TNotifyEvent;
|
||||||
|
FOnRxMuteTx: TNotifyEvent;
|
||||||
FOnInvalidate: TNotifyEvent;
|
FOnInvalidate: TNotifyEvent;
|
||||||
|
|
||||||
FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.)
|
FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.)
|
||||||
@@ -183,6 +185,7 @@ type
|
|||||||
AAGCMode: Integer);
|
AAGCMode: Integer);
|
||||||
procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
|
procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
|
||||||
procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean);
|
procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean);
|
||||||
|
procedure SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX)
|
||||||
procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
|
procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
|
||||||
const CurName: string);
|
const CurName: string);
|
||||||
procedure UpdateSMeter(DB: Double);
|
procedure UpdateSMeter(DB: Double);
|
||||||
@@ -218,6 +221,7 @@ type
|
|||||||
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
||||||
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
||||||
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
||||||
|
property OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx;
|
||||||
property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect;
|
property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect;
|
||||||
property OnClose: TNotifyEvent read FOnClose write FOnClose;
|
property OnClose: TNotifyEvent read FOnClose write FOnClose;
|
||||||
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
|
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
|
||||||
@@ -258,9 +262,10 @@ const
|
|||||||
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
|
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
|
||||||
HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели)
|
HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели)
|
||||||
HIT_SQL_SLIDER = -32; // трек порога FM-squelch
|
HIT_SQL_SLIDER = -32; // трек порога FM-squelch
|
||||||
|
HIT_RXMUTE = -33; // бейдж RXM: приём слайса на своей передаче
|
||||||
HIT_AGC_PICK_BASE = 100000;
|
HIT_AGC_PICK_BASE = 100000;
|
||||||
HIT_DEV_BASE = 200000;
|
HIT_DEV_BASE = 200000;
|
||||||
// Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-32) и с базами выше.
|
// Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-33) и с базами выше.
|
||||||
HIT_MODE_BASE = -1000; // -1000 .. -1000-(OVL_MODE_COUNT-1)
|
HIT_MODE_BASE = -1000; // -1000 .. -1000-(OVL_MODE_COUNT-1)
|
||||||
HIT_FILT_BASE = 1000; // 1000 + индекс кнопки в ряду фильтров
|
HIT_FILT_BASE = 1000; // 1000 + индекс кнопки в ряду фильтров
|
||||||
|
|
||||||
@@ -455,7 +460,7 @@ begin
|
|||||||
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
|
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
|
||||||
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
|
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
|
||||||
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
|
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
|
||||||
FTxSel := True; FSlice := 'A';
|
FTxSel := True; FSlice := 'A'; FRxMuteTx := True;
|
||||||
FDMRText := ''; FDMRSynced := False;
|
FDMRText := ''; FDMRSynced := False;
|
||||||
FDMRCandidateText := ''; FDMRCandidateSynced := False;
|
FDMRCandidateText := ''; FDMRCandidateSynced := False;
|
||||||
FDMRCandidateSince := 0;
|
FDMRCandidateSince := 0;
|
||||||
@@ -987,7 +992,7 @@ end;
|
|||||||
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
|
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
|
||||||
var
|
var
|
||||||
FreqStr, SStr, BWStr, DMRStr: string;
|
FreqStr, SStr, BWStr, DMRStr: string;
|
||||||
CurY, X, RW, i, DMRX, DMRRight: Integer;
|
CurY, X, RW, i, DMRX, DMRRight, TxRight: Integer;
|
||||||
R: TRect;
|
R: TRect;
|
||||||
BarL, BarW, MeterX: Integer;
|
BarL, BarW, MeterX: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -1005,16 +1010,25 @@ begin
|
|||||||
RW := W - PAD*2;
|
RW := W - PAD*2;
|
||||||
CurY := PAD;
|
CurY := PAD;
|
||||||
|
|
||||||
{ --- Шапка: главный [A] BW ... SPLIT [TX]; слайс [B] BW ... [×] [TX] --- }
|
{ --- Шапка: главный [A] BW ... SPLIT [TX]; слайс [B] BW ... [RXM] [TX] [×] --- }
|
||||||
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_BADGE_TXT);
|
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_BADGE_TXT);
|
||||||
RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
|
RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
|
||||||
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
|
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
|
||||||
// Место освободилось от SPLIT (его у слайсов нет) — ставим вплотную к TX.
|
// Она крайняя справа, TX и RXM выстраиваются левее неё.
|
||||||
|
TxRight := W - PAD;
|
||||||
if not FIsMain then
|
if not FIsMain then
|
||||||
begin
|
begin
|
||||||
R := Rect(W - PAD - 26 - 6 - 14, CurY, W - PAD - 26 - 6, CurY + HDR_BADGE_H);
|
R := Rect(W - PAD - 14, CurY, W - PAD, CurY + HDR_BADGE_H);
|
||||||
DrawBadge(C, R, #$C3#$97, CLR_BTN_NORM, CLR_DIM); // UTF-8 '×'
|
DrawBadge(C, R, #$C3#$97, CLR_BTN_NORM, CLR_DIM); // UTF-8 '×'
|
||||||
RegisterHit(R, HIT_CLOSE);
|
RegisterHit(R, HIT_CLOSE);
|
||||||
|
TxRight := W - PAD - 14 - 6;
|
||||||
|
// RXM — приём слайса на СВОЕЙ передаче. Подсвечен (умолч.) = глушим, т.е.
|
||||||
|
// себя не слышим; погашен = self-monitor (свой downlink через транспондер).
|
||||||
|
// У главного приёмника то же самое делает кнопка RX MUTE на левой панели.
|
||||||
|
R := Rect(TxRight - 26 - 4 - 32, CurY, TxRight - 26 - 4, CurY + HDR_BADGE_H);
|
||||||
|
if FRxMuteTx then DrawBadge(C, R, 'RXM', CLR_ACCENT, CLR_BADGE_TXT)
|
||||||
|
else DrawBadge(C, R, 'RXM', CLR_BTN_NORM, CLR_DIM);
|
||||||
|
RegisterHit(R, HIT_RXMUTE);
|
||||||
end;
|
end;
|
||||||
// ширина фильтра
|
// ширина фильтра
|
||||||
if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0])
|
if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0])
|
||||||
@@ -1029,7 +1043,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
DMRStr := FDMRText;
|
DMRStr := FDMRText;
|
||||||
DMRX := X + 22 + C.TextWidth(BWStr) + 8;
|
DMRX := X + 22 + C.TextWidth(BWStr) + 8;
|
||||||
DMRRight := W - PAD - 26 - 6 - 14 - 5; // до кнопок × и TX
|
DMRRight := TxRight - 26 - 4 - 32 - 5; // до бейджей RXM, TX и ×
|
||||||
while (Length(DMRStr) > 1) and
|
while (Length(DMRStr) > 1) and
|
||||||
(C.TextWidth(DMRStr) > DMRRight - DMRX) do
|
(C.TextWidth(DMRStr) > DMRRight - DMRX) do
|
||||||
SetLength(DMRStr, Length(DMRStr) - 1);
|
SetLength(DMRStr, Length(DMRStr) - 1);
|
||||||
@@ -1038,7 +1052,7 @@ begin
|
|||||||
C.TextOut(DMRX, CurY + 1, DMRStr);
|
C.TextOut(DMRX, CurY + 1, DMRStr);
|
||||||
end;
|
end;
|
||||||
// TX badge: a DMR slice is visibly disabled and has no hit target.
|
// TX badge: a DMR slice is visibly disabled and has no hit target.
|
||||||
R := Rect(W - PAD - 26, CurY, W - PAD, CurY + HDR_BADGE_H);
|
R := Rect(TxRight - 26, CurY, TxRight, CurY + HDR_BADGE_H);
|
||||||
if not TxSelectionAllowed then
|
if not TxSelectionAllowed then
|
||||||
DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM)
|
DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM)
|
||||||
else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite)
|
else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite)
|
||||||
@@ -1205,6 +1219,8 @@ begin
|
|||||||
FOnTxSelect(Self);
|
FOnTxSelect(Self);
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
HIT_RXMUTE: begin FRxMuteTx := not FRxMuteTx; RequestInvalidate;
|
||||||
|
if Assigned(FOnRxMuteTx) then FOnRxMuteTx(Self); Exit; end;
|
||||||
HIT_SLICE_SEL: begin if Assigned(FOnSliceSelect) then FOnSliceSelect(Self); Exit; end;
|
HIT_SLICE_SEL: begin if Assigned(FOnSliceSelect) then FOnSliceSelect(Self); Exit; end;
|
||||||
HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end;
|
HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end;
|
||||||
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
|
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
|
||||||
@@ -1487,6 +1503,13 @@ begin
|
|||||||
RequestInvalidate;
|
RequestInvalidate;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TVfoOverlay.SetRxMuteTxState(AOn: Boolean);
|
||||||
|
begin
|
||||||
|
if FRxMuteTx = AOn then Exit;
|
||||||
|
FRxMuteTx := AOn;
|
||||||
|
RequestInvalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
|
procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
|
||||||
begin
|
begin
|
||||||
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;
|
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;
|
||||||
|
|||||||
Reference in New Issue
Block a user