mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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]
|
||||
|
||||
+325
-11
@@ -122,6 +122,24 @@ type
|
||||
PLLLock: Boolean;
|
||||
end;
|
||||
|
||||
// Слайс уровня контроллера: логическая обёртка над WDSP-слайс-каналом
|
||||
// (в TWDSPEngine) + собственный аудио-выход со своей звуковухой.
|
||||
TCtrlSlice = record
|
||||
Active: Boolean;
|
||||
Id: Integer; // = id слайса в TWDSPEngine
|
||||
Letter: Char; // буква флага ('A'..'F'), пока для UI
|
||||
TargetHz: Double; // абсолютная частота слайса
|
||||
Mode: Integer;
|
||||
FilterLo: Integer;
|
||||
FilterHi: Integer;
|
||||
AGC: TWDSPAGCMode;
|
||||
Volume: Double; // 0..1
|
||||
Muted: Boolean;
|
||||
DeviceIndex: Integer; // PortAudio-индекс (-1 = default)
|
||||
DevName: string;
|
||||
Audio: TAudioOutput; // владеет; свой поток вывода
|
||||
end;
|
||||
|
||||
{ TRadioController }
|
||||
TRadioController = class
|
||||
private
|
||||
@@ -134,6 +152,11 @@ type
|
||||
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
|
||||
FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер)
|
||||
procedure Changed(Field: TRadioField);
|
||||
// --- Мультислайсы ---
|
||||
function FindSliceIndex(Id: Integer): Integer; // -1 если нет
|
||||
procedure OnSliceAudioReady(SliceId: Integer;
|
||||
const Left, Right: array of Single; Count: Integer);
|
||||
procedure RefreshSliceShifts; // пересчёт ShiftHz всех слайсов при смене центра
|
||||
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||||
@@ -163,6 +186,12 @@ type
|
||||
FDSPEngine: TWDSPEngine;
|
||||
FAudioOut: TAudioOutput;
|
||||
FAudioIn: TAudioInput;
|
||||
|
||||
// ---- Мультислайсы (софт-fan-out) ----
|
||||
// Слайс = доп. приёмник (свой WDSP-канал в движке) + своя звуковуха.
|
||||
// TargetHz абсолютна; ShiftHz в движок = TargetHz - FCenterFreq.
|
||||
FSlices: array[0..MAX_SLICES-1] of TCtrlSlice;
|
||||
FNextSliceId: Integer; // монотонный генератор id слайсов
|
||||
FSettings: TSettingsManager;
|
||||
FChannelStore: TChannelStore;
|
||||
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
|
||||
@@ -467,6 +496,32 @@ type
|
||||
procedure DriveBy(Delta: Integer);
|
||||
procedure SetAtten(Idx: Integer);
|
||||
|
||||
// ---- Мультислайсы (софт-fan-out) ----
|
||||
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
|
||||
// на устройстве DevIndex (-1=default). Возвращает Id слайса или -1 при отказе
|
||||
// (нет слотов / TargetHz вне захваченной полосы). DevName для персиста/UI.
|
||||
function AddSlice(TargetHz: Double; Mode, FilterLo, FilterHi: Integer;
|
||||
AGC: TWDSPAGCMode; Vol: Double;
|
||||
DevIndex: Integer; const DevName: string): Integer;
|
||||
procedure RemoveSlice(Id: Integer);
|
||||
procedure SetSliceTarget(Id: Integer; TargetHz: Double);
|
||||
procedure SetSliceMode(Id, Mode: Integer);
|
||||
procedure SetSliceFilter(Id, Low, High: Integer);
|
||||
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
|
||||
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
|
||||
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||||
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
|
||||
procedure SetSliceMute(Id: Integer; Mute: Boolean);
|
||||
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
|
||||
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
|
||||
function SliceCount: Integer;
|
||||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||||
// Диапазон захваченной полосы вокруг центра, куда можно ставить софт-слайс.
|
||||
procedure CapturedSpan(out LoHz, HiHz: Double);
|
||||
function SliceFitsCapture(TargetHz: Double): Boolean;
|
||||
|
||||
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
|
||||
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
|
||||
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
|
||||
@@ -625,6 +680,8 @@ begin
|
||||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||||
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||||
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
||||
FNextSliceId := 1; // мультислайсы: генератор id
|
||||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
||||
FCurrentBand := 5; FCurrentXvtr := -1;
|
||||
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
||||
@@ -649,6 +706,9 @@ begin
|
||||
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
|
||||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||||
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
||||
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
||||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||
|
||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||||
@@ -672,12 +732,21 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.FreeEngines;
|
||||
var i: Integer;
|
||||
begin
|
||||
if Assigned(FNetwork) then
|
||||
begin
|
||||
if FNetwork.Connected then FNetwork.Disconnect;
|
||||
FreeAndNil(FNetwork);
|
||||
end;
|
||||
// Слайсы: закрываем их аудио-выходы (WDSP-каналы закроет FDSPEngine.Close ниже).
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].Audio <> nil) then
|
||||
begin
|
||||
FSlices[i].Audio.Close;
|
||||
FreeAndNil(FSlices[i].Audio);
|
||||
FSlices[i].Active := False;
|
||||
end;
|
||||
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
||||
if Assigned(FDSPEngine) then
|
||||
@@ -966,6 +1035,227 @@ begin
|
||||
FAudioOut.Write(Left, Right, Count);
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Мультислайсы (софт-fan-out)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TRadioController.FindSliceIndex(Id: Integer): Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := -1;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
|
||||
end;
|
||||
|
||||
// DSP-поток: аудио готового слайса → его собственный выход. Слайс — локальный
|
||||
// монитор (в web/на радио не уходит), поэтому только на свою звуковуху.
|
||||
procedure TRadioController.OnSliceAudioReady(SliceId: Integer;
|
||||
const Left, Right: array of Single; Count: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
if not FLocalAudio then Exit;
|
||||
idx := FindSliceIndex(SliceId);
|
||||
if idx < 0 then Exit;
|
||||
if FSlices[idx].Audio <> nil then
|
||||
FSlices[idx].Audio.Write(Left, Right, Count);
|
||||
end;
|
||||
|
||||
procedure TRadioController.RefreshSliceShifts;
|
||||
var i: Integer;
|
||||
begin
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active then
|
||||
FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FCenterFreq);
|
||||
end;
|
||||
|
||||
procedure TRadioController.CapturedSpan(out LoHz, HiHz: Double);
|
||||
// Полезная полоса ±47.5% от захвата (запас на краях фильтра/анти-алиасинга).
|
||||
var Half: Double;
|
||||
begin
|
||||
Half := FSampleRate * 0.475;
|
||||
LoHz := FCenterFreq - Half;
|
||||
HiHz := FCenterFreq + Half;
|
||||
end;
|
||||
|
||||
function TRadioController.SliceFitsCapture(TargetHz: Double): Boolean;
|
||||
var Lo, Hi: Double;
|
||||
begin
|
||||
CapturedSpan(Lo, Hi);
|
||||
Result := (TargetHz >= Lo) and (TargetHz <= Hi);
|
||||
end;
|
||||
|
||||
function TRadioController.AddSlice(TargetHz: Double;
|
||||
Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double;
|
||||
DevIndex: Integer; const DevName: string): Integer;
|
||||
var
|
||||
i, slot, Id: Integer;
|
||||
AO: TAudioOutput;
|
||||
begin
|
||||
Result := -1;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
if not SliceFitsCapture(TargetHz) then Exit; // вне захваченной полосы (Этап 3=HW DDC)
|
||||
|
||||
slot := -1;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if not FSlices[i].Active then begin slot := i; Break; end;
|
||||
if slot < 0 then Exit; // нет свободных слотов
|
||||
|
||||
Id := FNextSliceId;
|
||||
if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi,
|
||||
TargetHz - FCenterFreq, Vol, AGC) then Exit;
|
||||
Inc(FNextSliceId);
|
||||
|
||||
// Свой аудио-выход слайса на выбранной звуковухе.
|
||||
AO := TAudioOutput.Create(48000);
|
||||
AO.OutputBufferSize := FSettings.LoadAudioBufferSize;
|
||||
AO.DeviceIndex := DevIndex;
|
||||
if FLocalAudio then AO.Open;
|
||||
|
||||
FSlices[slot].Active := True;
|
||||
FSlices[slot].Id := Id;
|
||||
// Буква флага. Слайс A — это главный приёмник (не в FSlices[]), поэтому доп.
|
||||
// слайсы нумеруются с 'B' (slot 0 → B, 1 → C, …), иначе получалось два «A».
|
||||
FSlices[slot].Letter := Chr(Ord('B') + slot);
|
||||
FSlices[slot].TargetHz := TargetHz;
|
||||
FSlices[slot].Mode := Mode;
|
||||
FSlices[slot].FilterLo := FilterLo;
|
||||
FSlices[slot].FilterHi := FilterHi;
|
||||
FSlices[slot].AGC := AGC;
|
||||
FSlices[slot].Volume := Vol;
|
||||
FSlices[slot].Muted := False;
|
||||
FSlices[slot].DeviceIndex := DevIndex;
|
||||
FSlices[slot].DevName := DevName;
|
||||
FSlices[slot].Audio := AO;
|
||||
|
||||
Result := Id;
|
||||
Changed(rfDevice);
|
||||
end;
|
||||
|
||||
procedure TRadioController.RemoveSlice(Id: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
if Assigned(FDSPEngine) then FDSPEngine.RemoveSlice(Id);
|
||||
if FSlices[idx].Audio <> nil then
|
||||
begin
|
||||
FSlices[idx].Audio.Close;
|
||||
FreeAndNil(FSlices[idx].Audio);
|
||||
end;
|
||||
FSlices[idx].Active := False;
|
||||
Changed(rfDevice);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceTarget(Id: Integer; TargetHz: Double);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].TargetHz := TargetHz;
|
||||
if Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetSliceShift(Id, TargetHz - FCenterFreq);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceMode(Id, Mode: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].Mode := Mode;
|
||||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceFilter(Id, Low, High: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].FilterLo := Low;
|
||||
FSlices[idx].FilterHi := High;
|
||||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceFilter(Id, Low, High);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].AGC := AGC;
|
||||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceAGC(Id, AGC);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceVolume(Id: Integer; Vol: Double);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].Volume := Vol;
|
||||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceVolume(Id, Vol);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceMute(Id: Integer; Mute: Boolean);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].Muted := Mute;
|
||||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer;
|
||||
SNBOn, ANFOn: Boolean);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
if Assigned(FDSPEngine) then
|
||||
begin
|
||||
FDSPEngine.SetSliceNRMode(Id, NRMode);
|
||||
FDSPEngine.SetSliceNBMode(Id, NBMode);
|
||||
FDSPEngine.SetSliceSNB(Id, SNBOn);
|
||||
FDSPEngine.SetSliceANF(Id, ANFOn);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.SliceSMeter(Id: Integer): Double;
|
||||
begin
|
||||
if Assigned(FDSPEngine) then Result := FDSPEngine.GetSliceSMeter(Id)
|
||||
else Result := -130.0;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer;
|
||||
const DevName: string);
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].DeviceIndex := DevIndex;
|
||||
FSlices[idx].DevName := DevName;
|
||||
if FSlices[idx].Audio <> nil then
|
||||
begin
|
||||
FSlices[idx].Audio.Close;
|
||||
FSlices[idx].Audio.DeviceIndex := DevIndex;
|
||||
if FLocalAudio then FSlices[idx].Audio.Open;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.SliceCount: Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active then Inc(Result);
|
||||
end;
|
||||
|
||||
function TRadioController.GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||||
var idx: Integer;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
Result := idx >= 0;
|
||||
if Result then S := FSlices[idx];
|
||||
end;
|
||||
|
||||
procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
|
||||
@@ -1335,16 +1625,31 @@ begin
|
||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||
end;
|
||||
|
||||
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||||
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||
var Half: Integer;
|
||||
begin
|
||||
Half := BW div 2;
|
||||
case Mode of
|
||||
0: begin Lo := -BW; Hi := -100; end; // LSB
|
||||
1: begin Lo := 100; Hi := BW; end; // USB
|
||||
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceModeBW(Id, Mode, BW: Integer);
|
||||
var Lo, Hi: Integer;
|
||||
begin
|
||||
FilterEdgesFor(Mode, BW, Lo, Hi);
|
||||
SetSliceMode(Id, Mode);
|
||||
SetSliceFilter(Id, Lo, Hi);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyModeFilter;
|
||||
var
|
||||
Lo, Hi, Half: Integer;
|
||||
Lo, Hi: Integer;
|
||||
begin
|
||||
Half := FFilterBW div 2;
|
||||
case FMode of
|
||||
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB
|
||||
1: begin Lo := 100; Hi := FFilterBW; end; // USB
|
||||
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
|
||||
end;
|
||||
FilterEdgesFor(FMode, FFilterBW, Lo, Hi);
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
begin
|
||||
FDSPEngine.SetFilter(Lo, Hi);
|
||||
@@ -1921,6 +2226,7 @@ begin
|
||||
NeedPush := True;
|
||||
end;
|
||||
|
||||
RefreshSliceShifts; // центр мог сместиться — держим слайсы на их TargetHz
|
||||
if NeedPush then PushNetworkState;
|
||||
Changed(rfActiveVfo);
|
||||
end;
|
||||
@@ -1941,6 +2247,7 @@ begin
|
||||
FCenterFreq := Hz;
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
|
||||
RefreshSliceShifts; // слайсы привязаны к абсолютной частоте — сдвинуть NCO
|
||||
PushNetworkState;
|
||||
Changed(rfCenterFreq);
|
||||
end;
|
||||
@@ -1972,6 +2279,7 @@ begin
|
||||
end;
|
||||
FCenterFreq := ActiveFreq;
|
||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||||
RefreshSliceShifts;
|
||||
PushNetworkState;
|
||||
Changed(rfVfoA);
|
||||
Changed(rfVfoB);
|
||||
@@ -2057,14 +2365,20 @@ begin
|
||||
Changed(rfFilter);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyAGCToEngine;
|
||||
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех
|
||||
// фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
||||
class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode;
|
||||
const
|
||||
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для
|
||||
// всех фронтендов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
||||
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||||
begin
|
||||
if (M < 0) or (M > 4) then M := 1;
|
||||
Result := AGCModes[M];
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyAGCToEngine;
|
||||
begin
|
||||
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
|
||||
FDSPEngine.SetAGC(AGCModes[FAGCMode], 50.0);
|
||||
FDSPEngine.SetAGC(AGCModeFromUI(FAGCMode), 50.0);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetAGCMode(M: Integer);
|
||||
|
||||
+85
-5
@@ -70,6 +70,7 @@ type
|
||||
FADCOverloadVisible: Boolean;
|
||||
FSampleRateOverlay: TSampleRateOverlay;
|
||||
FVfoOverlay: TVfoOverlay;
|
||||
FSliceOverlays: TFPList; // доп. слайс-флаги (B+); не владеет (владелец MainForm)
|
||||
FBandPlanOverlay: TBandPlanOverlay;
|
||||
// ── Marker ────────────────────────────────────────────────────────────────
|
||||
FMarkerActive: Boolean;
|
||||
@@ -108,12 +109,15 @@ type
|
||||
procedure SetFillSpectrum(V: Boolean);
|
||||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||||
procedure DrawSliceFilterMarkers(C: TCanvas; W, H: Integer);
|
||||
procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
procedure CopyGridToSpectrum(W, H: Integer);
|
||||
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
|
||||
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
function GetWaterfallDirty: Boolean;
|
||||
procedure SetWaterfallDirty(V: Boolean);
|
||||
// Waterfall sub-view delegating accessors
|
||||
@@ -218,6 +222,11 @@ type
|
||||
property PbSMeterRight: TPaintBox write SetPbSMeterRight;
|
||||
property SampleRateOverlay: TSampleRateOverlay read FSampleRateOverlay write FSampleRateOverlay;
|
||||
property VfoOverlay: TVfoOverlay read FVfoOverlay write FVfoOverlay;
|
||||
// Доп. слайс-флаги (B+): рисуются поверх спектра после главного оверлея.
|
||||
procedure AddSliceOverlay(O: TVfoOverlay);
|
||||
procedure RemoveSliceOverlay(O: TVfoOverlay);
|
||||
function SliceOverlayCount: Integer;
|
||||
function SliceOverlayAt(Index: Integer): TVfoOverlay;
|
||||
property BandPlanOverlay: TBandPlanOverlay read FBandPlanOverlay write FBandPlanOverlay;
|
||||
|
||||
// ── Данные от DSP ─────────────────────────────────────────────────────────
|
||||
@@ -244,6 +253,9 @@ type
|
||||
// ── Утилиты ───────────────────────────────────────────────────────────────
|
||||
procedure InvalidateGridCache; virtual;
|
||||
procedure InvalidateOverlayCache; virtual;
|
||||
// Инвалидация ТОЛЬКО текстуры главного VFO-флага (без band/samplerate/слайсов).
|
||||
// Нужна для дешёвого обновления S-метра главного флага в GL-вьюхе.
|
||||
procedure InvalidateVfoOverlay; virtual;
|
||||
procedure SetTheme(const T: TAppTheme); virtual;
|
||||
procedure InvalidateRulerCache; virtual;
|
||||
procedure ResetSpectrumBuf; virtual;
|
||||
@@ -329,6 +341,7 @@ begin
|
||||
FWaterfall := TWaterfallView.Create;
|
||||
FSMeter := TSMeterView.Create;
|
||||
FRuler := TRulerView.Create;
|
||||
FSliceOverlays := TFPList.Create;
|
||||
ResetSpectrumBuf;
|
||||
end;
|
||||
|
||||
@@ -339,9 +352,30 @@ begin
|
||||
FWaterfall.Free;
|
||||
FSMeter.Free;
|
||||
FRuler.Free;
|
||||
FSliceOverlays.Free; // не владеет флагами — только список
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.AddSliceOverlay(O: TVfoOverlay);
|
||||
begin
|
||||
if (O <> nil) and (FSliceOverlays.IndexOf(O) < 0) then FSliceOverlays.Add(O);
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.RemoveSliceOverlay(O: TVfoOverlay);
|
||||
begin
|
||||
FSliceOverlays.Remove(O);
|
||||
end;
|
||||
|
||||
function TSpectrumView.SliceOverlayCount: Integer;
|
||||
begin
|
||||
Result := FSliceOverlays.Count;
|
||||
end;
|
||||
|
||||
function TSpectrumView.SliceOverlayAt(Index: Integer): TVfoOverlay;
|
||||
begin
|
||||
Result := TVfoOverlay(FSliceOverlays[Index]);
|
||||
end;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Setters with cascade to sub-views
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -583,6 +617,32 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// Маркер фильтра каждого слайса (B+) на спектре — полупрозрачная полоса
|
||||
// пропускания + края + центральная несущая. Янтарный цвет, как в GL-вьюхе
|
||||
// (SpectrumViewOpengl.DrawSliceFilterMarkers). Рисуется под кривой спектра.
|
||||
procedure TSpectrumView.DrawSliceFilterMarkers(C: TCanvas; W, H: Integer);
|
||||
const
|
||||
SLICE_R = $F0; SLICE_G = $A8; SLICE_B = $10; // янтарный (совпадает с SLICE_CLR в GL)
|
||||
SLICE_CLR = TColor($0010A8F0);
|
||||
var
|
||||
i, SX1, SX2, SVfoX: Integer;
|
||||
O: TVfoOverlay;
|
||||
begin
|
||||
for i := 0 to FSliceOverlays.Count - 1 do
|
||||
begin
|
||||
O := TVfoOverlay(FSliceOverlays[i]);
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
if SX2 > SX1 then BlendBand(SX1, SX2, H, SLICE_R, SLICE_G, SLICE_B, 60);
|
||||
C.Pen.Color := SLICE_CLR; C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||||
C.MoveTo(SX1, 0); C.LineTo(SX1, H);
|
||||
C.MoveTo(SX2, 0); C.LineTo(SX2, H);
|
||||
C.Pen.Width := 2;
|
||||
C.MoveTo(SVfoX, 0); C.LineTo(SVfoX, H - 12);
|
||||
C.Pen.Width := 1;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer);
|
||||
var
|
||||
Y: Integer;
|
||||
@@ -620,15 +680,22 @@ end;
|
||||
|
||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
begin
|
||||
CalcFilterBandXFor(VfoFreq, FMode, FFilterBW, W, X1, X2, VfoX);
|
||||
end;
|
||||
|
||||
// То же, но для произвольного режима/полосы (маркеры слайсов).
|
||||
procedure TSpectrumView.CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||
out X1, X2, VfoX: Integer);
|
||||
var
|
||||
Lo_Hz, Hi_Hz, Half: Double;
|
||||
begin
|
||||
Half := FFilterBW / 2;
|
||||
Half := BW / 2;
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
case FMode of
|
||||
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
||||
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||
case Mode of
|
||||
0: begin Lo_Hz := -BW; Hi_Hz := -100; end;
|
||||
1: begin Lo_Hz := 100; Hi_Hz := BW; end;
|
||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||
end;
|
||||
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||
X2 := Max(0, Min(W - 1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
||||
@@ -737,6 +804,13 @@ begin
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.InvalidateVfoOverlay;
|
||||
begin
|
||||
// База (CPU-путь) перерисовывает флаг по FCacheDirty/FMeterDirty оверлея — здесь
|
||||
// достаточно пометить спектр грязным. GL-подкласс переопределяет.
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.SetTheme(const T: TAppTheme);
|
||||
begin
|
||||
FTheme := T;
|
||||
@@ -905,6 +979,9 @@ begin
|
||||
C.Pen.Style := psSolid;
|
||||
end;
|
||||
|
||||
// Полосы фильтра слайсов (B+) — под кривой спектра, как в GL-вьюхе.
|
||||
DrawSliceFilterMarkers(C, W, H);
|
||||
|
||||
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
||||
if FWDSPReady then
|
||||
begin
|
||||
@@ -1009,6 +1086,9 @@ begin
|
||||
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
||||
if Assigned(FVfoOverlay) then
|
||||
FVfoOverlay.DrawOverlay(FSpectrumBitmap, W, H);
|
||||
// Доп. слайс-флаги (B+) поверх главного.
|
||||
for i := 0 to FSliceOverlays.Count - 1 do
|
||||
TVfoOverlay(FSliceOverlays[i]).DrawOverlay(FSpectrumBitmap, W, H);
|
||||
|
||||
// Восстановление непрозрачности. На Cocoa проверено, что проход не нужен
|
||||
// (Canvas-операции сохраняют альфу $FF), поэтому там его не делаем.
|
||||
|
||||
+112
-7
@@ -43,6 +43,12 @@ type
|
||||
FSampleOverlayTex: TGLTextureCache;
|
||||
FVfoOverlayTex: TGLTextureCache;
|
||||
FBandOverlayTex: TGLTextureCache;
|
||||
// Текстуры флагов слайсов (B+), привязка по указателю оверлея.
|
||||
FSliceTex: array of record
|
||||
Overlay: TVfoOverlay;
|
||||
Tex: TGLTextureCache;
|
||||
LastX, LastY, LastW, LastH: Integer;
|
||||
end;
|
||||
FLastAGCText: string;
|
||||
FLastAGCY: Integer;
|
||||
FLastAGCHangY: Integer;
|
||||
@@ -83,6 +89,9 @@ type
|
||||
procedure DrawBeaconMarkersGL(W, H: Integer);
|
||||
procedure DrawADCOverlay(W, H: Integer);
|
||||
procedure DrawCachedOverlays(W, H: Integer);
|
||||
procedure DrawSliceOverlays(W, H: Integer);
|
||||
procedure DrawSliceFilterMarkers(W, H: Integer);
|
||||
function SliceTexIndex(O: TVfoOverlay): Integer; // индекс в FSliceTex, -1 если нет
|
||||
function FormatFreqGL(Hz: Double): string;
|
||||
function ActiveVfoFrequency: Double;
|
||||
public
|
||||
@@ -96,6 +105,7 @@ type
|
||||
procedure PaintSpectrum(Sender: TObject); override;
|
||||
procedure InvalidateGridCache; override;
|
||||
procedure InvalidateOverlayCache; override;
|
||||
procedure InvalidateVfoOverlay; override;
|
||||
procedure SetTheme(const T: TAppTheme); override;
|
||||
end;
|
||||
|
||||
@@ -138,6 +148,7 @@ begin
|
||||
end;
|
||||
|
||||
destructor TSpectrumViewOpenGL.Destroy;
|
||||
var i: Integer;
|
||||
begin
|
||||
if Assigned(FGLControl) and FGLControl.MakeCurrent then
|
||||
begin
|
||||
@@ -149,7 +160,9 @@ begin
|
||||
DeleteTexture(FSampleOverlayTex);
|
||||
DeleteTexture(FVfoOverlayTex);
|
||||
DeleteTexture(FBandOverlayTex);
|
||||
for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex);
|
||||
end;
|
||||
SetLength(FSliceTex, 0);
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
@@ -392,6 +405,13 @@ begin
|
||||
FVfoOverlayDirty := True;
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.InvalidateVfoOverlay;
|
||||
begin
|
||||
// Только текстура главного флага — НЕ трогаем FOverlayDirty, иначе band/
|
||||
// samplerate/слайсы зря перезаливались бы на каждый тик S-метра главного.
|
||||
FVfoOverlayDirty := True;
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.SetTheme(const T: TAppTheme);
|
||||
begin
|
||||
inherited SetTheme(T);
|
||||
@@ -536,6 +556,29 @@ begin
|
||||
DrawLine(TXVfoX, 0, TXVfoX, H - 12, TColor($002030E0), 2, False);
|
||||
DrawRect(TXVfoX - 5, 0, TXVfoX + 5, 8, TColor($002030E0), 1);
|
||||
end;
|
||||
|
||||
DrawSliceFilterMarkers(W, H);
|
||||
end;
|
||||
|
||||
// Маркер фильтра каждого слайса (B+) на спектре: полупрозрачная полоса
|
||||
// пропускания + центральная несущая. Цвет янтарный — отличить от главного.
|
||||
procedure TSpectrumViewOpenGL.DrawSliceFilterMarkers(W, H: Integer);
|
||||
const
|
||||
SLICE_CLR = TColor($0010A8F0); // янтарный (BGR), как акцент TX-select флага
|
||||
var
|
||||
i, SX1, SX2, SVfoX: Integer;
|
||||
O: TVfoOverlay;
|
||||
begin
|
||||
for i := 0 to SliceOverlayCount - 1 do
|
||||
begin
|
||||
O := SliceOverlayAt(i);
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
if SX2 > SX1 then DrawRect(SX1, 0, SX2, H, SLICE_CLR, 60 / 255);
|
||||
DrawLine(SX1, 0, SX1, H, SLICE_CLR, 1, False);
|
||||
DrawLine(SX2, 0, SX2, H, SLICE_CLR, 1, False);
|
||||
DrawLine(SVfoX, 0, SVfoX, H - 12, SLICE_CLR, 2, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.DrawAGCLines(W, H: Integer; DBmax,
|
||||
@@ -783,16 +826,11 @@ begin
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||
begin
|
||||
if FOverlayDirty or FVfoOverlayDirty or FVfoOverlayTex.Dirty or
|
||||
FVfoOverlay.CacheDirty or FVfoOverlay.MeterDirty or
|
||||
(FLastVfoX <> FVfoOverlay.Left) or (FLastVfoY <> FVfoOverlay.Top) or
|
||||
(FLastVfoW <> FVfoOverlay.Width) or (FLastVfoH <> FVfoOverlay.Height) then
|
||||
begin
|
||||
B := TBitmap.Create;
|
||||
try
|
||||
FVfoOverlay.DrawOverlayBitmap(B);
|
||||
UploadBitmap(FVfoOverlayTex, B, True, VFO_OVERLAY_ALPHA);
|
||||
finally
|
||||
B.Free;
|
||||
end;
|
||||
UploadBitmap(FVfoOverlayTex, FVfoOverlay.EnsureRendered, True, VFO_OVERLAY_ALPHA);
|
||||
FLastVfoX := FVfoOverlay.Left;
|
||||
FLastVfoY := FVfoOverlay.Top;
|
||||
FLastVfoW := FVfoOverlay.Width;
|
||||
@@ -801,9 +839,76 @@ begin
|
||||
end;
|
||||
DrawTexture(FVfoOverlayTex, FVfoOverlay.Left, FVfoOverlay.Top);
|
||||
end;
|
||||
|
||||
DrawSliceOverlays(W, H);
|
||||
FOverlayDirty := False;
|
||||
end;
|
||||
|
||||
function TSpectrumViewOpenGL.SliceTexIndex(O: TVfoOverlay): Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := -1;
|
||||
for i := 0 to High(FSliceTex) do
|
||||
if FSliceTex[i].Overlay = O then Exit(i);
|
||||
end;
|
||||
|
||||
// Флаги слайсов (B+): своя GL-текстура на флаг (привязка по указателю оверлея).
|
||||
// Текстуры «сирот» (флаг удалён) вычищаются здесь же.
|
||||
procedure TSpectrumViewOpenGL.DrawSliceOverlays(W, H: Integer);
|
||||
var
|
||||
n, i, ti, j: Integer;
|
||||
O: TVfoOverlay;
|
||||
Alive: Boolean;
|
||||
begin
|
||||
// 1. Убрать текстуры флагов, которых уже нет в списке.
|
||||
i := 0;
|
||||
while i <= High(FSliceTex) do
|
||||
begin
|
||||
Alive := False;
|
||||
for j := 0 to SliceOverlayCount - 1 do
|
||||
if SliceOverlayAt(j) = FSliceTex[i].Overlay then begin Alive := True; Break; end;
|
||||
if not Alive then
|
||||
begin
|
||||
DeleteTexture(FSliceTex[i].Tex);
|
||||
for j := i to High(FSliceTex) - 1 do FSliceTex[j] := FSliceTex[j + 1];
|
||||
SetLength(FSliceTex, Length(FSliceTex) - 1);
|
||||
end
|
||||
else
|
||||
Inc(i);
|
||||
end;
|
||||
|
||||
// 2. Нарисовать каждый видимый флаг, при нужде (пере)залив текстуру.
|
||||
n := SliceOverlayCount;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
O := SliceOverlayAt(i);
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
ti := SliceTexIndex(O);
|
||||
if ti < 0 then
|
||||
begin
|
||||
SetLength(FSliceTex, Length(FSliceTex) + 1);
|
||||
ti := High(FSliceTex);
|
||||
FSliceTex[ti].Overlay := O;
|
||||
FSliceTex[ti].Tex.Dirty := True;
|
||||
FSliceTex[ti].LastX := MaxInt; // форсируем первую заливку
|
||||
end;
|
||||
// Перезаливаем текстуру слайса ТОЛЬКО когда изменился он сам (CacheDirty:
|
||||
// режим/громкость, MeterDirty: S-метр) или его позиция — не по глобальному
|
||||
// FOverlayDirty. При MeterDirty EnsureRendered перерисует лишь полоску метра.
|
||||
if O.CacheDirty or O.MeterDirty or FSliceTex[ti].Tex.Dirty or
|
||||
(FSliceTex[ti].LastX <> O.Left) or (FSliceTex[ti].LastY <> O.Top) or
|
||||
(FSliceTex[ti].LastW <> O.Width) or (FSliceTex[ti].LastH <> O.Height) then
|
||||
begin
|
||||
UploadBitmap(FSliceTex[ti].Tex, O.EnsureRendered, True, VFO_OVERLAY_ALPHA);
|
||||
FSliceTex[ti].LastX := O.Left;
|
||||
FSliceTex[ti].LastY := O.Top;
|
||||
FSliceTex[ti].LastW := O.Width;
|
||||
FSliceTex[ti].LastH := O.Height;
|
||||
end;
|
||||
DrawTexture(FSliceTex[ti].Tex, O.Left, O.Top);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.PaintSpectrum(Sender: TObject);
|
||||
var
|
||||
W, H: Integer;
|
||||
|
||||
+126
-7
@@ -52,6 +52,7 @@ type
|
||||
FTx: Boolean; // передача идёт (красный)
|
||||
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
|
||||
FSlice: Char; // буква слайса ('A')
|
||||
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
|
||||
|
||||
FDevNames: TStringList; // имена устройств вывода
|
||||
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
|
||||
@@ -72,12 +73,21 @@ type
|
||||
FOnTxSelect: TNotifyEvent;
|
||||
FOnInvalidate: TNotifyEvent;
|
||||
|
||||
FSliceId: Integer; // логический id слайса (владелец — MainForm/контроллер)
|
||||
FIsMain: Boolean; // главный приёмник (слайс A) — без кнопки закрытия
|
||||
FOnSliceSelect: TNotifyEvent; // клик по бейджу буквы — сделать слайс активным
|
||||
FOnClose: TNotifyEvent; // клик по × (только неглавные) — удалить слайс
|
||||
|
||||
FHitRects: array[0..63] of TRect;
|
||||
FHitActions: array[0..63] of Integer;
|
||||
FHitCount: Integer;
|
||||
FHotIdx: Integer;
|
||||
FCacheBitmap: TBitmap;
|
||||
FCacheDirty: Boolean;
|
||||
FMeterDirty: Boolean; // изменился только S-метр — перерисовать полоску, не весь флаг
|
||||
FMeterRect: TRect; // прямоугольник сигнал-бара (запоминается при полном рендере)
|
||||
FSRowTop: Integer; // верх строки частоты (для S-метки справа)
|
||||
FLastSTextRect: TRect; // последний прямоугольник S-метки (для аккуратной очистки)
|
||||
|
||||
procedure DrawSelf(C: TCanvas; W, H: Integer);
|
||||
procedure DrawButton(C: TCanvas; const R: TRect; const Lbl: string;
|
||||
@@ -100,6 +110,8 @@ type
|
||||
procedure SetPanel(P: TFlyPanel);
|
||||
procedure ApplyVolFromX(LX: Integer);
|
||||
procedure RequestInvalidate;
|
||||
procedure RequestMeterInvalidate;
|
||||
procedure RefreshMeter(C: TCanvas);
|
||||
procedure DoHit(HV: Integer);
|
||||
|
||||
protected
|
||||
@@ -114,6 +126,10 @@ type
|
||||
destructor Destroy; override;
|
||||
procedure DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
procedure DrawOverlayBitmap(Target: TBitmap);
|
||||
// Готовит внутренний кэш-битмап (полный рендер по CacheDirty либо дешёвая
|
||||
// перерисовка одной полоски метра по MeterDirty) и возвращает его. Один путь
|
||||
// для CPU (BlendBitmapKey) и GL (UploadBitmap).
|
||||
function EnsureRendered: TBitmap;
|
||||
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
||||
function HandleMouseMove(X, Y: Integer): Boolean;
|
||||
function HandleMouseUp(X, Y: Integer): Boolean;
|
||||
@@ -126,6 +142,20 @@ type
|
||||
const CurName: string);
|
||||
procedure UpdateSMeter(DB: Double);
|
||||
procedure UpdateVfo(AVfoHz: Double);
|
||||
// Привязка к слайсу: id, буква флага, признак главного (слайс A).
|
||||
procedure SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
|
||||
property SliceId: Integer read FSliceId;
|
||||
property IsMain: Boolean read FIsMain;
|
||||
// Для маркера фильтра слайса на спектре (читает GL-вьюха).
|
||||
property DisplayVfoHz: Double read FVfoHz;
|
||||
property DisplayMode: Integer read FMode;
|
||||
property DisplayBW: Integer read FFilterBW;
|
||||
// Битмап-кэш требует перерисовки (GL-вьюха решает, перезаливать ли текстуру).
|
||||
property CacheDirty: Boolean read FCacheDirty;
|
||||
// Изменился только S-метр (перезалить текстуру, но рендер дешёвый — одна полоска).
|
||||
property MeterDirty: Boolean read FMeterDirty;
|
||||
// Порог изменения S-метра (дБ) для перерисовки: у слайсов выше (меньше рендеров).
|
||||
property SMeterThreshold: Double read FSMeterThreshold write FSMeterThreshold;
|
||||
property HotIdx: Integer read FHotIdx;
|
||||
property BaseHeight: Integer read ComputeHeight;
|
||||
property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect;
|
||||
@@ -136,6 +166,8 @@ type
|
||||
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
||||
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
||||
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
||||
property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect;
|
||||
property OnClose: TNotifyEvent read FOnClose write FOnClose;
|
||||
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
|
||||
end;
|
||||
|
||||
@@ -162,6 +194,8 @@ const
|
||||
HIT_TX = -26;
|
||||
HIT_AUD_UP = -27;
|
||||
HIT_AUD_DN = -28;
|
||||
HIT_SLICE_SEL = -29; // клик по бейджу буквы (выбрать слайс активным)
|
||||
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
|
||||
HIT_AGC_PICK_BASE = 100000;
|
||||
HIT_DEV_BASE = 200000;
|
||||
|
||||
@@ -338,6 +372,8 @@ begin
|
||||
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
|
||||
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
|
||||
FTxSel := True; FSlice := 'A';
|
||||
FSliceId := 0; FIsMain := True; // по умолчанию — главный приёмник (слайс A)
|
||||
FSMeterThreshold := 0.4; // главный флаг — чувствительный метр
|
||||
FPanel := fpNone;
|
||||
FAudScroll := 0;
|
||||
FVolDrag := False;
|
||||
@@ -346,6 +382,8 @@ begin
|
||||
FCacheBitmap := TBitmap.Create;
|
||||
FCacheBitmap.PixelFormat := pf32bit;
|
||||
FCacheDirty := True;
|
||||
FMeterDirty := False;
|
||||
FLastSTextRect := Rect(0, 0, 0, 0);
|
||||
Width := OVL_W;
|
||||
Height := OVL_BASE_H;
|
||||
Cursor := crHandPoint;
|
||||
@@ -373,6 +411,53 @@ begin
|
||||
if Assigned(FOnInvalidate) then FOnInvalidate(Self);
|
||||
end;
|
||||
|
||||
// Лёгкая инвалидация: изменился только S-метр. НЕ трогает FCacheDirty, поэтому
|
||||
// хром флага (частота/режим/кнопки) не перерисовывается — перезаливается только
|
||||
// полоска метра (RefreshMeter в EnsureRendered).
|
||||
procedure TVfoOverlay.RequestMeterInvalidate;
|
||||
begin
|
||||
FMeterDirty := True;
|
||||
inherited Invalidate;
|
||||
if Assigned(FOnInvalidate) then FOnInvalidate(Self);
|
||||
end;
|
||||
|
||||
// Дешёвая перерисовка одной полоски метра + S-метки поверх уже отрендеренного
|
||||
// хрома. Обе зоны самодостаточны и целиком очищаются под фон карточки.
|
||||
procedure TVfoOverlay.RefreshMeter(C: TCanvas);
|
||||
var
|
||||
SStr: string;
|
||||
STextW: Integer;
|
||||
ClearR, NewR: TRect;
|
||||
begin
|
||||
// Сигнал-бар — самодостаточный прямоугольник, перерисовывается целиком.
|
||||
C.Brush.Color := CLR_CARD_BG;
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Pen.Style := psClear;
|
||||
C.FillRect(FMeterRect);
|
||||
DrawMeterBar(C, FMeterRect.Left, FMeterRect.Top,
|
||||
FMeterRect.Right - FMeterRect.Left, MET_H);
|
||||
|
||||
// S-метка в строке частоты (справа). Очищаем объединение старого и нового
|
||||
// прямоугольника, чтобы не оставалось «хвостов» при сужении текста.
|
||||
SStr := DBmToSLabel(FSMeterDB);
|
||||
C.Font.Name := 'Courier New';
|
||||
C.Font.Style := [fsBold];
|
||||
C.Font.Size := 9;
|
||||
STextW := C.TextWidth(SStr);
|
||||
NewR := Rect(FMeterRect.Right - STextW - 3, FSRowTop + 4, FMeterRect.Right, FSRowTop + FRQ_H);
|
||||
ClearR := NewR;
|
||||
if FLastSTextRect.Left < ClearR.Left then ClearR.Left := FLastSTextRect.Left;
|
||||
if FLastSTextRect.Top < ClearR.Top then ClearR.Top := FLastSTextRect.Top;
|
||||
C.Brush.Color := CLR_CARD_BG;
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Pen.Style := psClear;
|
||||
C.FillRect(ClearR);
|
||||
C.Font.Color := CLR_SVAL;
|
||||
C.Brush.Style := bsClear;
|
||||
C.TextOut(FMeterRect.Right - STextW - 2, FSRowTop + 6, SStr);
|
||||
FLastSTextRect := NewR;
|
||||
end;
|
||||
|
||||
function TVfoOverlay.FormatFreq(Hz: Double): string;
|
||||
var
|
||||
T, iM, iK, iH: Int64;
|
||||
@@ -800,8 +885,16 @@ begin
|
||||
RW := W - PAD*2;
|
||||
CurY := PAD;
|
||||
|
||||
{ --- Шапка: [A] BW ... SPLIT [TX] --- }
|
||||
{ --- Шапка: [A] BW ... [×] SPLIT [TX] --- }
|
||||
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, CLR_ACCENT, CLR_CARD_BG);
|
||||
RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
|
||||
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
|
||||
if not FIsMain then
|
||||
begin
|
||||
R := Rect(W - PAD - 26 - 44 - 6 - 14, CurY, W - PAD - 26 - 44 - 6, CurY + HDR_BADGE_H);
|
||||
DrawBadge(C, R, #$C3#$97, CLR_BTN_NORM, CLR_DIM); // UTF-8 '×'
|
||||
RegisterHit(R, HIT_CLOSE);
|
||||
end;
|
||||
// ширина фильтра
|
||||
if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0])
|
||||
else BWStr := IntToStr(FFilterBW);
|
||||
@@ -828,10 +921,12 @@ begin
|
||||
DrawIndicator(C, X + 48, CurY + 2, 'ANF', FANFOn);
|
||||
DrawIndicator(C, X + 80, CurY + 2, 'SNB', FSNBOn);
|
||||
MeterX := X + 112;
|
||||
FMeterRect := Rect(MeterX, CurY, W - PAD, CurY + MET_H); // для RefreshMeter
|
||||
DrawMeterBar(C, MeterX, CurY, W - PAD - MeterX, MET_H);
|
||||
Inc(CurY, MET_H);
|
||||
|
||||
{ --- Частота + S --- }
|
||||
FSRowTop := CurY; // для RefreshMeter
|
||||
FreqStr := FormatFreq(FVfoHz);
|
||||
C.Font.Name := 'Courier New'; C.Font.Size := 13; C.Font.Style := [fsBold];
|
||||
C.Font.Color := CLR_FREQ; C.Brush.Style := bsClear;
|
||||
@@ -839,6 +934,7 @@ begin
|
||||
SStr := DBmToSLabel(FSMeterDB);
|
||||
C.Font.Size := 9; C.Font.Color := CLR_SVAL;
|
||||
C.TextOut(W - PAD - C.TextWidth(SStr) - 2, CurY + 6, SStr);
|
||||
FLastSTextRect := Rect(W - PAD - C.TextWidth(SStr) - 3, CurY + 4, W - PAD, CurY + FRQ_H);
|
||||
Inc(CurY, FRQ_H);
|
||||
|
||||
{ --- Нижний бар: 🔊 vol | MODE DSP AUD --- }
|
||||
@@ -899,11 +995,8 @@ begin
|
||||
DrawSelf(Canvas, Width, Height);
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
function TVfoOverlay.EnsureRendered: TBitmap;
|
||||
begin
|
||||
if (not Visible) or (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
|
||||
if (Left >= W) or (Top >= H) then Exit;
|
||||
|
||||
if (FCacheBitmap.Width <> Width) or (FCacheBitmap.Height <> Height) then
|
||||
begin
|
||||
FCacheBitmap.SetSize(Width, Height);
|
||||
@@ -917,7 +1010,21 @@ begin
|
||||
FCacheBitmap.Canvas.FillRect(Rect(0, 0, Width, Height));
|
||||
DrawSelf(FCacheBitmap.Canvas, Width, Height);
|
||||
FCacheDirty := False;
|
||||
FMeterDirty := False;
|
||||
end
|
||||
else if FMeterDirty then
|
||||
begin
|
||||
RefreshMeter(FCacheBitmap.Canvas); // дешёвый апдейт: только полоска метра
|
||||
FMeterDirty := False;
|
||||
end;
|
||||
Result := FCacheBitmap;
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
begin
|
||||
if (not Visible) or (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
|
||||
if (Left >= W) or (Top >= H) then Exit;
|
||||
EnsureRendered;
|
||||
BlendBitmapKey(Target, FCacheBitmap, Left, Top, VFO_OVERLAY_ALPHA);
|
||||
end;
|
||||
|
||||
@@ -932,6 +1039,7 @@ begin
|
||||
Target.Canvas.FillRect(Rect(0, 0, Width, Height));
|
||||
DrawSelf(Target.Canvas, Width, Height);
|
||||
FCacheDirty := False;
|
||||
FMeterDirty := False;
|
||||
end;
|
||||
|
||||
{ ---- Обработка хитов ---- }
|
||||
@@ -949,6 +1057,8 @@ begin
|
||||
if Assigned(FOnMute) then FOnMute(Self); Exit; end;
|
||||
HIT_SPLIT: begin if Assigned(FOnSplit) then FOnSplit(Self); Exit; end;
|
||||
HIT_TX: begin if Assigned(FOnTxSelect) then FOnTxSelect(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);
|
||||
RequestInvalidate; Exit; end;
|
||||
HIT_AUD_DN: begin if FAudScroll < FDevNames.Count - AUD_MAX_VIS then
|
||||
@@ -1174,6 +1284,15 @@ begin
|
||||
RequestInvalidate;
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
|
||||
begin
|
||||
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;
|
||||
FSliceId := ASliceId;
|
||||
FSlice := ALetter;
|
||||
FIsMain := AIsMain;
|
||||
RequestInvalidate;
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.SetAudioDevices(Names: TStrings; const Indices: array of Integer;
|
||||
const CurName: string);
|
||||
var
|
||||
@@ -1190,10 +1309,10 @@ end;
|
||||
|
||||
procedure TVfoOverlay.UpdateSMeter(DB: Double);
|
||||
begin
|
||||
if Abs(DB - FSMeterDB) > 0.4 then
|
||||
if Abs(DB - FSMeterDB) > FSMeterThreshold then
|
||||
begin
|
||||
FSMeterDB := DB;
|
||||
RequestInvalidate;
|
||||
RequestMeterInvalidate; // только полоска метра, без ре-рендера всего флага
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
+517
-81
@@ -96,6 +96,10 @@ const
|
||||
RXA_CHAN = 0;
|
||||
TXA_CHAN = 1; // используем разные каналы для RX и TX
|
||||
|
||||
// --- Мультислайсы (софт-fan-out) ---
|
||||
MAX_SLICES = 6; // потолок одновременных софтовых слайсов
|
||||
SLICE_CHAN_BASE = 2; // WDSP-каналы слайсов: 0=RXA, 1=TXA, слайсы с 2+
|
||||
|
||||
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||
DSP_BUFSIZE = 1024;
|
||||
@@ -135,6 +139,10 @@ type
|
||||
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
||||
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
||||
Count: Integer) of object;
|
||||
// Аудио готового слайса — вызывается из DSP-потока по каждому активному слайсу.
|
||||
// SliceId — логический id (из RadioController), не WDSP-канал.
|
||||
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
|
||||
Count: Integer) of object;
|
||||
// Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком,
|
||||
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
|
||||
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
|
||||
@@ -143,6 +151,28 @@ type
|
||||
// TX mic source: откуда получаем сэмплы для модулятора.
|
||||
TTXMicSource = (txmsRadio, txmsSoundCard, txmsWeb);
|
||||
|
||||
// Софтовый слайс: отдельный WDSP-канал, кормится тем же IQ-блоком, что и
|
||||
// RXA_CHAN, но со своим NCO-сдвигом/режимом/фильтром/AGC/громкостью и своим
|
||||
// аудио-callback'ом. Геометрия канала = как у RXA_CHAN.
|
||||
TSliceChan = record
|
||||
Active: Boolean; // слот занят
|
||||
Id: Integer; // логический id слайса
|
||||
Chan: Integer; // WDSP-канал (SLICE_CHAN_BASE + слот)
|
||||
ShiftHz: Double; // NCO-сдвиг = TargetHz - CapturedCenterHz
|
||||
Mode: Integer; // индекс режима (MODE_*)
|
||||
FilterLo: Integer;
|
||||
FilterHi: Integer;
|
||||
AGCMode: TWDSPAGCMode;
|
||||
Volume: Double; // 0..1
|
||||
Muted: Boolean;
|
||||
NRMode: Integer; // 0=off,1..4
|
||||
NBMode: Integer; // 0=off,1=NB,2=NB2
|
||||
SNBOn: Boolean;
|
||||
ANFOn: Boolean;
|
||||
SMeter: Double; // последнее показание S-метра канала (dBm), пишет DSP-поток
|
||||
Opened: Boolean; // WDSP-канал реально открыт (после Open движка)
|
||||
end;
|
||||
|
||||
{ TWDSPEngine }
|
||||
TWDSPEngine = class
|
||||
private
|
||||
@@ -219,6 +249,15 @@ type
|
||||
FOnWaterfall: TOnWaterfallReady;
|
||||
FOnTXIQ: TOnTXIQReady;
|
||||
|
||||
// --- Мультислайсы ---
|
||||
FSlices: array[0..MAX_SLICES-1] of TSliceChan;
|
||||
FSliceLock: TCriticalSection; // сериализует add/remove/set vs DSP-обработку
|
||||
FSliceIn: array of Double; // interleaved вход fexchange0 (переиспользуем)
|
||||
FSliceOut: array of Double; // interleaved выход fexchange0
|
||||
FSliceOutL: array of Single; // деинтерливленный L для callback
|
||||
FSliceOutR: array of Single; // R
|
||||
FOnSliceAudio: TOnSliceAudio;
|
||||
|
||||
// TX mic ring buffer (lock-free: один writer — receive thread,
|
||||
// один reader — TX DSP thread)
|
||||
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
|
||||
@@ -311,6 +350,13 @@ type
|
||||
|
||||
function ModeToWDSP(Mode: Integer): Integer;
|
||||
procedure ApplyDefaultFilter;
|
||||
// --- Мультислайсы ---
|
||||
function FindSliceIdx(Id: Integer): Integer; // -1 если нет
|
||||
procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки
|
||||
procedure CloseSliceChannel(Idx: Integer); // CloseChannel (запись хранит)
|
||||
procedure ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; FixedGain: Double);
|
||||
procedure ProcessSlices; // fan-out в ProcessRXBlock
|
||||
procedure AllocSliceBuffers; // (пере)аллокация буферов
|
||||
procedure FeedDisplaySample(const I, Q: Double);
|
||||
procedure ProcessRXBlock;
|
||||
procedure PushIQItemToDSP(const Item: TIQQueueItem);
|
||||
@@ -329,6 +375,10 @@ type
|
||||
procedure ApplyNBState;
|
||||
procedure ApplySNBState;
|
||||
procedure ApplyANFState;
|
||||
procedure ApplyNRToChan(Chan, NRMode: Integer);
|
||||
procedure ApplyNBToChan(Chan, NBMode: Integer);
|
||||
procedure ApplySNBToChan(Chan: Integer; On_: Boolean);
|
||||
procedure ApplyANFToChan(Chan: Integer; On_: Boolean);
|
||||
procedure ApplyNoiseFilterState;
|
||||
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
||||
procedure FeedTXDisplaySample(const I, Q: Double);
|
||||
@@ -350,6 +400,26 @@ type
|
||||
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
|
||||
procedure SetBeaconDecoder(D: TBeaconDecoder);
|
||||
|
||||
// --- Мультислайсы (софт-fan-out) ---
|
||||
// Добавляет слайс с логическим Id (уникальным). ShiftHz = TargetHz - центр
|
||||
// захвата. Возвращает False если нет свободных слотов или Id уже есть.
|
||||
function AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
|
||||
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean;
|
||||
procedure RemoveSlice(Id: Integer);
|
||||
procedure SetSliceShift(Id: Integer; ShiftHz: Double);
|
||||
procedure SetSliceMode(Id, Mode: Integer);
|
||||
procedure SetSliceFilter(Id, Low, High: Integer);
|
||||
procedure SetSliceAGC(Id: Integer; Mode: TWDSPAGCMode);
|
||||
procedure SetSliceVolume(Id: Integer; Vol: Double);
|
||||
procedure SetSliceMute(Id: Integer; Mute: Boolean);
|
||||
procedure SetSliceNRMode(Id, Mode: Integer);
|
||||
procedure SetSliceNBMode(Id, Mode: Integer);
|
||||
procedure SetSliceSNB(Id: Integer; On_: Boolean);
|
||||
procedure SetSliceANF(Id: Integer; On_: Boolean);
|
||||
function GetSliceSMeter(Id: Integer): Double;
|
||||
function SliceCount: Integer;
|
||||
function HasSlice(Id: Integer): Boolean;
|
||||
|
||||
// Подача 24-bit big-endian IQ из DDC пакета
|
||||
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
|
||||
procedure PushDDCPacket(const Buf: array of Byte;
|
||||
@@ -475,6 +545,7 @@ type
|
||||
property WfAvgMode: Integer read FWfAvgMode;
|
||||
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
||||
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
||||
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
||||
@@ -813,6 +884,12 @@ begin
|
||||
FDSPThread := nil;
|
||||
FDisplayThread := nil;
|
||||
|
||||
// Мультислайсы
|
||||
FSliceLock := TCriticalSection.Create;
|
||||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||
FOnSliceAudio := nil;
|
||||
AllocSliceBuffers;
|
||||
|
||||
// TX инициализация
|
||||
FTXMicHead := 0;
|
||||
FTXMicTail := 0;
|
||||
@@ -887,6 +964,7 @@ begin
|
||||
Close;
|
||||
RTLEventDestroy(FQueueSem);
|
||||
RTLEventDestroy(FTXMicSem);
|
||||
FSliceLock.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
@@ -1052,6 +1130,7 @@ end;
|
||||
function TWDSPEngine.Open: Boolean;
|
||||
var
|
||||
DllPath: string;
|
||||
i: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
FLastError := '';
|
||||
@@ -1131,6 +1210,18 @@ begin
|
||||
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
||||
ApplyTXChainSettings;
|
||||
ApplyNoiseFilterState;
|
||||
|
||||
// Переоткрываем WDSP-каналы активных слайсов (например после ChangeSampleRate,
|
||||
// где Close закрыл их, но записи FSlices сохранились).
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and not FSlices[i].Opened then
|
||||
OpenSliceChannel(i);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
|
||||
Result := True;
|
||||
|
||||
// Запускаем DSP поток после успешной инициализации WDSP
|
||||
@@ -1149,6 +1240,8 @@ begin
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.Close;
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
|
||||
@@ -1161,6 +1254,15 @@ begin
|
||||
FreeAndNil(FDSPThread);
|
||||
end;
|
||||
|
||||
// Закрываем WDSP-каналы слайсов (записи FSlices сохраняем — переоткроются в Open).
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Opened then CloseSliceChannel(i);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
|
||||
CloseAnalyzer;
|
||||
destroy_anbEXT(RXA_CHAN);
|
||||
destroy_nobEXT(RXA_CHAN);
|
||||
@@ -1195,6 +1297,7 @@ begin
|
||||
SetLength(FRXAccQ, FBufSize);
|
||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
FDispPos := 0;
|
||||
AllocSliceBuffers;
|
||||
|
||||
// Переинициализируем WDSP с новым rate
|
||||
Open;
|
||||
@@ -1379,11 +1482,360 @@ begin
|
||||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||||
end;
|
||||
|
||||
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
|
||||
ProcessSlices;
|
||||
|
||||
except
|
||||
// Защита от падения libwdsp в потоке — просто пропускаем блок
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Мультислайсы (софт-fan-out)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TWDSPEngine.AllocSliceBuffers;
|
||||
begin
|
||||
SetLength(FSliceIn, FBufSize * 2);
|
||||
SetLength(FSliceOut, FBufSize * 2); // с запасом (>= FAudioBufSize*2)
|
||||
SetLength(FSliceOutL, FAudioBufSize);
|
||||
SetLength(FSliceOutR, FAudioBufSize);
|
||||
end;
|
||||
|
||||
function TWDSPEngine.FindSliceIdx(Id: Integer): Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := -1;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
|
||||
end;
|
||||
|
||||
// Применяет AGC к произвольному каналу — вынесено из SetAGC чтобы делить с слайсами.
|
||||
procedure TWDSPEngine.ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode;
|
||||
FixedGain: Double);
|
||||
begin
|
||||
SetRXAAGCMode(Chan, Ord(Mode));
|
||||
SetRXAAGCSlope(Chan, FAGCSlope);
|
||||
SetRXAAGCTop(Chan, FAGCTop);
|
||||
case Mode of
|
||||
agcOff:
|
||||
SetRXAAGCFixed(Chan, FixedGain);
|
||||
agcLong: begin
|
||||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 2000);
|
||||
SetRXAAGCDecay(Chan, 2000); SetRXAAGCHangThreshold(Chan, FAGCHangThreshold);
|
||||
end;
|
||||
agcSlow: begin
|
||||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 1000);
|
||||
SetRXAAGCDecay(Chan, 500); SetRXAAGCHangThreshold(Chan, FAGCHangThreshold);
|
||||
end;
|
||||
agcMedium: begin
|
||||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 0);
|
||||
SetRXAAGCDecay(Chan, 250); SetRXAAGCHangThreshold(Chan, 100);
|
||||
end;
|
||||
agcFast: begin
|
||||
SetRXAAGCAttack(Chan, 2); SetRXAAGCHang(Chan, 0);
|
||||
SetRXAAGCDecay(Chan, 50); SetRXAAGCHangThreshold(Chan, 100);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
|
||||
// под FSliceLock и только при FInitialized.
|
||||
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
|
||||
var Chan: Integer;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
if FSlices[Idx].Opened then Exit;
|
||||
Chan := FSlices[Idx].Chan;
|
||||
OpenChannel(Chan, FBufSize, FAudioBufSize, FSampleRate, FAudioRate, FAudioRate,
|
||||
0, 0, 0.010, 0.010, 0.010, 0.010, 0);
|
||||
// NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open.
|
||||
create_anbEXT(Chan, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||||
create_nobEXT(Chan, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||||
SetRXABandpassWindow(Chan, 1);
|
||||
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
|
||||
SetRXAShiftRun(Chan, 1);
|
||||
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz);
|
||||
RXASetPassband(Chan, FSlices[Idx].FilterLo, FSlices[Idx].FilterHi);
|
||||
ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0);
|
||||
// NR/NB/SNB/ANF из сохранённого состояния слайса.
|
||||
ApplyNRToChan(Chan, FSlices[Idx].NRMode);
|
||||
ApplyNBToChan(Chan, FSlices[Idx].NBMode);
|
||||
ApplySNBToChan(Chan, FSlices[Idx].SNBOn);
|
||||
ApplyANFToChan(Chan, FSlices[Idx].ANFOn);
|
||||
SetRXAPanelGain1(Chan, FSlices[Idx].Volume);
|
||||
SetRXAPanelSelect(Chan, 3);
|
||||
SetRXAPanelRun(Chan, 1);
|
||||
SetChannelState(Chan, 1, 0);
|
||||
FSlices[Idx].Opened := True;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.CloseSliceChannel(Idx: Integer);
|
||||
begin
|
||||
if not FSlices[Idx].Opened then Exit;
|
||||
SetChannelState(FSlices[Idx].Chan, 0, 1);
|
||||
destroy_anbEXT(FSlices[Idx].Chan);
|
||||
destroy_nobEXT(FSlices[Idx].Chan);
|
||||
CloseChannel(FSlices[Idx].Chan);
|
||||
FSlices[Idx].Opened := False;
|
||||
end;
|
||||
|
||||
// Fan-out: прогоняет тот же входной IQ-блок через каждый активный слайс-канал.
|
||||
// Вызывается из ProcessRXBlock (DSP-поток) после основного канала.
|
||||
procedure TWDSPEngine.ProcessSlices;
|
||||
var
|
||||
s, i, k, Err: Integer;
|
||||
V: Double;
|
||||
begin
|
||||
if not Assigned(FOnSliceAudio) then Exit;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for s := 0 to MAX_SLICES - 1 do
|
||||
begin
|
||||
if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
|
||||
if FSlices[s].Muted then Continue;
|
||||
// Свежая копия входного IQ — fexchange0 портит вход in-place.
|
||||
for i := 0 to FBufSize - 1 do
|
||||
begin
|
||||
FSliceIn[i * 2] := FRXAccI[i];
|
||||
FSliceIn[i * 2 + 1] := FRXAccQ[i];
|
||||
end;
|
||||
Err := 0;
|
||||
fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err);
|
||||
// S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор.
|
||||
FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV);
|
||||
V := FSlices[s].Volume;
|
||||
for k := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
FSliceOutL[k] := FSliceOut[k * 2] * V;
|
||||
FSliceOutR[k] := FSliceOut[k * 2 + 1] * V;
|
||||
end;
|
||||
FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize);
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
|
||||
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean;
|
||||
var i, slot: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
if FindSliceIdx(Id) >= 0 then Exit; // уже существует
|
||||
slot := -1;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if not FSlices[i].Active then begin slot := i; Break; end;
|
||||
if slot < 0 then Exit; // нет свободных слотов
|
||||
FSlices[slot].Active := True;
|
||||
FSlices[slot].Id := Id;
|
||||
FSlices[slot].Chan := SLICE_CHAN_BASE + slot;
|
||||
FSlices[slot].Mode := Mode;
|
||||
FSlices[slot].FilterLo := FilterLo;
|
||||
FSlices[slot].FilterHi := FilterHi;
|
||||
FSlices[slot].ShiftHz := ShiftHz;
|
||||
FSlices[slot].Volume := Volume;
|
||||
FSlices[slot].AGCMode := AGC;
|
||||
FSlices[slot].Muted := False;
|
||||
FSlices[slot].NRMode := NR_MODE_OFF;
|
||||
FSlices[slot].NBMode := NB_MODE_OFF;
|
||||
FSlices[slot].SNBOn := False;
|
||||
FSlices[slot].ANFOn := False;
|
||||
FSlices[slot].SMeter := -130.0;
|
||||
FSlices[slot].Opened := False;
|
||||
if FInitialized then OpenSliceChannel(slot);
|
||||
Result := True;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.RemoveSlice(Id: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id);
|
||||
if idx < 0 then Exit;
|
||||
CloseSliceChannel(idx);
|
||||
FSlices[idx].Active := False;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceShift(Id: Integer; ShiftHz: Double);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].ShiftHz := ShiftHz;
|
||||
if FSlices[idx].Opened then SetRXAShiftFreq(FSlices[idx].Chan, ShiftHz);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceMode(Id, Mode: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].Mode := Mode;
|
||||
if FSlices[idx].Opened then SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode));
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceFilter(Id, Low, High: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].FilterLo := Low;
|
||||
FSlices[idx].FilterHi := High;
|
||||
if FSlices[idx].Opened then RXASetPassband(FSlices[idx].Chan, Low, High);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceAGC(Id: Integer; Mode: TWDSPAGCMode);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].AGCMode := Mode;
|
||||
if FSlices[idx].Opened then ApplyAGCToChan(FSlices[idx].Chan, Mode, 50.0);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceVolume(Id: Integer; Vol: Double);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].Volume := Vol;
|
||||
if FSlices[idx].Opened then SetRXAPanelGain1(FSlices[idx].Chan, Vol);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceMute(Id: Integer; Mute: Boolean);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].Muted := Mute;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceNRMode(Id, Mode: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
if Mode < NR_MODE_OFF then Mode := NR_MODE_OFF;
|
||||
if Mode > NR_MODE_4 then Mode := NR_MODE_4;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].NRMode := Mode;
|
||||
if FSlices[idx].Opened then ApplyNRToChan(FSlices[idx].Chan, Mode);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceNBMode(Id, Mode: Integer);
|
||||
var idx: Integer;
|
||||
begin
|
||||
if Mode < NB_MODE_OFF then Mode := NB_MODE_OFF;
|
||||
if Mode > NB_MODE_2 then Mode := NB_MODE_2;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].NBMode := Mode;
|
||||
if FSlices[idx].Opened then ApplyNBToChan(FSlices[idx].Chan, Mode);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceSNB(Id: Integer; On_: Boolean);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].SNBOn := On_;
|
||||
if FSlices[idx].Opened then ApplySNBToChan(FSlices[idx].Chan, On_);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceANF(Id: Integer; On_: Boolean);
|
||||
var idx: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].ANFOn := On_;
|
||||
if FSlices[idx].Opened then ApplyANFToChan(FSlices[idx].Chan, On_);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.GetSliceSMeter(Id: Integer): Double;
|
||||
var idx: Integer;
|
||||
begin
|
||||
Result := -130.0;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id);
|
||||
if idx >= 0 then Result := FSlices[idx].SMeter;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.SliceCount: Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active then Inc(Result);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.HasSlice(Id: Integer): Boolean;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
Result := FindSliceIdx(Id) >= 0;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RX управление
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1412,42 +1864,7 @@ begin
|
||||
if not FInitialized then Exit;
|
||||
|
||||
// Точно по piHPSDR receiver.c: set_agc()
|
||||
SetRXAAGCMode(RXA_CHAN, Ord(Mode));
|
||||
SetRXAAGCSlope(RXA_CHAN, FAGCSlope);
|
||||
SetRXAAGCTop(RXA_CHAN, FAGCTop);
|
||||
|
||||
case Mode of
|
||||
agcOff:
|
||||
SetRXAAGCFixed(RXA_CHAN, FixedGain);
|
||||
|
||||
agcLong: begin
|
||||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||||
SetRXAAGCHang(RXA_CHAN, 2000);
|
||||
SetRXAAGCDecay(RXA_CHAN, 2000);
|
||||
SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold);
|
||||
end;
|
||||
|
||||
agcSlow: begin
|
||||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||||
SetRXAAGCHang(RXA_CHAN, 1000);
|
||||
SetRXAAGCDecay(RXA_CHAN, 500);
|
||||
SetRXAAGCHangThreshold(RXA_CHAN, FAGCHangThreshold);
|
||||
end;
|
||||
|
||||
agcMedium: begin
|
||||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||||
SetRXAAGCHang(RXA_CHAN, 0);
|
||||
SetRXAAGCDecay(RXA_CHAN, 250);
|
||||
SetRXAAGCHangThreshold(RXA_CHAN, 100);
|
||||
end;
|
||||
|
||||
agcFast: begin
|
||||
SetRXAAGCAttack(RXA_CHAN, 2);
|
||||
SetRXAAGCHang(RXA_CHAN, 0);
|
||||
SetRXAAGCDecay(RXA_CHAN, 50);
|
||||
SetRXAAGCHangThreshold(RXA_CHAN, 100);
|
||||
end;
|
||||
end;
|
||||
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
|
||||
|
||||
// Читаем обратно hang level и thresh для линии на спектре
|
||||
UpdateAGCLines(FLastSpectrumW);
|
||||
@@ -1502,69 +1919,88 @@ begin
|
||||
FSampleRate);
|
||||
end;
|
||||
|
||||
// Параметризованные по каналу версии — делятся между главным каналом и слайсами.
|
||||
procedure TWDSPEngine.ApplyNRToChan(Chan, NRMode: Integer);
|
||||
begin
|
||||
SetRXAANRVals(Chan, 64, 16, 16e-4, 10e-7);
|
||||
SetRXAANRPosition(Chan, FILTER_NR_AGC_POSITION);
|
||||
SetRXAANRRun(Chan, Ord(NRMode = NR_MODE_1));
|
||||
|
||||
SetRXAEMNRPosition(Chan, FILTER_NR_AGC_POSITION);
|
||||
SetRXAEMNRgainMethod(Chan, FILTER_NR2_GAIN_METHOD);
|
||||
SetRXAEMNRnpeMethod(Chan, FILTER_NR2_NPE_METHOD);
|
||||
SetRXAEMNRtrainZetaThresh(Chan, FILTER_NR2_TRAINED_THRESHOLD);
|
||||
SetRXAEMNRtrainT2(Chan, FILTER_NR2_TRAINED_T2);
|
||||
SetRXAEMNRpost2Taper(Chan, FILTER_NR2_POST_TAPER);
|
||||
SetRXAEMNRpost2Nlevel(Chan, FILTER_NR2_POST_NLEVEL);
|
||||
SetRXAEMNRpost2Factor(Chan, FILTER_NR2_POST_FACTOR);
|
||||
SetRXAEMNRpost2Rate(Chan, FILTER_NR2_POST_RATE);
|
||||
SetRXAEMNRaeRun(Chan, 1);
|
||||
SetRXAEMNRpost2Run(Chan, FILTER_NR2_POST_RUN);
|
||||
SetRXAEMNRRun(Chan, Ord(NRMode = NR_MODE_2));
|
||||
|
||||
SetRXARNNRPosition(Chan, FILTER_NR_AGC_POSITION);
|
||||
SetRXARNNRRun(Chan, Ord(NRMode = NR_MODE_3));
|
||||
|
||||
SetRXASBNRreductionAmount(Chan, FILTER_NR4_REDUCTION_AMOUNT);
|
||||
SetRXASBNRsmoothingFactor(Chan, FILTER_NR4_SMOOTHING_FACTOR);
|
||||
SetRXASBNRwhiteningFactor(Chan, FILTER_NR4_WHITENING_FACTOR);
|
||||
SetRXASBNRnoiseRescale(Chan, FILTER_NR4_NOISE_RESCALE);
|
||||
SetRXASBNRpostFilterThreshold(Chan, FILTER_NR4_POST_THRESHOLD);
|
||||
SetRXASBNRnoiseScalingType(Chan, FILTER_NR4_NOISE_SCALING_TYPE);
|
||||
SetRXASBNRPosition(Chan, FILTER_NR_AGC_POSITION);
|
||||
SetRXASBNRRun(Chan, Ord(NRMode = NR_MODE_4));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyNBToChan(Chan, NBMode: Integer);
|
||||
begin
|
||||
SetEXTANBTau(Chan, FILTER_NB_TAU);
|
||||
SetEXTANBHangtime(Chan, FILTER_NB_HANGTIME);
|
||||
SetEXTANBAdvtime(Chan, FILTER_NB_ADVTIME);
|
||||
SetEXTANBThreshold(Chan, FILTER_NB_THRESHOLD);
|
||||
SetEXTANBRun(Chan, Ord(NBMode = NB_MODE_1));
|
||||
|
||||
SetEXTNOBMode(Chan, FILTER_NB2_MODE);
|
||||
SetEXTNOBTau(Chan, FILTER_NB_TAU);
|
||||
SetEXTNOBHangtime(Chan, FILTER_NB_HANGTIME);
|
||||
SetEXTNOBAdvtime(Chan, FILTER_NB_ADVTIME);
|
||||
SetEXTNOBThreshold(Chan, FILTER_NB_THRESHOLD);
|
||||
SetEXTNOBRun(Chan, Ord(NBMode = NB_MODE_2));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplySNBToChan(Chan: Integer; On_: Boolean);
|
||||
begin
|
||||
SetRXASNBARun(Chan, Ord(On_));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyANFToChan(Chan: Integer; On_: Boolean);
|
||||
begin
|
||||
SetRXAANFPosition(Chan, FILTER_NR_AGC_POSITION);
|
||||
SetRXAANFRun(Chan, Ord(On_));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyNRState;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
|
||||
SetRXAANRVals(RXA_CHAN, 64, 16, 16e-4, 10e-7);
|
||||
SetRXAANRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||||
SetRXAANRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_1));
|
||||
|
||||
SetRXAEMNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||||
SetRXAEMNRgainMethod(RXA_CHAN, FILTER_NR2_GAIN_METHOD);
|
||||
SetRXAEMNRnpeMethod(RXA_CHAN, FILTER_NR2_NPE_METHOD);
|
||||
SetRXAEMNRtrainZetaThresh(RXA_CHAN, FILTER_NR2_TRAINED_THRESHOLD);
|
||||
SetRXAEMNRtrainT2(RXA_CHAN, FILTER_NR2_TRAINED_T2);
|
||||
SetRXAEMNRpost2Taper(RXA_CHAN, FILTER_NR2_POST_TAPER);
|
||||
SetRXAEMNRpost2Nlevel(RXA_CHAN, FILTER_NR2_POST_NLEVEL);
|
||||
SetRXAEMNRpost2Factor(RXA_CHAN, FILTER_NR2_POST_FACTOR);
|
||||
SetRXAEMNRpost2Rate(RXA_CHAN, FILTER_NR2_POST_RATE);
|
||||
SetRXAEMNRaeRun(RXA_CHAN, 1);
|
||||
SetRXAEMNRpost2Run(RXA_CHAN, FILTER_NR2_POST_RUN);
|
||||
SetRXAEMNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_2));
|
||||
|
||||
SetRXARNNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||||
SetRXARNNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_3));
|
||||
|
||||
SetRXASBNRreductionAmount(RXA_CHAN, FILTER_NR4_REDUCTION_AMOUNT);
|
||||
SetRXASBNRsmoothingFactor(RXA_CHAN, FILTER_NR4_SMOOTHING_FACTOR);
|
||||
SetRXASBNRwhiteningFactor(RXA_CHAN, FILTER_NR4_WHITENING_FACTOR);
|
||||
SetRXASBNRnoiseRescale(RXA_CHAN, FILTER_NR4_NOISE_RESCALE);
|
||||
SetRXASBNRpostFilterThreshold(RXA_CHAN, FILTER_NR4_POST_THRESHOLD);
|
||||
SetRXASBNRnoiseScalingType(RXA_CHAN, FILTER_NR4_NOISE_SCALING_TYPE);
|
||||
SetRXASBNRPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||||
SetRXASBNRRun(RXA_CHAN, Ord(FNRMode = NR_MODE_4));
|
||||
ApplyNRToChan(RXA_CHAN, FNRMode);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyNBState;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
|
||||
SetEXTANBTau(RXA_CHAN, FILTER_NB_TAU);
|
||||
SetEXTANBHangtime(RXA_CHAN, FILTER_NB_HANGTIME);
|
||||
SetEXTANBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME);
|
||||
SetEXTANBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD);
|
||||
SetEXTANBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_1));
|
||||
|
||||
SetEXTNOBMode(RXA_CHAN, FILTER_NB2_MODE);
|
||||
SetEXTNOBTau(RXA_CHAN, FILTER_NB_TAU);
|
||||
SetEXTNOBHangtime(RXA_CHAN, FILTER_NB_HANGTIME);
|
||||
SetEXTNOBAdvtime(RXA_CHAN, FILTER_NB_ADVTIME);
|
||||
SetEXTNOBThreshold(RXA_CHAN, FILTER_NB_THRESHOLD);
|
||||
SetEXTNOBRun(RXA_CHAN, Ord(FNBMode = NB_MODE_2));
|
||||
ApplyNBToChan(RXA_CHAN, FNBMode);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplySNBState;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
SetRXASNBARun(RXA_CHAN, Ord(FSNBEnabled));
|
||||
ApplySNBToChan(RXA_CHAN, FSNBEnabled);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyANFState;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
SetRXAANFPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
||||
SetRXAANFRun(RXA_CHAN, Ord(FANFEnabled));
|
||||
ApplyANFToChan(RXA_CHAN, FANFEnabled);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyNoiseFilterState;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# Мультислайсы (несколько приёмных фильтров, как во FlexRadio)
|
||||
|
||||
Статус: **план; UI-каркас флага слайса уже сделан** (см. «Что уже готово»),
|
||||
DSP/роутинг (Этапы 1/3) — не начаты. Ничего не коммитить без явной просьбы.
|
||||
|
||||
## Что уже готово
|
||||
|
||||
`VfoOverlay.pas` переделан в **«флаг слайса»** в стиле SmartSDR (6 коммитов
|
||||
`4a92256..a04c592`, уже в `main`). Это ровно UI-управление одним слайсом из
|
||||
Этапа 2, только пока в единственном экземпляре:
|
||||
|
||||
- Флаг-накладка поверх спектра с буквой слайса (`FSlice`, сейчас захардкожено `'A'`),
|
||||
fly-out пикерами **MODE / DSP / AUD**, слайдером громкости, mute, SPLIT,
|
||||
кликабельным **TX-бейджем** (выбор TX-слайса) и S-метром.
|
||||
- **Per-flag выбор аудио-устройства** уже есть: `SetAudioDevices` + `OnAudioDevice`
|
||||
(`VfoOverlay.pas:31,125,136`) — прокручиваемый список в AUD-fly-out. Это готовый
|
||||
UI под «своё звуковое устройство на слайс» из абстракции `TSlice.DeviceIndex`.
|
||||
- События наружу: `OnSelect/OnDSPChange/OnAGCChange/OnVolume/OnMute/OnAudioDevice/`
|
||||
`OnSplit/OnTxSelect/OnInvalidate` (`VfoOverlay.pas:131-139`).
|
||||
- Автопозиционирование: флаг флипается влево, если не влезает справа (`6ad5f29`).
|
||||
|
||||
Привязка в приложении **пока одиночная**: один `FVfoOverlay` в `MainForm`
|
||||
(`MainForm.pas:125`), созданный/подключённый на `MainForm.pas:962-972`, привязан к
|
||||
VFO A (`UpdateVfo(FController.FVfoA)`, `MainForm.pas:3265`); хендлеры
|
||||
`OnVfoOverlay*` шлют изменения в главный тракт. `SpectrumView` держит один
|
||||
`FVfoOverlay` и рисует его в `DrawOverlay` (`SpectrumView.pas:72,220,1010`).
|
||||
|
||||
Чтобы превратить это в мультислайс — нужно из одиночного флага сделать **коллекцию
|
||||
флагов (по одному на слайс)** и развести их события на конкретный `SliceId` вместо
|
||||
глобального VFO A. Детали — в обновлённом Этапе 2.
|
||||
|
||||
## Цель
|
||||
|
||||
Несколько независимых приёмных «слайсов» на одном ADC/одном IQ-потоке:
|
||||
у каждого слайса своя частота, режим, полоса, AGC и **своё звуковое
|
||||
устройство** (как DAX/наушники/динамик во Flex). Панадаптер (спектр+водопад)
|
||||
остаётся один, слайсы рисуются маркерами поверх.
|
||||
|
||||
## Как это устроено у Flex и что из этого берём
|
||||
|
||||
Слайс во Flex — это **не «ещё один фильтр» на одном приёмнике**, а полноценный
|
||||
независимый узкополосный приёмник (свой DDC в FPGA + демодулятор + фильтр + AGC +
|
||||
аудио). Общий у слайсов — только панадаптер (широкий FFT-захват).
|
||||
|
||||
Один WDSP-канал = один демодулятор = один фильтр = один аудиопоток. Значит для
|
||||
«разный звук/режим/полоса на каждый слайс» нужен **отдельный WDSP-канал на слайс**.
|
||||
Способ получить узкополосный IQ для канала различается по железу — см. ниже.
|
||||
|
||||
## Ключевое различие бэкендов
|
||||
|
||||
```
|
||||
openHPSDR (Protocol 2, V4.3, MAX_DDCS=80):
|
||||
ADC0 ─┬─ DDC (FPGA-тюнер) → IQ-поток порт 1035+n → WDSP-канал → фильтр/AGC → AudioOut[dev]
|
||||
├─ DDC → ...
|
||||
└─ DDC → ...
|
||||
• Аппаратные DDC: слайс тюнится КУДА УГОДНО в полосе ADC (весь КВ), дёшево по CPU.
|
||||
• На Angelia/Orion/Saturn DDC0/DDC1 заняты PureSignal → слайсы с DDC2+ (в коде DDCBase=2).
|
||||
• RX2 = ВТОРОЙ ADC (ADC1), отдельная антенна/diversity — ОРТОГОНАЛЬНО слайсам, не нужно.
|
||||
|
||||
Pluto (AD9363, libiio):
|
||||
один RX-тракт: один LO, один rf_bandwidth, один IQ-поток (притворяется одним DDC,
|
||||
пушится как TDDCIQPacket в тот же конвейер).
|
||||
• Аппаратных DDC НЕТ. Слайсы = СОФТОВЫЙ fan-out одного IQ-блока на N WDSP-каналов,
|
||||
каждый со своим SetRXAShiftFreq в пределах захваченной полосы вокруг LO.
|
||||
• Для QO-100 идеально: весь транспондер (~500 кГц) в захвате, LO залочен на маяк.
|
||||
```
|
||||
|
||||
## Единая абстракция
|
||||
|
||||
```pascal
|
||||
TSlice = record
|
||||
Id: Integer;
|
||||
TargetHz: Double; // абсолютная частота слайса
|
||||
Mode: TRxMode; // USB/LSB/CW/FM/...
|
||||
FilterLo,
|
||||
FilterHi: Integer;
|
||||
AGCMode: TWDSPAGCMode;
|
||||
Volume: Double;
|
||||
WDSPChannel: Integer; // канал WDSP (0..N); 0=главный, 1=TX заняты
|
||||
Source: (srcSoftShift, srcHardDDC); // как получаем узкий IQ
|
||||
DDCIndex: Integer; // если srcHardDDC — какой аппаратный DDC
|
||||
ShiftHz: Double; // если srcSoftShift — сдвиг = TargetHz - CapturedCenterHz
|
||||
AudioOut: TAudioOutput; // свой инстанс со своим DeviceIndex
|
||||
DeviceIndex: Integer; // выбранная звуковуха (или -1 = default)
|
||||
Enabled: Boolean;
|
||||
end;
|
||||
```
|
||||
|
||||
Правило маршрутизации по частоте (одно на оба бэкенда):
|
||||
- слайс **внутри** текущего захваченного span → `srcSoftShift` (Pluto и HPSDR);
|
||||
- слайс **вне** захваченного span → `srcHardDDC` (только HPSDR; Pluto так не умеет).
|
||||
|
||||
## Порядок работ
|
||||
|
||||
### Этап 1 — Софтовый fan-out в WDSPEngine (backend-agnostic)
|
||||
|
||||
Даёт слайсы сразу на Pluto/QO-100 (единственный режим) и на HPSDR для слайсов
|
||||
внутри видимой полосы. Pluto уже пушит IQ через тот же DDC-конвейер, поэтому
|
||||
fan-out живёт в движке и не зависит от бэкенда.
|
||||
|
||||
- `WDSPEngine.pas`: сейчас `RXA_CHAN=0` один канал, один `FRXOut`, один аудиовыход
|
||||
(`WDSPEngine.pas:96`, alloc буферов ~`:159`, OpenChannel ~`:1081`).
|
||||
- Добавить массив слайс-каналов: для каждого активного слайса `OpenChannel(chN, ...)`
|
||||
с той же геометрией, что RXA_CHAN, + `SetRXAShiftRun/SetRXAShiftFreq(chN, ShiftHz)`
|
||||
(механизм уже используется: `WDSPEngine.pas:1099-1100`), `SetRXAMode`,
|
||||
`RXASetPassband`, AGC, `SetRXAPanelGain1`.
|
||||
- В DSP-потоке после накопления IQ-блока (`FRXAccI/Q`, `FRXAccPos`) прогонять
|
||||
`fexchange0` по КАЖДОМУ активному каналу из ОДНОГО и того же входного буфера
|
||||
(входной буфер не портить in-place между каналами — держать копию, как `FSpecBuf`).
|
||||
- Выход каждого канала → свой callback `OnSliceAudio(SliceId, L, R, Count)`.
|
||||
- `AudioOutput.pas`: уже готов к мульти-устройству — `DeviceIndex`, `EnumOutputDevices`,
|
||||
`FindDeviceByName` (`AudioOutput.pas:137-151`). Создавать **отдельный `TAudioOutput`
|
||||
на слайс**, каждый со своим `DeviceIndex`.
|
||||
- `RadioController.pas`: `TSlice[]` (add/remove/retune/setmode/setdevice), роутинг
|
||||
`OnSliceAudio` → нужный `TAudioOutput`.
|
||||
|
||||
Проверка Этапа 1: Pluto/QO-100 — 2 слайса (VFO + маяк-монитор), звук в две разные
|
||||
звуковухи, независимые режимы/полосы. Сборка: `lazbuild --ws=qt6 ewsdr.lpr`.
|
||||
|
||||
### Этап 2 — UI слайсов на водопаде + выбор устройства
|
||||
|
||||
Каркас флага слайса уже сделан (см. «Что уже готово»): один `TVfoOverlay` со всеми
|
||||
пикерами и per-flag выбором устройства. Остаётся размножить его на N слайсов и
|
||||
привязать к позиции по частоте. Выпадашка устройства и dropdown-паттерн **уже есть в
|
||||
самом флаге** (AUD fly-out) — отдельный `DeviceForm` не нужен.
|
||||
|
||||
- **Коллекция флагов вместо одного.** Из `MainForm.FVfoOverlay` (одиночный,
|
||||
`MainForm.pas:125`) сделать `array of TVfoOverlay` / список, по флагу на слайс;
|
||||
`SpectrumView.FVfoOverlay` (`SpectrumView.pas:72`) — тоже коллекция, рисовать все в
|
||||
`DrawOverlay` (`SpectrumView.pas:1010`). Разместить: `SetSlice(Id, Letter)` в оверлей,
|
||||
события `OnVfoOverlay*` должны нести `SliceId` (сейчас бьют по VFO A напрямую —
|
||||
завести маппинг оверлей→слайс, роутить в нужный `TSlice`/WDSP-канал из Этапа 1).
|
||||
- **Позиционирование по частоте.** `PositionVfoOverlay` (`MainForm.pas:518`) сейчас
|
||||
ставит один флаг; расширить на маппинг `TargetHz → X` через `GetViewWindow`/
|
||||
`SyncSpecViewFreq` (зум-работа). Слайс за краем видимой полосы — маркер у края
|
||||
(флип влево/вправо уже есть, `6ad5f29`).
|
||||
- **ЛКМ по водопаду** = поставить/двинуть активный слайс; выделение активного
|
||||
(подсветка буквы/рамки флага). Хит-тест мыши флага уже разведён
|
||||
(`HandleMouseDown/Move/Up`, `MainForm.pas:4424`) — добавить «клик мимо флага по
|
||||
спектру = ретюн активного слайса».
|
||||
- **TX-select** уже кликабелен (`OnTxSelect`, `08c71c4`) — при мультислайсе выбирать,
|
||||
какой слайс идёт в TX; снимать флаг с остальных.
|
||||
- Персист: список слайсов в `Settings.pas` (per-band или глобально — решить), включая
|
||||
`DeviceIndex`/имя устройства каждого флага.
|
||||
|
||||
### Этап 3 — Аппаратные DDC для openHPSDR (слайс куда угодно по КВ)
|
||||
|
||||
Апгрейд поверх общего каркаса; нужен только чтобы ставить слайс ВНЕ захваченной
|
||||
полосы. Плита ~на 70% есть:
|
||||
- `ConfigureDDCs(NumDDCs, ...)` уже включает N DDC с per-DDC конфигом/ADC-source
|
||||
(`HPSDRNetwork.pas:220`, `DDCConfig[ddc*6+0]=ADCSource`).
|
||||
- Приём разведён по DDC: порты `1035+n` → `HandleDDCIQ(Buf,Len,DDCIdx)`
|
||||
(`HPSDRNetwork.pas:392`).
|
||||
- High Priority уже держит частоту на КАЖДЫЙ DDC (bytes 9-12=DDC0, 13-16=DDC1, …)
|
||||
`HPSDRProtocol.pas:271`.
|
||||
|
||||
Чего не хватает:
|
||||
1. Per-DDC частота: сейчас `SetRunAndFreq(...; DDC0FreqHz; ...)` знает только DDC0
|
||||
(`HPSDRNetwork.pas:224`) — писать частотное слово каждого DDC в High Priority.
|
||||
2. Роутинг `HandleDDCIQ(DDCIdx)` → соответствующий WDSP-канал слайса.
|
||||
3. Учёт DDCBase=2 (PureSignal) и Ethernet-бюджета (реально 4–8 слайсов, не 80).
|
||||
|
||||
`feature/rx2-support` — это про ВТОРОЙ ADC, не про слайсы; но её каркас
|
||||
(доп. WDSP-канал + доп. аудиовыход + роутинг по индексу) частично переиспользуем.
|
||||
|
||||
## Оговорки / риски
|
||||
|
||||
- CPU софтовых слайсов ~линейно от их числа: каждый WDSP-канал жуёт полный rate и
|
||||
децимирует сам (аппаратный DDC дешевле — децимирует железо). Для QO-100 span мал,
|
||||
несколько слайсов — норма.
|
||||
- Не портить входной IQ-буфер между каналами (fexchange0 пишет in-place).
|
||||
- Pluto: слайсы только в пределах захваченной полосы; выход за край = ретюн LO
|
||||
(двигает всё, при QO-100 LO залочен на маяк — не трогать).
|
||||
- Пороговая аккуратность: старт/стоп WDSP-каналов на лету (см. заметку про задержки
|
||||
RXA стоп/старт в `WDSPEngine.pas:~230`).
|
||||
|
||||
## Связанные заметки памяти
|
||||
|
||||
- project_pluto_integration, project_qo100_zoom_panel, project_qo100_decoder_lock
|
||||
- project_spectrum_zoom (GetViewWindow / оверлеи / маппинг мыши)
|
||||
- project_channels_feature (dropdown/форма-редактор как паттерн UI)
|
||||
@@ -17,9 +17,9 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="215"/>
|
||||
<BuildNr Value="232"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="27">
|
||||
<MacroValues Count="50">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
@@ -47,6 +47,29 @@
|
||||
<Macro25 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro26 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro27 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro28 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro29 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro30 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro31 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro32 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro33 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro34 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro35 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro36 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro37 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro38 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro39 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro40 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro41 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro42 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro43 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro44 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro45 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro46 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro47 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro48 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro49 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro50 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
|
||||
Reference in New Issue
Block a user