mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(slices): multi-slice receivers (FlexRadio-style) — soft fan-out + per-slice flag/audio
Independent RX slices sharing one panadapter: each has its own freq/mode/filter/AGC/NR-NB-SNB-ANF, S-meter, volume and sound card. VFO A = main receiver; B+ are soft fan-out WDSP channels within the captured span (ideal for Pluto/QO-100; HW DDC path is Stage 3). - WDSPEngine: soft fan-out (TSliceChan, ProcessSlices in DSP thread, per-slice NCO/mode/filter/AGC/NR/NB/SNB/ANF + S-meter, OnSliceAudio), FSliceLock serialises add/remove/set vs DSP; reopened on rate change. - RadioController: TCtrlSlice with its own TAudioOutput/device, Add/Remove/SetSlice*, RefreshSliceShifts keeps slices on target Hz across centre changes. Slice letters start at 'B' (A = main receiver). - UI: per-slice flags (VfoOverlay) with letter badge/close, filter-band markers in BOTH GL and CPU renderers, drag-retune, wheel; flags placed beside the filter band with flip, like the main flag. Ctrl+click adds. - Main flag (slice A) visible whenever the left panel is hidden OR any slice exists; synced with panel-driven DSP/mode/AGC changes. - Perf: S-meter decoupled from flag chrome cache (FMeterDirty + meter-only RefreshMeter via EnsureRendered); InvalidateVfoOverlay avoids global overlay-cache churn on meter ticks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+544
-26
@@ -122,7 +122,14 @@ type
|
||||
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
||||
FDeviceDialog: TDeviceDialog;
|
||||
FPendingIP: string; // IP устройства для подключения
|
||||
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
|
||||
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре (слайс A / главный)
|
||||
FSliceFlags: array of TVfoOverlay; // флаги доп. слайсов (B+)
|
||||
FActiveFlag: TVfoOverlay; // флаг, чьё событие сейчас обрабатывается (для роутинга по SliceId)
|
||||
FActiveSliceId: Integer; // выбранный активный слайс (0 = главный)
|
||||
FPendingSliceClose: Integer; // SliceId к удалению после возврата из мыши (0 = нет)
|
||||
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
||||
FSliceDragId: Integer; // какой слайс тащим
|
||||
FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц)
|
||||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||||
FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100 внизу спектра
|
||||
FPanelHidden: Boolean; // True = левая панель скрыта
|
||||
@@ -516,6 +523,7 @@ type
|
||||
// -------------------------------------------------------------------------
|
||||
procedure BtnSettingsClick(Sender: TObject);
|
||||
procedure PositionVfoOverlay;
|
||||
procedure UpdateVfoOverlayVisibility; // флаг A виден: панель скрыта ИЛИ есть слайсы B+
|
||||
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||||
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||
procedure OnVfoOverlayAGCChange(AGCMode: Integer);
|
||||
@@ -524,9 +532,26 @@ type
|
||||
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
||||
procedure OnVfoOverlaySplit(Sender: TObject);
|
||||
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
||||
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
|
||||
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
|
||||
procedure ProcessPendingSliceClose; // отложенное удаление флага
|
||||
procedure PushVfoOverlayExtState;
|
||||
procedure PushVfoOverlayAudioDevices;
|
||||
procedure OnVfoOverlayInvalidate(Sender: TObject);
|
||||
// --- Мультислайсы (UI) ---
|
||||
function CurSliceId: Integer; // SliceId обрабатываемого сейчас флага
|
||||
procedure WireOverlayEvents(O: TVfoOverlay);
|
||||
procedure CreateSliceFlag(SliceId: Integer);
|
||||
procedure DestroySliceFlag(SliceId: Integer);
|
||||
function FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
||||
procedure AddSliceAtFreq(Hz: Double);
|
||||
procedure PositionSliceFlags;
|
||||
procedure PushSliceFlagState(SliceId: Integer);
|
||||
function DispatchFlagsMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
||||
procedure DispatchFlagsMouseMove(X, Y: Integer);
|
||||
function DispatchFlagsMouseUp(X, Y: Integer): Boolean;
|
||||
procedure DispatchFlagsMouseLeave;
|
||||
function SliceAtPixel(X: Integer): Integer; // SliceId, чья несущая под X (0 = нет)
|
||||
procedure PbSpectrumDblClick(Sender: TObject);
|
||||
procedure TrkDriveChange(Sender: TObject);
|
||||
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
||||
@@ -960,16 +985,14 @@ begin
|
||||
FDeviceDialog.Store := FController.FDeviceStore;
|
||||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||||
FVfoOverlay := TVfoOverlay.Create(Self);
|
||||
FVfoOverlay.OnSelect := OnModeFilterSelect;
|
||||
FVfoOverlay.OnDSPChange := OnVfoOverlayDSPChange;
|
||||
FVfoOverlay.OnAGCChange := OnVfoOverlayAGCChange;
|
||||
FVfoOverlay.OnVolume := OnVfoOverlayVolume;
|
||||
FVfoOverlay.OnMute := OnVfoOverlayMute;
|
||||
FVfoOverlay.OnAudioDevice := OnVfoOverlayAudioDevice;
|
||||
FVfoOverlay.OnSplit := OnVfoOverlaySplit;
|
||||
FVfoOverlay.OnTxSelect := OnVfoOverlayTxSelect;
|
||||
FVfoOverlay.OnInvalidate := OnVfoOverlayInvalidate;
|
||||
WireOverlayEvents(FVfoOverlay);
|
||||
FVfoOverlay.SetSliceInfo(0, 'A', True); // главный приёмник = слайс A
|
||||
FVfoOverlay.Visible := False;
|
||||
FActiveSliceId := 0;
|
||||
FActiveFlag := nil;
|
||||
FPendingSliceClose := 0;
|
||||
FSliceDrag := False;
|
||||
FSliceDragId := 0;
|
||||
FPanelHidden := False;
|
||||
FController.FShowSpectrum := True;
|
||||
FController.FShowWaterfall := True;
|
||||
@@ -1413,6 +1436,7 @@ begin
|
||||
SyncSpecViewFreq;
|
||||
UpdatePanZoomBar;
|
||||
PositionVfoOverlay;
|
||||
PositionSliceFlags;
|
||||
FSpecView.DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
FSpecView.DrawWaterfall;
|
||||
@@ -1497,6 +1521,16 @@ begin
|
||||
PushDeviceListToWeb; // web-overlay тоже получает обновлённый список
|
||||
end;
|
||||
end;
|
||||
// Если флаг A виден при ОТКРЫТОЙ панели (есть слайсы B+), держим его в
|
||||
// синхроне с DSP/режимом/полосой/AGC, пришедшими с ЛЕВОЙ панели. При скрытой
|
||||
// панели флаг правит себя сам (DoHit), синк не нужен. Только дискретные поля —
|
||||
// SetState/SetDSPState безусловно инвалидируют (не звать на потоковых rfVfoA).
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible and (not FPanelHidden) and
|
||||
(Field in [rfMode, rfFilter, rfNR, rfNB, rfSNB, rfANF, rfAGCMode]) then
|
||||
begin
|
||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||||
end;
|
||||
finally
|
||||
FSyncingFromController := False;
|
||||
end;
|
||||
@@ -3188,6 +3222,7 @@ procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||||
var
|
||||
NowTick: QWord;
|
||||
ElapsedTick: QWord;
|
||||
I: Integer;
|
||||
begin
|
||||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||||
@@ -3266,6 +3301,19 @@ begin
|
||||
PushVfoOverlayExtState;
|
||||
PositionVfoOverlay;
|
||||
end;
|
||||
if Length(FSliceFlags) > 0 then
|
||||
begin
|
||||
PositionSliceFlags; // держим слайс-флаги на частоте
|
||||
// S-метр слайсов троттлим до ~10 Гц: каждое изменение метра = софтовый
|
||||
// ре-рендер флага (дорого), 60 Гц там не нужно.
|
||||
if GetTickCount64 - FLastSliceMeterMs >= 100 then
|
||||
begin
|
||||
FLastSliceMeterMs := GetTickCount64;
|
||||
for I := 0 to High(FSliceFlags) do // S-метр каждого слайса из его канала
|
||||
if FSliceFlags[I] <> nil then
|
||||
FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId));
|
||||
end;
|
||||
end;
|
||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||
if FSpectrumDirty then
|
||||
@@ -4421,11 +4469,22 @@ procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var VC, VS: Double;
|
||||
begin
|
||||
if Assigned(FVfoOverlay) and
|
||||
FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
||||
if DispatchFlagsMouseDown(Button, X, Y) then
|
||||
begin
|
||||
ProcessPendingSliceClose; // × мог пометить слайс к удалению
|
||||
Exit;
|
||||
end;
|
||||
if Assigned(FSampleRateOverlay) and
|
||||
FSampleRateOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
||||
|
||||
// Ctrl+ЛКМ по спектру — добавить софт-слайс на этой частоте (в пределах захвата).
|
||||
if (Button = mbLeft) and (ssCtrl in Shift) and (PbSpectrum.Width > 0) then
|
||||
begin
|
||||
FController.GetViewWindow(VC, VS);
|
||||
AddSliceAtFreq(PixelToFreq(X, PbSpectrum.Width, VC, VS));
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if Button = mbLeft then
|
||||
begin
|
||||
// Наведение маяка: ЛКМ указывает на маяк (после включения BEACON «вооружено»,
|
||||
@@ -4441,6 +4500,18 @@ begin
|
||||
UpdateBeaconButton;
|
||||
Exit;
|
||||
end;
|
||||
// ЛКМ по маркеру несущей слайса — начинаем его перетаскивание (перетюн).
|
||||
if (PbSpectrum.Width > 0) then
|
||||
begin
|
||||
FSliceDragId := SliceAtPixel(X);
|
||||
if FSliceDragId <> 0 then
|
||||
begin
|
||||
FSliceDrag := True;
|
||||
FActiveSliceId := FSliceDragId;
|
||||
PbSpectrum.Cursor := crSizeWE;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
// Левая кнопка: сбрасываем маркер если активен
|
||||
if FSpecView.MarkerActive then
|
||||
begin
|
||||
@@ -4485,18 +4556,40 @@ end;
|
||||
|
||||
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var
|
||||
VC, VS, NewHz, LoHz, HiHz: Double;
|
||||
begin
|
||||
// Перетаскивание маркера несущей слайса — перетюн в пределах захваченной полосы.
|
||||
if FSliceDrag then
|
||||
begin
|
||||
if (ssLeft in Shift) and (PbSpectrum.Width > 0) then
|
||||
begin
|
||||
FController.GetViewWindow(VC, VS);
|
||||
NewHz := PixelToFreq(X, PbSpectrum.Width, VC, VS);
|
||||
FController.CapturedSpan(LoHz, HiHz);
|
||||
if NewHz < LoHz then NewHz := LoHz;
|
||||
if NewHz > HiHz then NewHz := HiHz;
|
||||
FController.SetSliceTarget(FSliceDragId, NewHz);
|
||||
PushSliceFlagState(FSliceDragId); // обновить частоту во флаге
|
||||
PositionSliceFlags;
|
||||
FSpectrumDirty := True;
|
||||
PbSpectrum.Invalidate;
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if Assigned(FSampleRateOverlay) then
|
||||
begin
|
||||
FSampleRateOverlay.HandleMouseMove(X, Y);
|
||||
end;
|
||||
if Assigned(FVfoOverlay) then
|
||||
FVfoOverlay.HandleMouseMove(X, Y);
|
||||
DispatchFlagsMouseMove(X, Y);
|
||||
if not FSpecDrag then
|
||||
begin
|
||||
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
||||
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
||||
PbSpectrum.Cursor := crHandPoint
|
||||
else if SliceAtPixel(X) <> 0 then
|
||||
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
||||
else
|
||||
PbSpectrum.Cursor := crDefault;
|
||||
end;
|
||||
@@ -4514,7 +4607,14 @@ end;
|
||||
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.HandleMouseUp(X, Y) then
|
||||
if FSliceDrag then
|
||||
begin
|
||||
FSliceDrag := False;
|
||||
FSliceDragId := 0;
|
||||
PbSpectrum.Cursor := crDefault;
|
||||
Exit;
|
||||
end;
|
||||
if DispatchFlagsMouseUp(X, Y) then
|
||||
begin
|
||||
FSpecDrag := False;
|
||||
PbSpectrum.Cursor := crDefault;
|
||||
@@ -4533,8 +4633,7 @@ procedure TMainForm.PbSpectrumMouseLeave(Sender: TObject);
|
||||
begin
|
||||
if Assigned(FSampleRateOverlay) then
|
||||
FSampleRateOverlay.HandleMouseLeave;
|
||||
if Assigned(FVfoOverlay) then
|
||||
FVfoOverlay.HandleMouseLeave;
|
||||
DispatchFlagsMouseLeave;
|
||||
if not FSpecDrag then PbSpectrum.Cursor := crDefault;
|
||||
end;
|
||||
|
||||
@@ -5008,6 +5107,28 @@ begin
|
||||
begin
|
||||
FLeftPanelW := PanelLeft.Width; // сохраняем реальную ширину (учитывает DPI на Win32)
|
||||
PanelLeft.Width := 0;
|
||||
end
|
||||
else
|
||||
PanelLeft.Width := FLeftPanelW; // восстанавливаем сохранённую ширину
|
||||
UpdateVfoOverlayVisibility; // видимость флага A решает общий инвариант
|
||||
if Assigned(FSampleRateOverlay) then
|
||||
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
|
||||
ResizeSpectrumPanels;
|
||||
ResizeSMeter;
|
||||
end;
|
||||
|
||||
// Флаг главного приёмника (слайс A) виден, когда СКРЫТА левая панель ЛИБО есть
|
||||
// доп. слайсы (B+): тогда A всегда представлен флагом на спектре рядом с B/C…,
|
||||
// а сворачивание/раскрытие панели при живых слайсах не дёргает его появление.
|
||||
procedure TMainForm.UpdateVfoOverlayVisibility;
|
||||
var
|
||||
ShouldShow: Boolean;
|
||||
begin
|
||||
if not Assigned(FVfoOverlay) then Exit;
|
||||
ShouldShow := FPanelHidden or (Length(FSliceFlags) > 0);
|
||||
if ShouldShow = FVfoOverlay.Visible then Exit;
|
||||
if ShouldShow then
|
||||
begin
|
||||
FVfoOverlay.Width := OVL_W;
|
||||
FVfoOverlay.Height := FVfoOverlay.BaseHeight; // растёт сам при открытии fly-out
|
||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||
@@ -5019,14 +5140,9 @@ begin
|
||||
end
|
||||
else
|
||||
begin
|
||||
PanelLeft.Width := FLeftPanelW; // восстанавливаем сохранённую ширину
|
||||
FVfoOverlay.HandleMouseLeave;
|
||||
FVfoOverlay.Visible := False;
|
||||
end;
|
||||
if Assigned(FSampleRateOverlay) then
|
||||
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
|
||||
ResizeSpectrumPanels;
|
||||
ResizeSMeter;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PositionVfoOverlay;
|
||||
@@ -5089,6 +5205,14 @@ procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||||
var
|
||||
Idx: Integer;
|
||||
begin
|
||||
// Событие пришло от флага слайса (B+) — маршрутизируем в slice-API, главный не трогаем.
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
||||
PushSliceFlagState(CurSliceId);
|
||||
PositionSliceFlags;
|
||||
Exit;
|
||||
end;
|
||||
FController.FMode := Mode;
|
||||
FController.FFilterBW := FilterBW;
|
||||
if Mode = MODE_FM then
|
||||
@@ -5118,6 +5242,11 @@ end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||
begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceDSP(CurSliceId, NRMode, NBMode, SNBOn, ANFOn);
|
||||
Exit;
|
||||
end;
|
||||
// Команды контроллера применяют к DSP и через OnControllerState обновляют кнопки.
|
||||
FController.SetNR(NRMode);
|
||||
FController.SetNB(NBMode);
|
||||
@@ -5127,34 +5256,59 @@ end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayAGCChange(AGCMode: Integer);
|
||||
begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceAGCMode(CurSliceId, TRadioController.AGCModeFromUI(AGCMode));
|
||||
Exit;
|
||||
end;
|
||||
FController.SetAGCMode(AGCMode);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayVolume(Vol: Integer);
|
||||
begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceVolume(CurSliceId, Vol / 100.0);
|
||||
Exit;
|
||||
end;
|
||||
FController.SetVolume(Vol);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayMute(Sender: TObject);
|
||||
var S: TCtrlSlice;
|
||||
begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
if FController.GetSlice(CurSliceId, S) then
|
||||
FController.SetSliceMute(CurSliceId, not S.Muted);
|
||||
Exit;
|
||||
end;
|
||||
FController.ToggleMute;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlaySplit(Sender: TObject);
|
||||
begin
|
||||
if CurSliceId <> 0 then Exit; // SPLIT — только главный (слайсы не передают)
|
||||
FController.SetSplit(not FController.FSplitTxB);
|
||||
PushVfoOverlayExtState;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayTxSelect(Sender: TObject);
|
||||
begin
|
||||
// TODO(слайсы): выбрать этот слайс как TX-слайс. Пока один слайс —
|
||||
// он всегда TX-слайс, поэтому клик ничего не меняет (хук под мультислайсы).
|
||||
// TODO(слайсы): выбор TX-слайса при полноценной мультислайс-передаче.
|
||||
// Пока передаёт только главный канал — клик по TX неглавного игнорируем.
|
||||
if CurSliceId <> 0 then Exit;
|
||||
PushVfoOverlayExtState;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
||||
begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
||||
PushSliceFlagState(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||
Exit;
|
||||
end;
|
||||
ApplyAudioDevice(DevIndex, DevName);
|
||||
PushVfoOverlayAudioDevices; // обновить подсветку текущего устройства
|
||||
end;
|
||||
@@ -5193,9 +5347,326 @@ begin
|
||||
if Assigned(FSpecView) then
|
||||
begin
|
||||
FSpecView.SpectrumDirty := True;
|
||||
FSpecView.InvalidateOverlayCache;
|
||||
// Полная инвалидация кэша текстур оверлеев (band/samplerate/главный VFO) нужна
|
||||
// ТОЛЬКО когда у главного оверлея изменился хром (CacheDirty). Если сдвинулся
|
||||
// лишь S-метр (MeterDirty) — перезаливаем только текстуру главного флага, не
|
||||
// дёргая band/samplerate/слайсы. Слайсы перезаливают свою текстуру сами.
|
||||
if Sender = TObject(FVfoOverlay) then
|
||||
begin
|
||||
if FVfoOverlay.CacheDirty then
|
||||
FSpecView.InvalidateOverlayCache
|
||||
else
|
||||
FSpecView.InvalidateVfoOverlay;
|
||||
end;
|
||||
end;
|
||||
if Assigned(PbSpectrum) then PbSpectrum.Invalidate;
|
||||
if Assigned(PbSpectrum) then
|
||||
begin
|
||||
PbSpectrum.Invalidate;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Мультислайсы (UI): коллекция флагов, роутинг событий по SliceId
|
||||
// ===========================================================================
|
||||
|
||||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный приёмник / слайс A).
|
||||
function TMainForm.CurSliceId: Integer;
|
||||
begin
|
||||
if Assigned(FActiveFlag) then Result := FActiveFlag.SliceId else Result := 0;
|
||||
end;
|
||||
|
||||
// Единая проводка событий флага (главного и слайсов) на общие обработчики.
|
||||
procedure TMainForm.WireOverlayEvents(O: TVfoOverlay);
|
||||
begin
|
||||
O.OnSelect := OnModeFilterSelect;
|
||||
O.OnDSPChange := OnVfoOverlayDSPChange;
|
||||
O.OnAGCChange := OnVfoOverlayAGCChange;
|
||||
O.OnVolume := OnVfoOverlayVolume;
|
||||
O.OnMute := OnVfoOverlayMute;
|
||||
O.OnAudioDevice := OnVfoOverlayAudioDevice;
|
||||
O.OnSplit := OnVfoOverlaySplit;
|
||||
O.OnTxSelect := OnVfoOverlayTxSelect;
|
||||
O.OnSliceSelect := OnVfoOverlaySliceSelect;
|
||||
O.OnClose := OnVfoOverlayClose;
|
||||
O.OnInvalidate := OnVfoOverlayInvalidate;
|
||||
end;
|
||||
|
||||
function TMainForm.FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := nil;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if (FSliceFlags[i] <> nil) and (FSliceFlags[i].SliceId = SliceId) then
|
||||
Exit(FSliceFlags[i]);
|
||||
end;
|
||||
|
||||
procedure TMainForm.CreateSliceFlag(SliceId: Integer);
|
||||
var
|
||||
O: TVfoOverlay;
|
||||
S: TCtrlSlice;
|
||||
begin
|
||||
if FindSliceFlag(SliceId) <> nil then Exit;
|
||||
if not FController.GetSlice(SliceId, S) then Exit;
|
||||
O := TVfoOverlay.Create(Self);
|
||||
WireOverlayEvents(O);
|
||||
O.SetSliceInfo(SliceId, S.Letter, False);
|
||||
O.SMeterThreshold := 1.5; // слайсы: грубее метр → меньше дорогих ре-рендеров флага
|
||||
O.Width := OVL_W;
|
||||
O.Height := O.BaseHeight;
|
||||
O.Visible := True; // флаги слайсов видны всегда (не зависят от скрытия панели)
|
||||
SetLength(FSliceFlags, Length(FSliceFlags) + 1);
|
||||
FSliceFlags[High(FSliceFlags)] := O;
|
||||
FSpecView.AddSliceOverlay(O);
|
||||
PushSliceFlagState(SliceId);
|
||||
UpdateVfoOverlayVisibility; // появился слайс B+ → показать и флаг A (если панель открыта)
|
||||
PositionSliceFlags;
|
||||
end;
|
||||
|
||||
procedure TMainForm.DestroySliceFlag(SliceId: Integer);
|
||||
var
|
||||
i, j: Integer;
|
||||
O: TVfoOverlay;
|
||||
begin
|
||||
O := FindSliceFlag(SliceId);
|
||||
if O = nil then Exit;
|
||||
FController.RemoveSlice(SliceId);
|
||||
FSpecView.RemoveSliceOverlay(O);
|
||||
// Убираем из массива со сжатием.
|
||||
j := 0;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if FSliceFlags[i] <> O then
|
||||
begin
|
||||
FSliceFlags[j] := FSliceFlags[i];
|
||||
Inc(j);
|
||||
end;
|
||||
SetLength(FSliceFlags, j);
|
||||
O.Free;
|
||||
if FActiveSliceId = SliceId then FActiveSliceId := 0;
|
||||
UpdateVfoOverlayVisibility; // закрыт последний слайс при открытой панели → скрыть флаг A
|
||||
OnVfoOverlayInvalidate(nil);
|
||||
end;
|
||||
|
||||
// Добавить софт-слайс на частоте Hz (режим/полоса копируются с главного).
|
||||
procedure TMainForm.AddSliceAtFreq(Hz: Double);
|
||||
var
|
||||
Lo, Hi, Id: Integer;
|
||||
begin
|
||||
if not FController.SliceFitsCapture(Hz) then Exit; // вне захваченной полосы (см. Этап 3)
|
||||
TRadioController.FilterEdgesFor(FController.FMode, FController.FFilterBW, Lo, Hi);
|
||||
Id := FController.AddSlice(Hz, FController.FMode, Lo, Hi,
|
||||
TRadioController.AGCModeFromUI(FController.FAGCMode), 0.7, -1, '');
|
||||
if Id < 0 then Exit;
|
||||
CreateSliceFlag(Id);
|
||||
FActiveSliceId := Id;
|
||||
end;
|
||||
|
||||
// SliceId, чья несущая под пикселем X (в пределах допуска), 0 если нет.
|
||||
function TMainForm.SliceAtPixel(X: Integer): Integer;
|
||||
const TOL = 6;
|
||||
var
|
||||
i, CarrierX: Integer;
|
||||
VC, VS: Double;
|
||||
S: TCtrlSlice;
|
||||
O: TVfoOverlay;
|
||||
begin
|
||||
Result := 0;
|
||||
if (PbSpectrum = nil) or (PbSpectrum.Width <= 0) then Exit;
|
||||
FController.GetViewWindow(VC, VS);
|
||||
if VS <= 0 then Exit;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
begin
|
||||
O := FSliceFlags[i];
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
if not FController.GetSlice(O.SliceId, S) then Continue;
|
||||
CarrierX := Round((S.TargetHz - VC + VS / 2) / VS * PbSpectrum.Width);
|
||||
if Abs(X - CarrierX) <= TOL then Exit(O.SliceId);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PositionSliceFlags;
|
||||
var
|
||||
i, VfoX, OW, NewLeft, NewTop, FilterStartX, FilterEndX, BWs: Integer;
|
||||
VC, VS, LoHz, HiHz: Double;
|
||||
S: TCtrlSlice;
|
||||
O: TVfoOverlay;
|
||||
Moved: Boolean;
|
||||
begin
|
||||
if (Length(FSliceFlags) = 0) or (PbSpectrum = nil) then Exit;
|
||||
Moved := False;
|
||||
FController.GetViewWindow(VC, VS);
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
begin
|
||||
O := FSliceFlags[i];
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
if not FController.GetSlice(O.SliceId, S) then Continue;
|
||||
OW := O.Width;
|
||||
if (PbSpectrum.Width > 0) and (VS > 0) then
|
||||
VfoX := Round((S.TargetHz - VC + VS / 2) / VS * PbSpectrum.Width)
|
||||
else
|
||||
VfoX := PbSpectrum.Width div 2;
|
||||
// Кромки полосы фильтра слайса относительно несущей — как у главного флага
|
||||
// (PositionVfoOverlay): флаг ставится СБОКУ от полосы, а не от несущей.
|
||||
case S.Mode of
|
||||
MODE_LSB: BWs := -S.FilterLo;
|
||||
MODE_USB: BWs := S.FilterHi;
|
||||
else BWs := S.FilterHi - S.FilterLo;
|
||||
end;
|
||||
case S.Mode of
|
||||
MODE_LSB: begin LoHz := -BWs; HiHz := -100; end;
|
||||
MODE_USB: begin LoHz := 100; HiHz := BWs; end;
|
||||
else begin LoHz := -BWs / 2; HiHz := BWs / 2; end;
|
||||
end;
|
||||
if (PbSpectrum.Width > 0) and (VS > 0) then
|
||||
begin
|
||||
FilterStartX := VfoX + Round(LoHz / VS * PbSpectrum.Width);
|
||||
FilterEndX := VfoX + Round(HiHz / VS * PbSpectrum.Width);
|
||||
end
|
||||
else
|
||||
begin
|
||||
FilterStartX := VfoX;
|
||||
FilterEndX := VfoX;
|
||||
end;
|
||||
FilterStartX := Max(0, Min(PbSpectrum.Width - 1, FilterStartX));
|
||||
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
|
||||
// По умолчанию справа от полосы; если не влезает — слева от неё.
|
||||
NewLeft := FilterEndX + 6;
|
||||
if NewLeft + OW > PbSpectrum.Width - 4 then NewLeft := FilterStartX - 6 - OW;
|
||||
NewLeft := Max(4, Min(PbSpectrum.Width - OW - 4, NewLeft));
|
||||
NewTop := 16 + i * 12; // лёгкий вертикальный разнос, чтобы флаги не слипались
|
||||
if (O.Left <> NewLeft) or (O.Top <> NewTop) then
|
||||
begin
|
||||
O.Left := NewLeft;
|
||||
O.Top := NewTop;
|
||||
Moved := True;
|
||||
end;
|
||||
end;
|
||||
// Инвалидируем спектр только если флаг реально сдвинулся (ретюн/зум/пан).
|
||||
// Раньше звали безусловно каждый тик → форс полной перерисовки на пустом месте.
|
||||
if Moved then
|
||||
begin
|
||||
OnVfoOverlayInvalidate(nil);
|
||||
end;
|
||||
end;
|
||||
|
||||
// Заливает во флаг слайса его текущее состояние из контроллера.
|
||||
procedure TMainForm.PushSliceFlagState(SliceId: Integer);
|
||||
var
|
||||
O: TVfoOverlay;
|
||||
S: TCtrlSlice;
|
||||
BW, Cnt, I: Integer;
|
||||
Names: TStringList;
|
||||
Indices: array[0..63] of Integer;
|
||||
Idx: array of Integer;
|
||||
begin
|
||||
O := FindSliceFlag(SliceId);
|
||||
if (O = nil) or not FController.GetSlice(SliceId, S) then Exit;
|
||||
case S.Mode of
|
||||
MODE_LSB: BW := -S.FilterLo;
|
||||
MODE_USB: BW := S.FilterHi;
|
||||
else BW := S.FilterHi - S.FilterLo;
|
||||
end;
|
||||
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
|
||||
O.SetDSPState(0, 0, False, False, FController.FAGCMode);
|
||||
O.SetExtState(Round(S.Volume * 100), S.Muted, False, False, False);
|
||||
// Список аудио-устройств (общий PA-список) + текущее устройство слайса.
|
||||
if Assigned(FController.FAudioOut) then
|
||||
begin
|
||||
Names := TStringList.Create;
|
||||
try
|
||||
Cnt := 0;
|
||||
FController.FAudioOut.EnumOutputDevices(Names, Indices, Cnt);
|
||||
SetLength(Idx, Cnt);
|
||||
for I := 0 to Cnt - 1 do Idx[I] := Indices[I];
|
||||
O.SetAudioDevices(Names, Idx, S.DevName);
|
||||
finally
|
||||
Names.Free;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlaySliceSelect(Sender: TObject);
|
||||
begin
|
||||
if Sender is TVfoOverlay then
|
||||
FActiveSliceId := TVfoOverlay(Sender).SliceId;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayClose(Sender: TObject);
|
||||
begin
|
||||
if not (Sender is TVfoOverlay) then Exit;
|
||||
if TVfoOverlay(Sender).IsMain then Exit;
|
||||
// Нельзя освобождать флаг внутри его же обработчика мыши — помечаем к удалению,
|
||||
// ProcessPendingSliceClose снимет его после возврата из диспетчера.
|
||||
FPendingSliceClose := TVfoOverlay(Sender).SliceId;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ProcessPendingSliceClose;
|
||||
var Id: Integer;
|
||||
begin
|
||||
if FPendingSliceClose = 0 then Exit;
|
||||
Id := FPendingSliceClose;
|
||||
FPendingSliceClose := 0;
|
||||
DestroySliceFlag(Id);
|
||||
end;
|
||||
|
||||
function TMainForm.DispatchFlagsMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||
begin
|
||||
FActiveFlag := FVfoOverlay;
|
||||
if FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit(True);
|
||||
end;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if (FSliceFlags[i] <> nil) and FSliceFlags[i].Visible then
|
||||
begin
|
||||
FActiveFlag := FSliceFlags[i];
|
||||
if FSliceFlags[i].HandleMouseDown(Button, X, Y) then Exit(True);
|
||||
end;
|
||||
FActiveFlag := nil;
|
||||
end;
|
||||
|
||||
procedure TMainForm.DispatchFlagsMouseMove(X, Y: Integer);
|
||||
var i: Integer;
|
||||
begin
|
||||
if Assigned(FVfoOverlay) then
|
||||
begin
|
||||
FActiveFlag := FVfoOverlay;
|
||||
FVfoOverlay.HandleMouseMove(X, Y);
|
||||
end;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if FSliceFlags[i] <> nil then
|
||||
begin
|
||||
FActiveFlag := FSliceFlags[i];
|
||||
FSliceFlags[i].HandleMouseMove(X, Y);
|
||||
end;
|
||||
FActiveFlag := nil;
|
||||
end;
|
||||
|
||||
function TMainForm.DispatchFlagsMouseUp(X, Y: Integer): Boolean;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
if Assigned(FVfoOverlay) then
|
||||
begin
|
||||
FActiveFlag := FVfoOverlay;
|
||||
if FVfoOverlay.HandleMouseUp(X, Y) then Result := True;
|
||||
end;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if FSliceFlags[i] <> nil then
|
||||
begin
|
||||
FActiveFlag := FSliceFlags[i];
|
||||
if FSliceFlags[i].HandleMouseUp(X, Y) then Result := True;
|
||||
end;
|
||||
FActiveFlag := nil;
|
||||
end;
|
||||
|
||||
procedure TMainForm.DispatchFlagsMouseLeave;
|
||||
var i: Integer;
|
||||
begin
|
||||
if Assigned(FVfoOverlay) then FVfoOverlay.HandleMouseLeave;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if FSliceFlags[i] <> nil then FSliceFlags[i].HandleMouseLeave;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
|
||||
@@ -5752,16 +6223,63 @@ procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState;
|
||||
P := C.ScreenToClient(Mouse.CursorPos);
|
||||
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
|
||||
end;
|
||||
// Курсор над флагом-оверлеем слайса (в клиентских координатах спектра)?
|
||||
function SliceUnderCursor: Integer;
|
||||
var P: TPoint; i: Integer; O: TVfoOverlay;
|
||||
begin
|
||||
Result := 0;
|
||||
if (PbSpectrum = nil) or (not PbSpectrum.Visible) then Exit;
|
||||
P := PbSpectrum.ScreenToClient(Mouse.CursorPos);
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
begin
|
||||
O := FSliceFlags[i];
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
if (P.X >= O.Left) and (P.X < O.Left + O.Width) and
|
||||
(P.Y >= O.Top) and (P.Y < O.Top + O.Height) then
|
||||
Exit(O.SliceId);
|
||||
end;
|
||||
end;
|
||||
var
|
||||
Step: Int64;
|
||||
Delta: Int64;
|
||||
Base: Int64;
|
||||
NewFreq: Int64;
|
||||
SliceId: Integer;
|
||||
SLo, SHi, SNew: Double;
|
||||
Sl: TCtrlSlice;
|
||||
begin
|
||||
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
||||
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
||||
// виджет крутил бы B, а этот хендлер — активный A.
|
||||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
||||
|
||||
// Колесо над маркером слайса — двигаем частоту ЭТОГО слайса, а не главного VFO.
|
||||
SliceId := SliceUnderCursor;
|
||||
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
||||
begin
|
||||
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||||
else if ssCtrl in Shift then Step := 1000
|
||||
else if ssShift in Shift then Step := 10
|
||||
else Step := 100;
|
||||
Delta := 1;
|
||||
if WheelDelta < 0 then Delta := -1;
|
||||
// Квантование к сетке шага + шаг в сторону колеса.
|
||||
Base := (Round(Sl.TargetHz) div Step) * Step;
|
||||
if Delta > 0 then SNew := Base + Step
|
||||
else if Round(Sl.TargetHz) = Base then SNew := Base - Step
|
||||
else SNew := Base;
|
||||
FController.CapturedSpan(SLo, SHi);
|
||||
if SNew < SLo then SNew := SLo;
|
||||
if SNew > SHi then SNew := SHi;
|
||||
FController.SetSliceTarget(SliceId, SNew);
|
||||
PushSliceFlagState(SliceId);
|
||||
PositionSliceFlags;
|
||||
FSpectrumDirty := True;
|
||||
PbSpectrum.Invalidate;
|
||||
FActiveSliceId := SliceId;
|
||||
Handled := True;
|
||||
Exit;
|
||||
end;
|
||||
if (FController.FMode = MODE_FM) and FController.FFMStepOn and
|
||||
not ((ssCtrl in Shift) or (ssShift in Shift)) then
|
||||
Step := FM_STEP_HZ[FController.FFMStepIdx]
|
||||
|
||||
Reference in New Issue
Block a user