diff --git a/MainForm.pas b/MainForm.pas index 3194020..009dd21 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -579,6 +579,7 @@ type procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string); procedure OnVfoOverlaySplit(Sender: TObject); procedure OnVfoOverlayTxSelect(Sender: TObject); + procedure OnVfoOverlayRxMuteTx(Sender: TObject); // RXM — приём слайса на TX procedure RefreshTxIndicators; // TX-бейджи всех флагов procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс @@ -5457,6 +5458,17 @@ begin FController.SetTxSlice(CurSliceId); // → Changed(rfTransmitting) → RefreshTxIndicators 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; // Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены // TX-слайса / состояния передачи. @@ -5539,6 +5551,7 @@ begin O.OnAudioDevice := OnVfoOverlayAudioDevice; O.OnSplit := OnVfoOverlaySplit; O.OnTxSelect := OnVfoOverlayTxSelect; + O.OnRxMuteTx := OnVfoOverlayRxMuteTx; O.OnSliceSelect := OnVfoOverlaySliceSelect; O.OnClose := OnVfoOverlayClose; O.OnInvalidate := OnVfoOverlayInvalidate; @@ -6477,6 +6490,7 @@ begin 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].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].FMSQLevel := FController.FSlices[s].FMSQLevel; 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); if Id < 0 then Exit; if SC.Muted then FController.SetSliceMute(Id, True); + FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx); if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel); P.CreateSliceFlag(Id); FActiveSliceId := Id; @@ -7604,6 +7619,16 @@ begin SliceId := FPans[i].SliceUnderCursor; if SliceId <> 0 then begin SlicePan := FPans[i]; Break; 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 begin if (Sl.Mode = MODE_DMR) and diff --git a/PanafallPanel.pas b/PanafallPanel.pas index 74c4bc5..c2c5280 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -275,6 +275,7 @@ type function SliceAtPixel(X: Integer): Integer; // SliceId флага под курсором мыши (прямоугольник оверлея; 0 = нет). function SliceUnderCursor: Integer; + function CursorOverDisplay: Boolean; // курсор над спектром/водопадом пана // Единый packing всех флагов (A+B+) в один ряд, без наездов. procedure LayoutFlags; // Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+. @@ -1142,6 +1143,22 @@ begin 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; var P: TPoint; i: Integer; O: TVfoOverlay; @@ -1321,6 +1338,7 @@ begin O.SetExtState(Round(S.Volume * 100), S.Muted, False, FController.FTransmitting and (FController.FTxSliceId = SliceId), FController.FTxSliceId = SliceId); + O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM // Список аудио-устройств (общий PA-список) + текущее устройство слайса. if Assigned(FController.FAudioOut) then begin diff --git a/RadioController.pas b/RadioController.pas index f7d64d7..38c5a59 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -158,6 +158,7 @@ type AGC: TWDSPAGCMode; Volume: Double; // 0..1 Muted: Boolean; + RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM) FMSQLevel: Integer; // порог squelch 0..100 DeviceIndex: Integer; // PortAudio-индекс (-1 = default) @@ -187,6 +188,7 @@ type const Left, Right: array of Single; Count: Integer); procedure OnSliceDMRAudioReady(SliceId: Integer; const PCM: array of Single; Count: Integer); + function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче? procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра // --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) --- procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст @@ -592,6 +594,8 @@ type procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode); procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1 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 SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer); function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока @@ -1334,10 +1338,19 @@ begin idx := FindSliceIndex(SliceId); if idx < 0 then Exit; if FSlices[idx].Mode = MODE_DMR then Exit; + if SliceMutedByTx(idx) then Exit; if FSlices[idx].Audio <> nil then FSlices[idx].Audio.Write(Left, Right, Count); 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 // WDSP slice channel. FeedAudio only copies it into that decoder's ring. procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer; @@ -1362,6 +1375,7 @@ begin idx := FindSliceIndex(SliceId); if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit; + if SliceMutedByTx(idx) then Exit; N := Min(Count, Length(PCM)); SetLength(Left, N * 6); SetLength(Right, N * 6); @@ -1783,6 +1797,7 @@ begin FSlices[slot].AGC := AGC; FSlices[slot].Volume := Vol; FSlices[slot].Muted := False; + FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен FSlices[slot].FMSQLevel := 40; FSlices[slot].DeviceIndex := DevIndex; @@ -1878,6 +1893,26 @@ begin if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute); 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; SNBOn, ANFOn: Boolean); var idx: Integer; diff --git a/Settings.pas b/Settings.pas index 94c790f..6de595a 100644 --- a/Settings.pas +++ b/Settings.pas @@ -104,6 +104,7 @@ type AGC: Integer; // Ord(TWDSPAGCMode) Volume: Double; // 0..1 Muted: Boolean; + RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл) FMSQOn: Boolean; FMSQLevel: Integer; DevName: string; // имя PortAudio-устройства ('' = default) @@ -1783,6 +1784,7 @@ begin SliceObj.Add('agc', P.Pans[i].Slices[j].AGC); SliceObj.Add('volume', P.Pans[i].Slices[j].Volume); 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_level', P.Pans[i].Slices[j].FMSQLevel); 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].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].RxMuteOnTx := JB(SliceObj, 'rx_mute_on_tx', True); 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].DevName := JS(SliceObj, 'dev_name', ''); diff --git a/VfoOverlay.pas b/VfoOverlay.pas index bb82b0e..5177417 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -77,6 +77,7 @@ type FSplit: Boolean; FTx: Boolean; // передача идёт (красный) FTxSel: Boolean; // этот слайс выбран для передачи (янтарный) + FRxMuteTx: Boolean; // RXM: приём слайса глушится на передаче (умолч. True) FSlice: Char; // буква слайса ('A') FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки FDMRText: string; // стабильный краткий статус рядом с полосой @@ -105,6 +106,7 @@ type FOnAudioDevice: TAudioDeviceEvent; FOnSplit: TNotifyEvent; FOnTxSelect: TNotifyEvent; + FOnRxMuteTx: TNotifyEvent; FOnInvalidate: TNotifyEvent; FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.) @@ -183,6 +185,7 @@ type AAGCMode: Integer); procedure SetSquelchState(AOn: Boolean; ALevel: Integer); 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; const CurName: string); procedure UpdateSMeter(DB: Double); @@ -218,6 +221,7 @@ type property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice; property OnSplit: TNotifyEvent read FOnSplit write FOnSplit; property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect; + property OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx; property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect; property OnClose: TNotifyEvent read FOnClose write FOnClose; property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate; @@ -258,9 +262,10 @@ const HIT_CLOSE = -30; // × закрытия слайса (неглавные) HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели) HIT_SQL_SLIDER = -32; // трек порога FM-squelch + HIT_RXMUTE = -33; // бейдж RXM: приём слайса на своей передаче HIT_AGC_PICK_BASE = 100000; HIT_DEV_BASE = 200000; - // Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-32) и с базами выше. + // Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-33) и с базами выше. HIT_MODE_BASE = -1000; // -1000 .. -1000-(OVL_MODE_COUNT-1) HIT_FILT_BASE = 1000; // 1000 + индекс кнопки в ряду фильтров @@ -455,7 +460,7 @@ begin FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0; FSQLOn := False; FSQLLevel := 40; FSqlDrag := False; FVolume := 70; FMuted := False; FSplit := False; FTx := False; - FTxSel := True; FSlice := 'A'; + FTxSel := True; FSlice := 'A'; FRxMuteTx := True; FDMRText := ''; FDMRSynced := False; FDMRCandidateText := ''; FDMRCandidateSynced := False; FDMRCandidateSince := 0; @@ -987,7 +992,7 @@ end; procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer); var FreqStr, SStr, BWStr, DMRStr: string; - CurY, X, RW, i, DMRX, DMRRight: Integer; + CurY, X, RW, i, DMRX, DMRRight, TxRight: Integer; R: TRect; BarL, BarW, MeterX: Integer; begin @@ -1005,16 +1010,25 @@ begin RW := W - PAD*2; 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); RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса // Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть). - // Место освободилось от SPLIT (его у слайсов нет) — ставим вплотную к TX. + // Она крайняя справа, TX и RXM выстраиваются левее неё. + TxRight := W - PAD; if not FIsMain then 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 '×' 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; // ширина фильтра if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0]) @@ -1029,7 +1043,7 @@ begin begin DMRStr := FDMRText; 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 (C.TextWidth(DMRStr) > DMRRight - DMRX) do SetLength(DMRStr, Length(DMRStr) - 1); @@ -1038,7 +1052,7 @@ begin C.TextOut(DMRX, CurY + 1, DMRStr); end; // 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 DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM) else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite) @@ -1205,6 +1219,8 @@ begin FOnTxSelect(Self); Exit; 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_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end; HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll); @@ -1487,6 +1503,13 @@ begin RequestInvalidate; 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); begin if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;