mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 04:47:35 +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).
|
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
||||||
FDeviceDialog: TDeviceDialog;
|
FDeviceDialog: TDeviceDialog;
|
||||||
FPendingIP: string; // IP устройства для подключения
|
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 слева вверху спектра
|
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||||||
FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100 внизу спектра
|
FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100 внизу спектра
|
||||||
FPanelHidden: Boolean; // True = левая панель скрыта
|
FPanelHidden: Boolean; // True = левая панель скрыта
|
||||||
@@ -516,6 +523,7 @@ type
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
procedure BtnSettingsClick(Sender: TObject);
|
procedure BtnSettingsClick(Sender: TObject);
|
||||||
procedure PositionVfoOverlay;
|
procedure PositionVfoOverlay;
|
||||||
|
procedure UpdateVfoOverlayVisibility; // флаг A виден: панель скрыта ИЛИ есть слайсы B+
|
||||||
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||||||
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||||
procedure OnVfoOverlayAGCChange(AGCMode: Integer);
|
procedure OnVfoOverlayAGCChange(AGCMode: Integer);
|
||||||
@@ -524,9 +532,26 @@ type
|
|||||||
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
procedure OnVfoOverlaySplit(Sender: TObject);
|
procedure OnVfoOverlaySplit(Sender: TObject);
|
||||||
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
||||||
|
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
|
||||||
|
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
|
||||||
|
procedure ProcessPendingSliceClose; // отложенное удаление флага
|
||||||
procedure PushVfoOverlayExtState;
|
procedure PushVfoOverlayExtState;
|
||||||
procedure PushVfoOverlayAudioDevices;
|
procedure PushVfoOverlayAudioDevices;
|
||||||
procedure OnVfoOverlayInvalidate(Sender: TObject);
|
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 PbSpectrumDblClick(Sender: TObject);
|
||||||
procedure TrkDriveChange(Sender: TObject);
|
procedure TrkDriveChange(Sender: TObject);
|
||||||
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
||||||
@@ -960,16 +985,14 @@ begin
|
|||||||
FDeviceDialog.Store := FController.FDeviceStore;
|
FDeviceDialog.Store := FController.FDeviceStore;
|
||||||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||||||
FVfoOverlay := TVfoOverlay.Create(Self);
|
FVfoOverlay := TVfoOverlay.Create(Self);
|
||||||
FVfoOverlay.OnSelect := OnModeFilterSelect;
|
WireOverlayEvents(FVfoOverlay);
|
||||||
FVfoOverlay.OnDSPChange := OnVfoOverlayDSPChange;
|
FVfoOverlay.SetSliceInfo(0, 'A', True); // главный приёмник = слайс A
|
||||||
FVfoOverlay.OnAGCChange := OnVfoOverlayAGCChange;
|
|
||||||
FVfoOverlay.OnVolume := OnVfoOverlayVolume;
|
|
||||||
FVfoOverlay.OnMute := OnVfoOverlayMute;
|
|
||||||
FVfoOverlay.OnAudioDevice := OnVfoOverlayAudioDevice;
|
|
||||||
FVfoOverlay.OnSplit := OnVfoOverlaySplit;
|
|
||||||
FVfoOverlay.OnTxSelect := OnVfoOverlayTxSelect;
|
|
||||||
FVfoOverlay.OnInvalidate := OnVfoOverlayInvalidate;
|
|
||||||
FVfoOverlay.Visible := False;
|
FVfoOverlay.Visible := False;
|
||||||
|
FActiveSliceId := 0;
|
||||||
|
FActiveFlag := nil;
|
||||||
|
FPendingSliceClose := 0;
|
||||||
|
FSliceDrag := False;
|
||||||
|
FSliceDragId := 0;
|
||||||
FPanelHidden := False;
|
FPanelHidden := False;
|
||||||
FController.FShowSpectrum := True;
|
FController.FShowSpectrum := True;
|
||||||
FController.FShowWaterfall := True;
|
FController.FShowWaterfall := True;
|
||||||
@@ -1413,6 +1436,7 @@ begin
|
|||||||
SyncSpecViewFreq;
|
SyncSpecViewFreq;
|
||||||
UpdatePanZoomBar;
|
UpdatePanZoomBar;
|
||||||
PositionVfoOverlay;
|
PositionVfoOverlay;
|
||||||
|
PositionSliceFlags;
|
||||||
FSpecView.DrawSpectrum;
|
FSpecView.DrawSpectrum;
|
||||||
PbSpectrum.Invalidate;
|
PbSpectrum.Invalidate;
|
||||||
FSpecView.DrawWaterfall;
|
FSpecView.DrawWaterfall;
|
||||||
@@ -1497,6 +1521,16 @@ begin
|
|||||||
PushDeviceListToWeb; // web-overlay тоже получает обновлённый список
|
PushDeviceListToWeb; // web-overlay тоже получает обновлённый список
|
||||||
end;
|
end;
|
||||||
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
|
finally
|
||||||
FSyncingFromController := False;
|
FSyncingFromController := False;
|
||||||
end;
|
end;
|
||||||
@@ -3188,6 +3222,7 @@ procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
|||||||
var
|
var
|
||||||
NowTick: QWord;
|
NowTick: QWord;
|
||||||
ElapsedTick: QWord;
|
ElapsedTick: QWord;
|
||||||
|
I: Integer;
|
||||||
begin
|
begin
|
||||||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||||||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||||||
@@ -3266,6 +3301,19 @@ begin
|
|||||||
PushVfoOverlayExtState;
|
PushVfoOverlayExtState;
|
||||||
PositionVfoOverlay;
|
PositionVfoOverlay;
|
||||||
end;
|
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 даёт новые строки чаще.
|
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||||
if FSpectrumDirty then
|
if FSpectrumDirty then
|
||||||
@@ -4421,11 +4469,22 @@ procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
|||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
var VC, VS: Double;
|
var VC, VS: Double;
|
||||||
begin
|
begin
|
||||||
if Assigned(FVfoOverlay) and
|
if DispatchFlagsMouseDown(Button, X, Y) then
|
||||||
FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
begin
|
||||||
|
ProcessPendingSliceClose; // × мог пометить слайс к удалению
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
if Assigned(FSampleRateOverlay) and
|
if Assigned(FSampleRateOverlay) and
|
||||||
FSampleRateOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
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
|
if Button = mbLeft then
|
||||||
begin
|
begin
|
||||||
// Наведение маяка: ЛКМ указывает на маяк (после включения BEACON «вооружено»,
|
// Наведение маяка: ЛКМ указывает на маяк (после включения BEACON «вооружено»,
|
||||||
@@ -4441,6 +4500,18 @@ begin
|
|||||||
UpdateBeaconButton;
|
UpdateBeaconButton;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
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
|
if FSpecView.MarkerActive then
|
||||||
begin
|
begin
|
||||||
@@ -4485,18 +4556,40 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||||||
X, Y: Integer);
|
X, Y: Integer);
|
||||||
|
var
|
||||||
|
VC, VS, NewHz, LoHz, HiHz: Double;
|
||||||
begin
|
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
|
if Assigned(FSampleRateOverlay) then
|
||||||
begin
|
begin
|
||||||
FSampleRateOverlay.HandleMouseMove(X, Y);
|
FSampleRateOverlay.HandleMouseMove(X, Y);
|
||||||
end;
|
end;
|
||||||
if Assigned(FVfoOverlay) then
|
DispatchFlagsMouseMove(X, Y);
|
||||||
FVfoOverlay.HandleMouseMove(X, Y);
|
|
||||||
if not FSpecDrag then
|
if not FSpecDrag then
|
||||||
begin
|
begin
|
||||||
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
||||||
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
||||||
PbSpectrum.Cursor := crHandPoint
|
PbSpectrum.Cursor := crHandPoint
|
||||||
|
else if SliceAtPixel(X) <> 0 then
|
||||||
|
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
||||||
else
|
else
|
||||||
PbSpectrum.Cursor := crDefault;
|
PbSpectrum.Cursor := crDefault;
|
||||||
end;
|
end;
|
||||||
@@ -4514,7 +4607,14 @@ end;
|
|||||||
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
begin
|
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
|
begin
|
||||||
FSpecDrag := False;
|
FSpecDrag := False;
|
||||||
PbSpectrum.Cursor := crDefault;
|
PbSpectrum.Cursor := crDefault;
|
||||||
@@ -4533,8 +4633,7 @@ procedure TMainForm.PbSpectrumMouseLeave(Sender: TObject);
|
|||||||
begin
|
begin
|
||||||
if Assigned(FSampleRateOverlay) then
|
if Assigned(FSampleRateOverlay) then
|
||||||
FSampleRateOverlay.HandleMouseLeave;
|
FSampleRateOverlay.HandleMouseLeave;
|
||||||
if Assigned(FVfoOverlay) then
|
DispatchFlagsMouseLeave;
|
||||||
FVfoOverlay.HandleMouseLeave;
|
|
||||||
if not FSpecDrag then PbSpectrum.Cursor := crDefault;
|
if not FSpecDrag then PbSpectrum.Cursor := crDefault;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -5008,6 +5107,28 @@ begin
|
|||||||
begin
|
begin
|
||||||
FLeftPanelW := PanelLeft.Width; // сохраняем реальную ширину (учитывает DPI на Win32)
|
FLeftPanelW := PanelLeft.Width; // сохраняем реальную ширину (учитывает DPI на Win32)
|
||||||
PanelLeft.Width := 0;
|
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.Width := OVL_W;
|
||||||
FVfoOverlay.Height := FVfoOverlay.BaseHeight; // растёт сам при открытии fly-out
|
FVfoOverlay.Height := FVfoOverlay.BaseHeight; // растёт сам при открытии fly-out
|
||||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||||
@@ -5019,14 +5140,9 @@ begin
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
PanelLeft.Width := FLeftPanelW; // восстанавливаем сохранённую ширину
|
|
||||||
FVfoOverlay.HandleMouseLeave;
|
FVfoOverlay.HandleMouseLeave;
|
||||||
FVfoOverlay.Visible := False;
|
FVfoOverlay.Visible := False;
|
||||||
end;
|
end;
|
||||||
if Assigned(FSampleRateOverlay) then
|
|
||||||
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
|
|
||||||
ResizeSpectrumPanels;
|
|
||||||
ResizeSMeter;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.PositionVfoOverlay;
|
procedure TMainForm.PositionVfoOverlay;
|
||||||
@@ -5089,6 +5205,14 @@ procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
|||||||
var
|
var
|
||||||
Idx: Integer;
|
Idx: Integer;
|
||||||
begin
|
begin
|
||||||
|
// Событие пришло от флага слайса (B+) — маршрутизируем в slice-API, главный не трогаем.
|
||||||
|
if CurSliceId <> 0 then
|
||||||
|
begin
|
||||||
|
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
||||||
|
PushSliceFlagState(CurSliceId);
|
||||||
|
PositionSliceFlags;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
FController.FMode := Mode;
|
FController.FMode := Mode;
|
||||||
FController.FFilterBW := FilterBW;
|
FController.FFilterBW := FilterBW;
|
||||||
if Mode = MODE_FM then
|
if Mode = MODE_FM then
|
||||||
@@ -5118,6 +5242,11 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
procedure TMainForm.OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||||
begin
|
begin
|
||||||
|
if CurSliceId <> 0 then
|
||||||
|
begin
|
||||||
|
FController.SetSliceDSP(CurSliceId, NRMode, NBMode, SNBOn, ANFOn);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
// Команды контроллера применяют к DSP и через OnControllerState обновляют кнопки.
|
// Команды контроллера применяют к DSP и через OnControllerState обновляют кнопки.
|
||||||
FController.SetNR(NRMode);
|
FController.SetNR(NRMode);
|
||||||
FController.SetNB(NBMode);
|
FController.SetNB(NBMode);
|
||||||
@@ -5127,34 +5256,59 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayAGCChange(AGCMode: Integer);
|
procedure TMainForm.OnVfoOverlayAGCChange(AGCMode: Integer);
|
||||||
begin
|
begin
|
||||||
|
if CurSliceId <> 0 then
|
||||||
|
begin
|
||||||
|
FController.SetSliceAGCMode(CurSliceId, TRadioController.AGCModeFromUI(AGCMode));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
FController.SetAGCMode(AGCMode);
|
FController.SetAGCMode(AGCMode);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayVolume(Vol: Integer);
|
procedure TMainForm.OnVfoOverlayVolume(Vol: Integer);
|
||||||
begin
|
begin
|
||||||
|
if CurSliceId <> 0 then
|
||||||
|
begin
|
||||||
|
FController.SetSliceVolume(CurSliceId, Vol / 100.0);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
FController.SetVolume(Vol);
|
FController.SetVolume(Vol);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayMute(Sender: TObject);
|
procedure TMainForm.OnVfoOverlayMute(Sender: TObject);
|
||||||
|
var S: TCtrlSlice;
|
||||||
begin
|
begin
|
||||||
|
if CurSliceId <> 0 then
|
||||||
|
begin
|
||||||
|
if FController.GetSlice(CurSliceId, S) then
|
||||||
|
FController.SetSliceMute(CurSliceId, not S.Muted);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
FController.ToggleMute;
|
FController.ToggleMute;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlaySplit(Sender: TObject);
|
procedure TMainForm.OnVfoOverlaySplit(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
|
if CurSliceId <> 0 then Exit; // SPLIT — только главный (слайсы не передают)
|
||||||
FController.SetSplit(not FController.FSplitTxB);
|
FController.SetSplit(not FController.FSplitTxB);
|
||||||
PushVfoOverlayExtState;
|
PushVfoOverlayExtState;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayTxSelect(Sender: TObject);
|
procedure TMainForm.OnVfoOverlayTxSelect(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
// TODO(слайсы): выбрать этот слайс как TX-слайс. Пока один слайс —
|
// TODO(слайсы): выбор TX-слайса при полноценной мультислайс-передаче.
|
||||||
// он всегда TX-слайс, поэтому клик ничего не меняет (хук под мультислайсы).
|
// Пока передаёт только главный канал — клик по TX неглавного игнорируем.
|
||||||
|
if CurSliceId <> 0 then Exit;
|
||||||
PushVfoOverlayExtState;
|
PushVfoOverlayExtState;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure TMainForm.OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
begin
|
begin
|
||||||
|
if CurSliceId <> 0 then
|
||||||
|
begin
|
||||||
|
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
||||||
|
PushSliceFlagState(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
ApplyAudioDevice(DevIndex, DevName);
|
ApplyAudioDevice(DevIndex, DevName);
|
||||||
PushVfoOverlayAudioDevices; // обновить подсветку текущего устройства
|
PushVfoOverlayAudioDevices; // обновить подсветку текущего устройства
|
||||||
end;
|
end;
|
||||||
@@ -5193,9 +5347,326 @@ begin
|
|||||||
if Assigned(FSpecView) then
|
if Assigned(FSpecView) then
|
||||||
begin
|
begin
|
||||||
FSpecView.SpectrumDirty := True;
|
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;
|
end;
|
||||||
|
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;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
|
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
|
||||||
@@ -5752,16 +6223,63 @@ procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState;
|
|||||||
P := C.ScreenToClient(Mouse.CursorPos);
|
P := C.ScreenToClient(Mouse.CursorPos);
|
||||||
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
|
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
|
||||||
end;
|
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
|
var
|
||||||
Step: Int64;
|
Step: Int64;
|
||||||
Delta: Int64;
|
Delta: Int64;
|
||||||
Base: Int64;
|
Base: Int64;
|
||||||
NewFreq: Int64;
|
NewFreq: Int64;
|
||||||
|
SliceId: Integer;
|
||||||
|
SLo, SHi, SNew: Double;
|
||||||
|
Sl: TCtrlSlice;
|
||||||
begin
|
begin
|
||||||
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
||||||
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
||||||
// виджет крутил бы B, а этот хендлер — активный A.
|
// виджет крутил бы B, а этот хендлер — активный A.
|
||||||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
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
|
if (FController.FMode = MODE_FM) and FController.FFMStepOn and
|
||||||
not ((ssCtrl in Shift) or (ssShift in Shift)) then
|
not ((ssCtrl in Shift) or (ssShift in Shift)) then
|
||||||
Step := FM_STEP_HZ[FController.FFMStepIdx]
|
Step := FM_STEP_HZ[FController.FFMStepIdx]
|
||||||
|
|||||||
+325
-11
@@ -122,6 +122,24 @@ type
|
|||||||
PLLLock: Boolean;
|
PLLLock: Boolean;
|
||||||
end;
|
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 }
|
||||||
TRadioController = class
|
TRadioController = class
|
||||||
private
|
private
|
||||||
@@ -134,6 +152,11 @@ type
|
|||||||
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
|
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
|
||||||
FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер)
|
FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер)
|
||||||
procedure Changed(Field: TRadioField);
|
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 ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||||||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||||||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||||||
@@ -163,6 +186,12 @@ type
|
|||||||
FDSPEngine: TWDSPEngine;
|
FDSPEngine: TWDSPEngine;
|
||||||
FAudioOut: TAudioOutput;
|
FAudioOut: TAudioOutput;
|
||||||
FAudioIn: TAudioInput;
|
FAudioIn: TAudioInput;
|
||||||
|
|
||||||
|
// ---- Мультислайсы (софт-fan-out) ----
|
||||||
|
// Слайс = доп. приёмник (свой WDSP-канал в движке) + своя звуковуха.
|
||||||
|
// TargetHz абсолютна; ShiftHz в движок = TargetHz - FCenterFreq.
|
||||||
|
FSlices: array[0..MAX_SLICES-1] of TCtrlSlice;
|
||||||
|
FNextSliceId: Integer; // монотонный генератор id слайсов
|
||||||
FSettings: TSettingsManager;
|
FSettings: TSettingsManager;
|
||||||
FChannelStore: TChannelStore;
|
FChannelStore: TChannelStore;
|
||||||
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
|
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
|
||||||
@@ -467,6 +496,32 @@ type
|
|||||||
procedure DriveBy(Delta: Integer);
|
procedure DriveBy(Delta: Integer);
|
||||||
procedure SetAtten(Idx: 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 на
|
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
|
||||||
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
|
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
|
||||||
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
|
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
|
||||||
@@ -625,6 +680,8 @@ begin
|
|||||||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||||||
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||||||
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
||||||
|
FNextSliceId := 1; // мультислайсы: генератор id
|
||||||
|
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||||
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
||||||
FCurrentBand := 5; FCurrentXvtr := -1;
|
FCurrentBand := 5; FCurrentXvtr := -1;
|
||||||
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
||||||
@@ -649,6 +706,9 @@ begin
|
|||||||
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
|
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
|
||||||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||||||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||||||
|
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
||||||
|
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
||||||
|
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||||
|
|
||||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||||
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||||||
@@ -672,12 +732,21 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.FreeEngines;
|
procedure TRadioController.FreeEngines;
|
||||||
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
if Assigned(FNetwork) then
|
if Assigned(FNetwork) then
|
||||||
begin
|
begin
|
||||||
if FNetwork.Connected then FNetwork.Disconnect;
|
if FNetwork.Connected then FNetwork.Disconnect;
|
||||||
FreeAndNil(FNetwork);
|
FreeAndNil(FNetwork);
|
||||||
end;
|
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(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||||||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
||||||
if Assigned(FDSPEngine) then
|
if Assigned(FDSPEngine) then
|
||||||
@@ -966,6 +1035,227 @@ begin
|
|||||||
FAudioOut.Write(Left, Right, Count);
|
FAudioOut.Write(Left, Right, Count);
|
||||||
end;
|
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;
|
procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
|
||||||
Count: Integer);
|
Count: Integer);
|
||||||
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
|
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
|
||||||
@@ -1335,16 +1625,31 @@ begin
|
|||||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.ApplyModeFilter;
|
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||||||
var
|
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||||
Lo, Hi, Half: Integer;
|
var Half: Integer;
|
||||||
begin
|
begin
|
||||||
Half := FFilterBW div 2;
|
Half := BW div 2;
|
||||||
case FMode of
|
case Mode of
|
||||||
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB
|
0: begin Lo := -BW; Hi := -100; end; // LSB
|
||||||
1: begin Lo := 100; Hi := FFilterBW; end; // USB
|
1: begin Lo := 100; Hi := BW; end; // USB
|
||||||
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
|
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
|
||||||
end;
|
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: Integer;
|
||||||
|
begin
|
||||||
|
FilterEdgesFor(FMode, FFilterBW, Lo, Hi);
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
begin
|
begin
|
||||||
FDSPEngine.SetFilter(Lo, Hi);
|
FDSPEngine.SetFilter(Lo, Hi);
|
||||||
@@ -1921,6 +2226,7 @@ begin
|
|||||||
NeedPush := True;
|
NeedPush := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
RefreshSliceShifts; // центр мог сместиться — держим слайсы на их TargetHz
|
||||||
if NeedPush then PushNetworkState;
|
if NeedPush then PushNetworkState;
|
||||||
Changed(rfActiveVfo);
|
Changed(rfActiveVfo);
|
||||||
end;
|
end;
|
||||||
@@ -1941,6 +2247,7 @@ begin
|
|||||||
FCenterFreq := Hz;
|
FCenterFreq := Hz;
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
|
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
|
||||||
|
RefreshSliceShifts; // слайсы привязаны к абсолютной частоте — сдвинуть NCO
|
||||||
PushNetworkState;
|
PushNetworkState;
|
||||||
Changed(rfCenterFreq);
|
Changed(rfCenterFreq);
|
||||||
end;
|
end;
|
||||||
@@ -1972,6 +2279,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FCenterFreq := ActiveFreq;
|
FCenterFreq := ActiveFreq;
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||||||
|
RefreshSliceShifts;
|
||||||
PushNetworkState;
|
PushNetworkState;
|
||||||
Changed(rfVfoA);
|
Changed(rfVfoA);
|
||||||
Changed(rfVfoB);
|
Changed(rfVfoB);
|
||||||
@@ -2057,14 +2365,20 @@ begin
|
|||||||
Changed(rfFilter);
|
Changed(rfFilter);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.ApplyAGCToEngine;
|
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех
|
||||||
|
// фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
||||||
|
class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode;
|
||||||
const
|
const
|
||||||
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для
|
|
||||||
// всех фронтендов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
|
||||||
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
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
|
begin
|
||||||
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
|
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;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetAGCMode(M: Integer);
|
procedure TRadioController.SetAGCMode(M: Integer);
|
||||||
|
|||||||
+84
-4
@@ -70,6 +70,7 @@ type
|
|||||||
FADCOverloadVisible: Boolean;
|
FADCOverloadVisible: Boolean;
|
||||||
FSampleRateOverlay: TSampleRateOverlay;
|
FSampleRateOverlay: TSampleRateOverlay;
|
||||||
FVfoOverlay: TVfoOverlay;
|
FVfoOverlay: TVfoOverlay;
|
||||||
|
FSliceOverlays: TFPList; // доп. слайс-флаги (B+); не владеет (владелец MainForm)
|
||||||
FBandPlanOverlay: TBandPlanOverlay;
|
FBandPlanOverlay: TBandPlanOverlay;
|
||||||
// ── Marker ────────────────────────────────────────────────────────────────
|
// ── Marker ────────────────────────────────────────────────────────────────
|
||||||
FMarkerActive: Boolean;
|
FMarkerActive: Boolean;
|
||||||
@@ -108,12 +109,15 @@ type
|
|||||||
procedure SetFillSpectrum(V: Boolean);
|
procedure SetFillSpectrum(V: Boolean);
|
||||||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||||
procedure DrawMarkerLine(C: TCanvas; 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 DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||||
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||||
procedure CopyGridToSpectrum(W, H: Integer);
|
procedure CopyGridToSpectrum(W, H: Integer);
|
||||||
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
|
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
|
||||||
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
|
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||||
out X1, X2, VfoX: Integer);
|
out X1, X2, VfoX: Integer);
|
||||||
|
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
|
||||||
|
out X1, X2, VfoX: Integer);
|
||||||
function GetWaterfallDirty: Boolean;
|
function GetWaterfallDirty: Boolean;
|
||||||
procedure SetWaterfallDirty(V: Boolean);
|
procedure SetWaterfallDirty(V: Boolean);
|
||||||
// Waterfall sub-view delegating accessors
|
// Waterfall sub-view delegating accessors
|
||||||
@@ -218,6 +222,11 @@ type
|
|||||||
property PbSMeterRight: TPaintBox write SetPbSMeterRight;
|
property PbSMeterRight: TPaintBox write SetPbSMeterRight;
|
||||||
property SampleRateOverlay: TSampleRateOverlay read FSampleRateOverlay write FSampleRateOverlay;
|
property SampleRateOverlay: TSampleRateOverlay read FSampleRateOverlay write FSampleRateOverlay;
|
||||||
property VfoOverlay: TVfoOverlay read FVfoOverlay write FVfoOverlay;
|
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;
|
property BandPlanOverlay: TBandPlanOverlay read FBandPlanOverlay write FBandPlanOverlay;
|
||||||
|
|
||||||
// ── Данные от DSP ─────────────────────────────────────────────────────────
|
// ── Данные от DSP ─────────────────────────────────────────────────────────
|
||||||
@@ -244,6 +253,9 @@ type
|
|||||||
// ── Утилиты ───────────────────────────────────────────────────────────────
|
// ── Утилиты ───────────────────────────────────────────────────────────────
|
||||||
procedure InvalidateGridCache; virtual;
|
procedure InvalidateGridCache; virtual;
|
||||||
procedure InvalidateOverlayCache; virtual;
|
procedure InvalidateOverlayCache; virtual;
|
||||||
|
// Инвалидация ТОЛЬКО текстуры главного VFO-флага (без band/samplerate/слайсов).
|
||||||
|
// Нужна для дешёвого обновления S-метра главного флага в GL-вьюхе.
|
||||||
|
procedure InvalidateVfoOverlay; virtual;
|
||||||
procedure SetTheme(const T: TAppTheme); virtual;
|
procedure SetTheme(const T: TAppTheme); virtual;
|
||||||
procedure InvalidateRulerCache; virtual;
|
procedure InvalidateRulerCache; virtual;
|
||||||
procedure ResetSpectrumBuf; virtual;
|
procedure ResetSpectrumBuf; virtual;
|
||||||
@@ -329,6 +341,7 @@ begin
|
|||||||
FWaterfall := TWaterfallView.Create;
|
FWaterfall := TWaterfallView.Create;
|
||||||
FSMeter := TSMeterView.Create;
|
FSMeter := TSMeterView.Create;
|
||||||
FRuler := TRulerView.Create;
|
FRuler := TRulerView.Create;
|
||||||
|
FSliceOverlays := TFPList.Create;
|
||||||
ResetSpectrumBuf;
|
ResetSpectrumBuf;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -339,9 +352,30 @@ begin
|
|||||||
FWaterfall.Free;
|
FWaterfall.Free;
|
||||||
FSMeter.Free;
|
FSMeter.Free;
|
||||||
FRuler.Free;
|
FRuler.Free;
|
||||||
|
FSliceOverlays.Free; // не владеет флагами — только список
|
||||||
inherited;
|
inherited;
|
||||||
end;
|
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
|
// Setters with cascade to sub-views
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -583,6 +617,32 @@ begin
|
|||||||
end;
|
end;
|
||||||
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);
|
procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer);
|
||||||
var
|
var
|
||||||
Y: Integer;
|
Y: Integer;
|
||||||
@@ -620,14 +680,21 @@ end;
|
|||||||
|
|
||||||
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||||
out X1, X2, VfoX: 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
|
var
|
||||||
Lo_Hz, Hi_Hz, Half: Double;
|
Lo_Hz, Hi_Hz, Half: Double;
|
||||||
begin
|
begin
|
||||||
Half := FFilterBW / 2;
|
Half := BW / 2;
|
||||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||||
case FMode of
|
case Mode of
|
||||||
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
0: begin Lo_Hz := -BW; Hi_Hz := -100; end;
|
||||||
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
1: begin Lo_Hz := 100; Hi_Hz := BW; end;
|
||||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||||
end;
|
end;
|
||||||
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||||
@@ -737,6 +804,13 @@ begin
|
|||||||
FSpectrumDirty := True;
|
FSpectrumDirty := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TSpectrumView.InvalidateVfoOverlay;
|
||||||
|
begin
|
||||||
|
// База (CPU-путь) перерисовывает флаг по FCacheDirty/FMeterDirty оверлея — здесь
|
||||||
|
// достаточно пометить спектр грязным. GL-подкласс переопределяет.
|
||||||
|
FSpectrumDirty := True;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TSpectrumView.SetTheme(const T: TAppTheme);
|
procedure TSpectrumView.SetTheme(const T: TAppTheme);
|
||||||
begin
|
begin
|
||||||
FTheme := T;
|
FTheme := T;
|
||||||
@@ -905,6 +979,9 @@ begin
|
|||||||
C.Pen.Style := psSolid;
|
C.Pen.Style := psSolid;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Полосы фильтра слайсов (B+) — под кривой спектра, как в GL-вьюхе.
|
||||||
|
DrawSliceFilterMarkers(C, W, H);
|
||||||
|
|
||||||
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
||||||
if FWDSPReady then
|
if FWDSPReady then
|
||||||
begin
|
begin
|
||||||
@@ -1009,6 +1086,9 @@ begin
|
|||||||
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
||||||
if Assigned(FVfoOverlay) then
|
if Assigned(FVfoOverlay) then
|
||||||
FVfoOverlay.DrawOverlay(FSpectrumBitmap, W, H);
|
FVfoOverlay.DrawOverlay(FSpectrumBitmap, W, H);
|
||||||
|
// Доп. слайс-флаги (B+) поверх главного.
|
||||||
|
for i := 0 to FSliceOverlays.Count - 1 do
|
||||||
|
TVfoOverlay(FSliceOverlays[i]).DrawOverlay(FSpectrumBitmap, W, H);
|
||||||
|
|
||||||
// Восстановление непрозрачности. На Cocoa проверено, что проход не нужен
|
// Восстановление непрозрачности. На Cocoa проверено, что проход не нужен
|
||||||
// (Canvas-операции сохраняют альфу $FF), поэтому там его не делаем.
|
// (Canvas-операции сохраняют альфу $FF), поэтому там его не делаем.
|
||||||
|
|||||||
+112
-7
@@ -43,6 +43,12 @@ type
|
|||||||
FSampleOverlayTex: TGLTextureCache;
|
FSampleOverlayTex: TGLTextureCache;
|
||||||
FVfoOverlayTex: TGLTextureCache;
|
FVfoOverlayTex: TGLTextureCache;
|
||||||
FBandOverlayTex: TGLTextureCache;
|
FBandOverlayTex: TGLTextureCache;
|
||||||
|
// Текстуры флагов слайсов (B+), привязка по указателю оверлея.
|
||||||
|
FSliceTex: array of record
|
||||||
|
Overlay: TVfoOverlay;
|
||||||
|
Tex: TGLTextureCache;
|
||||||
|
LastX, LastY, LastW, LastH: Integer;
|
||||||
|
end;
|
||||||
FLastAGCText: string;
|
FLastAGCText: string;
|
||||||
FLastAGCY: Integer;
|
FLastAGCY: Integer;
|
||||||
FLastAGCHangY: Integer;
|
FLastAGCHangY: Integer;
|
||||||
@@ -83,6 +89,9 @@ type
|
|||||||
procedure DrawBeaconMarkersGL(W, H: Integer);
|
procedure DrawBeaconMarkersGL(W, H: Integer);
|
||||||
procedure DrawADCOverlay(W, H: Integer);
|
procedure DrawADCOverlay(W, H: Integer);
|
||||||
procedure DrawCachedOverlays(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 FormatFreqGL(Hz: Double): string;
|
||||||
function ActiveVfoFrequency: Double;
|
function ActiveVfoFrequency: Double;
|
||||||
public
|
public
|
||||||
@@ -96,6 +105,7 @@ type
|
|||||||
procedure PaintSpectrum(Sender: TObject); override;
|
procedure PaintSpectrum(Sender: TObject); override;
|
||||||
procedure InvalidateGridCache; override;
|
procedure InvalidateGridCache; override;
|
||||||
procedure InvalidateOverlayCache; override;
|
procedure InvalidateOverlayCache; override;
|
||||||
|
procedure InvalidateVfoOverlay; override;
|
||||||
procedure SetTheme(const T: TAppTheme); override;
|
procedure SetTheme(const T: TAppTheme); override;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -138,6 +148,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TSpectrumViewOpenGL.Destroy;
|
destructor TSpectrumViewOpenGL.Destroy;
|
||||||
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
if Assigned(FGLControl) and FGLControl.MakeCurrent then
|
if Assigned(FGLControl) and FGLControl.MakeCurrent then
|
||||||
begin
|
begin
|
||||||
@@ -149,7 +160,9 @@ begin
|
|||||||
DeleteTexture(FSampleOverlayTex);
|
DeleteTexture(FSampleOverlayTex);
|
||||||
DeleteTexture(FVfoOverlayTex);
|
DeleteTexture(FVfoOverlayTex);
|
||||||
DeleteTexture(FBandOverlayTex);
|
DeleteTexture(FBandOverlayTex);
|
||||||
|
for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex);
|
||||||
end;
|
end;
|
||||||
|
SetLength(FSliceTex, 0);
|
||||||
inherited Destroy;
|
inherited Destroy;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -392,6 +405,13 @@ begin
|
|||||||
FVfoOverlayDirty := True;
|
FVfoOverlayDirty := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TSpectrumViewOpenGL.InvalidateVfoOverlay;
|
||||||
|
begin
|
||||||
|
// Только текстура главного флага — НЕ трогаем FOverlayDirty, иначе band/
|
||||||
|
// samplerate/слайсы зря перезаливались бы на каждый тик S-метра главного.
|
||||||
|
FVfoOverlayDirty := True;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TSpectrumViewOpenGL.SetTheme(const T: TAppTheme);
|
procedure TSpectrumViewOpenGL.SetTheme(const T: TAppTheme);
|
||||||
begin
|
begin
|
||||||
inherited SetTheme(T);
|
inherited SetTheme(T);
|
||||||
@@ -536,6 +556,29 @@ begin
|
|||||||
DrawLine(TXVfoX, 0, TXVfoX, H - 12, TColor($002030E0), 2, False);
|
DrawLine(TXVfoX, 0, TXVfoX, H - 12, TColor($002030E0), 2, False);
|
||||||
DrawRect(TXVfoX - 5, 0, TXVfoX + 5, 8, TColor($002030E0), 1);
|
DrawRect(TXVfoX - 5, 0, TXVfoX + 5, 8, TColor($002030E0), 1);
|
||||||
end;
|
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;
|
end;
|
||||||
|
|
||||||
procedure TSpectrumViewOpenGL.DrawAGCLines(W, H: Integer; DBmax,
|
procedure TSpectrumViewOpenGL.DrawAGCLines(W, H: Integer; DBmax,
|
||||||
@@ -783,16 +826,11 @@ begin
|
|||||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||||
begin
|
begin
|
||||||
if FOverlayDirty or FVfoOverlayDirty or FVfoOverlayTex.Dirty or
|
if FOverlayDirty or FVfoOverlayDirty or FVfoOverlayTex.Dirty or
|
||||||
|
FVfoOverlay.CacheDirty or FVfoOverlay.MeterDirty or
|
||||||
(FLastVfoX <> FVfoOverlay.Left) or (FLastVfoY <> FVfoOverlay.Top) or
|
(FLastVfoX <> FVfoOverlay.Left) or (FLastVfoY <> FVfoOverlay.Top) or
|
||||||
(FLastVfoW <> FVfoOverlay.Width) or (FLastVfoH <> FVfoOverlay.Height) then
|
(FLastVfoW <> FVfoOverlay.Width) or (FLastVfoH <> FVfoOverlay.Height) then
|
||||||
begin
|
begin
|
||||||
B := TBitmap.Create;
|
UploadBitmap(FVfoOverlayTex, FVfoOverlay.EnsureRendered, True, VFO_OVERLAY_ALPHA);
|
||||||
try
|
|
||||||
FVfoOverlay.DrawOverlayBitmap(B);
|
|
||||||
UploadBitmap(FVfoOverlayTex, B, True, VFO_OVERLAY_ALPHA);
|
|
||||||
finally
|
|
||||||
B.Free;
|
|
||||||
end;
|
|
||||||
FLastVfoX := FVfoOverlay.Left;
|
FLastVfoX := FVfoOverlay.Left;
|
||||||
FLastVfoY := FVfoOverlay.Top;
|
FLastVfoY := FVfoOverlay.Top;
|
||||||
FLastVfoW := FVfoOverlay.Width;
|
FLastVfoW := FVfoOverlay.Width;
|
||||||
@@ -801,9 +839,76 @@ begin
|
|||||||
end;
|
end;
|
||||||
DrawTexture(FVfoOverlayTex, FVfoOverlay.Left, FVfoOverlay.Top);
|
DrawTexture(FVfoOverlayTex, FVfoOverlay.Left, FVfoOverlay.Top);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
DrawSliceOverlays(W, H);
|
||||||
FOverlayDirty := False;
|
FOverlayDirty := False;
|
||||||
end;
|
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);
|
procedure TSpectrumViewOpenGL.PaintSpectrum(Sender: TObject);
|
||||||
var
|
var
|
||||||
W, H: Integer;
|
W, H: Integer;
|
||||||
|
|||||||
+126
-7
@@ -52,6 +52,7 @@ type
|
|||||||
FTx: Boolean; // передача идёт (красный)
|
FTx: Boolean; // передача идёт (красный)
|
||||||
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
|
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
|
||||||
FSlice: Char; // буква слайса ('A')
|
FSlice: Char; // буква слайса ('A')
|
||||||
|
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
|
||||||
|
|
||||||
FDevNames: TStringList; // имена устройств вывода
|
FDevNames: TStringList; // имена устройств вывода
|
||||||
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
|
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
|
||||||
@@ -72,12 +73,21 @@ type
|
|||||||
FOnTxSelect: TNotifyEvent;
|
FOnTxSelect: TNotifyEvent;
|
||||||
FOnInvalidate: TNotifyEvent;
|
FOnInvalidate: TNotifyEvent;
|
||||||
|
|
||||||
|
FSliceId: Integer; // логический id слайса (владелец — MainForm/контроллер)
|
||||||
|
FIsMain: Boolean; // главный приёмник (слайс A) — без кнопки закрытия
|
||||||
|
FOnSliceSelect: TNotifyEvent; // клик по бейджу буквы — сделать слайс активным
|
||||||
|
FOnClose: TNotifyEvent; // клик по × (только неглавные) — удалить слайс
|
||||||
|
|
||||||
FHitRects: array[0..63] of TRect;
|
FHitRects: array[0..63] of TRect;
|
||||||
FHitActions: array[0..63] of Integer;
|
FHitActions: array[0..63] of Integer;
|
||||||
FHitCount: Integer;
|
FHitCount: Integer;
|
||||||
FHotIdx: Integer;
|
FHotIdx: Integer;
|
||||||
FCacheBitmap: TBitmap;
|
FCacheBitmap: TBitmap;
|
||||||
FCacheDirty: Boolean;
|
FCacheDirty: Boolean;
|
||||||
|
FMeterDirty: Boolean; // изменился только S-метр — перерисовать полоску, не весь флаг
|
||||||
|
FMeterRect: TRect; // прямоугольник сигнал-бара (запоминается при полном рендере)
|
||||||
|
FSRowTop: Integer; // верх строки частоты (для S-метки справа)
|
||||||
|
FLastSTextRect: TRect; // последний прямоугольник S-метки (для аккуратной очистки)
|
||||||
|
|
||||||
procedure DrawSelf(C: TCanvas; W, H: Integer);
|
procedure DrawSelf(C: TCanvas; W, H: Integer);
|
||||||
procedure DrawButton(C: TCanvas; const R: TRect; const Lbl: string;
|
procedure DrawButton(C: TCanvas; const R: TRect; const Lbl: string;
|
||||||
@@ -100,6 +110,8 @@ type
|
|||||||
procedure SetPanel(P: TFlyPanel);
|
procedure SetPanel(P: TFlyPanel);
|
||||||
procedure ApplyVolFromX(LX: Integer);
|
procedure ApplyVolFromX(LX: Integer);
|
||||||
procedure RequestInvalidate;
|
procedure RequestInvalidate;
|
||||||
|
procedure RequestMeterInvalidate;
|
||||||
|
procedure RefreshMeter(C: TCanvas);
|
||||||
procedure DoHit(HV: Integer);
|
procedure DoHit(HV: Integer);
|
||||||
|
|
||||||
protected
|
protected
|
||||||
@@ -114,6 +126,10 @@ type
|
|||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
procedure DrawOverlay(Target: TBitmap; W, H: Integer);
|
procedure DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||||
procedure DrawOverlayBitmap(Target: TBitmap);
|
procedure DrawOverlayBitmap(Target: TBitmap);
|
||||||
|
// Готовит внутренний кэш-битмап (полный рендер по CacheDirty либо дешёвая
|
||||||
|
// перерисовка одной полоски метра по MeterDirty) и возвращает его. Один путь
|
||||||
|
// для CPU (BlendBitmapKey) и GL (UploadBitmap).
|
||||||
|
function EnsureRendered: TBitmap;
|
||||||
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
||||||
function HandleMouseMove(X, Y: Integer): Boolean;
|
function HandleMouseMove(X, Y: Integer): Boolean;
|
||||||
function HandleMouseUp(X, Y: Integer): Boolean;
|
function HandleMouseUp(X, Y: Integer): Boolean;
|
||||||
@@ -126,6 +142,20 @@ type
|
|||||||
const CurName: string);
|
const CurName: string);
|
||||||
procedure UpdateSMeter(DB: Double);
|
procedure UpdateSMeter(DB: Double);
|
||||||
procedure UpdateVfo(AVfoHz: 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 HotIdx: Integer read FHotIdx;
|
||||||
property BaseHeight: Integer read ComputeHeight;
|
property BaseHeight: Integer read ComputeHeight;
|
||||||
property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect;
|
property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect;
|
||||||
@@ -136,6 +166,8 @@ type
|
|||||||
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
||||||
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
||||||
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
||||||
|
property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect;
|
||||||
|
property OnClose: TNotifyEvent read FOnClose write FOnClose;
|
||||||
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
|
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -162,6 +194,8 @@ const
|
|||||||
HIT_TX = -26;
|
HIT_TX = -26;
|
||||||
HIT_AUD_UP = -27;
|
HIT_AUD_UP = -27;
|
||||||
HIT_AUD_DN = -28;
|
HIT_AUD_DN = -28;
|
||||||
|
HIT_SLICE_SEL = -29; // клик по бейджу буквы (выбрать слайс активным)
|
||||||
|
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
|
||||||
HIT_AGC_PICK_BASE = 100000;
|
HIT_AGC_PICK_BASE = 100000;
|
||||||
HIT_DEV_BASE = 200000;
|
HIT_DEV_BASE = 200000;
|
||||||
|
|
||||||
@@ -338,6 +372,8 @@ begin
|
|||||||
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
|
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
|
||||||
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
|
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
|
||||||
FTxSel := True; FSlice := 'A';
|
FTxSel := True; FSlice := 'A';
|
||||||
|
FSliceId := 0; FIsMain := True; // по умолчанию — главный приёмник (слайс A)
|
||||||
|
FSMeterThreshold := 0.4; // главный флаг — чувствительный метр
|
||||||
FPanel := fpNone;
|
FPanel := fpNone;
|
||||||
FAudScroll := 0;
|
FAudScroll := 0;
|
||||||
FVolDrag := False;
|
FVolDrag := False;
|
||||||
@@ -346,6 +382,8 @@ begin
|
|||||||
FCacheBitmap := TBitmap.Create;
|
FCacheBitmap := TBitmap.Create;
|
||||||
FCacheBitmap.PixelFormat := pf32bit;
|
FCacheBitmap.PixelFormat := pf32bit;
|
||||||
FCacheDirty := True;
|
FCacheDirty := True;
|
||||||
|
FMeterDirty := False;
|
||||||
|
FLastSTextRect := Rect(0, 0, 0, 0);
|
||||||
Width := OVL_W;
|
Width := OVL_W;
|
||||||
Height := OVL_BASE_H;
|
Height := OVL_BASE_H;
|
||||||
Cursor := crHandPoint;
|
Cursor := crHandPoint;
|
||||||
@@ -373,6 +411,53 @@ begin
|
|||||||
if Assigned(FOnInvalidate) then FOnInvalidate(Self);
|
if Assigned(FOnInvalidate) then FOnInvalidate(Self);
|
||||||
end;
|
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;
|
function TVfoOverlay.FormatFreq(Hz: Double): string;
|
||||||
var
|
var
|
||||||
T, iM, iK, iH: Int64;
|
T, iM, iK, iH: Int64;
|
||||||
@@ -800,8 +885,16 @@ begin
|
|||||||
RW := W - PAD*2;
|
RW := W - PAD*2;
|
||||||
CurY := PAD;
|
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);
|
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])
|
if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0])
|
||||||
else BWStr := IntToStr(FFilterBW);
|
else BWStr := IntToStr(FFilterBW);
|
||||||
@@ -828,10 +921,12 @@ begin
|
|||||||
DrawIndicator(C, X + 48, CurY + 2, 'ANF', FANFOn);
|
DrawIndicator(C, X + 48, CurY + 2, 'ANF', FANFOn);
|
||||||
DrawIndicator(C, X + 80, CurY + 2, 'SNB', FSNBOn);
|
DrawIndicator(C, X + 80, CurY + 2, 'SNB', FSNBOn);
|
||||||
MeterX := X + 112;
|
MeterX := X + 112;
|
||||||
|
FMeterRect := Rect(MeterX, CurY, W - PAD, CurY + MET_H); // для RefreshMeter
|
||||||
DrawMeterBar(C, MeterX, CurY, W - PAD - MeterX, MET_H);
|
DrawMeterBar(C, MeterX, CurY, W - PAD - MeterX, MET_H);
|
||||||
Inc(CurY, MET_H);
|
Inc(CurY, MET_H);
|
||||||
|
|
||||||
{ --- Частота + S --- }
|
{ --- Частота + S --- }
|
||||||
|
FSRowTop := CurY; // для RefreshMeter
|
||||||
FreqStr := FormatFreq(FVfoHz);
|
FreqStr := FormatFreq(FVfoHz);
|
||||||
C.Font.Name := 'Courier New'; C.Font.Size := 13; C.Font.Style := [fsBold];
|
C.Font.Name := 'Courier New'; C.Font.Size := 13; C.Font.Style := [fsBold];
|
||||||
C.Font.Color := CLR_FREQ; C.Brush.Style := bsClear;
|
C.Font.Color := CLR_FREQ; C.Brush.Style := bsClear;
|
||||||
@@ -839,6 +934,7 @@ begin
|
|||||||
SStr := DBmToSLabel(FSMeterDB);
|
SStr := DBmToSLabel(FSMeterDB);
|
||||||
C.Font.Size := 9; C.Font.Color := CLR_SVAL;
|
C.Font.Size := 9; C.Font.Color := CLR_SVAL;
|
||||||
C.TextOut(W - PAD - C.TextWidth(SStr) - 2, CurY + 6, SStr);
|
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);
|
Inc(CurY, FRQ_H);
|
||||||
|
|
||||||
{ --- Нижний бар: 🔊 vol | MODE DSP AUD --- }
|
{ --- Нижний бар: 🔊 vol | MODE DSP AUD --- }
|
||||||
@@ -899,11 +995,8 @@ begin
|
|||||||
DrawSelf(Canvas, Width, Height);
|
DrawSelf(Canvas, Width, Height);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
function TVfoOverlay.EnsureRendered: TBitmap;
|
||||||
begin
|
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
|
if (FCacheBitmap.Width <> Width) or (FCacheBitmap.Height <> Height) then
|
||||||
begin
|
begin
|
||||||
FCacheBitmap.SetSize(Width, Height);
|
FCacheBitmap.SetSize(Width, Height);
|
||||||
@@ -917,7 +1010,21 @@ begin
|
|||||||
FCacheBitmap.Canvas.FillRect(Rect(0, 0, Width, Height));
|
FCacheBitmap.Canvas.FillRect(Rect(0, 0, Width, Height));
|
||||||
DrawSelf(FCacheBitmap.Canvas, Width, Height);
|
DrawSelf(FCacheBitmap.Canvas, Width, Height);
|
||||||
FCacheDirty := False;
|
FCacheDirty := False;
|
||||||
|
FMeterDirty := False;
|
||||||
|
end
|
||||||
|
else if FMeterDirty then
|
||||||
|
begin
|
||||||
|
RefreshMeter(FCacheBitmap.Canvas); // дешёвый апдейт: только полоска метра
|
||||||
|
FMeterDirty := False;
|
||||||
end;
|
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);
|
BlendBitmapKey(Target, FCacheBitmap, Left, Top, VFO_OVERLAY_ALPHA);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -932,6 +1039,7 @@ begin
|
|||||||
Target.Canvas.FillRect(Rect(0, 0, Width, Height));
|
Target.Canvas.FillRect(Rect(0, 0, Width, Height));
|
||||||
DrawSelf(Target.Canvas, Width, Height);
|
DrawSelf(Target.Canvas, Width, Height);
|
||||||
FCacheDirty := False;
|
FCacheDirty := False;
|
||||||
|
FMeterDirty := False;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ ---- Обработка хитов ---- }
|
{ ---- Обработка хитов ---- }
|
||||||
@@ -949,6 +1057,8 @@ begin
|
|||||||
if Assigned(FOnMute) then FOnMute(Self); Exit; end;
|
if Assigned(FOnMute) then FOnMute(Self); Exit; end;
|
||||||
HIT_SPLIT: begin if Assigned(FOnSplit) then FOnSplit(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_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);
|
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
|
||||||
RequestInvalidate; Exit; end;
|
RequestInvalidate; Exit; end;
|
||||||
HIT_AUD_DN: begin if FAudScroll < FDevNames.Count - AUD_MAX_VIS then
|
HIT_AUD_DN: begin if FAudScroll < FDevNames.Count - AUD_MAX_VIS then
|
||||||
@@ -1174,6 +1284,15 @@ begin
|
|||||||
RequestInvalidate;
|
RequestInvalidate;
|
||||||
end;
|
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;
|
procedure TVfoOverlay.SetAudioDevices(Names: TStrings; const Indices: array of Integer;
|
||||||
const CurName: string);
|
const CurName: string);
|
||||||
var
|
var
|
||||||
@@ -1190,10 +1309,10 @@ end;
|
|||||||
|
|
||||||
procedure TVfoOverlay.UpdateSMeter(DB: Double);
|
procedure TVfoOverlay.UpdateSMeter(DB: Double);
|
||||||
begin
|
begin
|
||||||
if Abs(DB - FSMeterDB) > 0.4 then
|
if Abs(DB - FSMeterDB) > FSMeterThreshold then
|
||||||
begin
|
begin
|
||||||
FSMeterDB := DB;
|
FSMeterDB := DB;
|
||||||
RequestInvalidate;
|
RequestMeterInvalidate; // только полоска метра, без ре-рендера всего флага
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+517
-81
@@ -96,6 +96,10 @@ const
|
|||||||
RXA_CHAN = 0;
|
RXA_CHAN = 0;
|
||||||
TXA_CHAN = 1; // используем разные каналы для RX и TX
|
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-спектра/водопада
|
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||||
DSP_BUFSIZE = 1024;
|
DSP_BUFSIZE = 1024;
|
||||||
@@ -135,6 +139,10 @@ type
|
|||||||
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
||||||
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
||||||
Count: Integer) of object;
|
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 дёргает этот колбэк перед каждым тиком,
|
// Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком,
|
||||||
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
|
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
|
||||||
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
|
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
|
||||||
@@ -143,6 +151,28 @@ type
|
|||||||
// TX mic source: откуда получаем сэмплы для модулятора.
|
// TX mic source: откуда получаем сэмплы для модулятора.
|
||||||
TTXMicSource = (txmsRadio, txmsSoundCard, txmsWeb);
|
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 }
|
||||||
TWDSPEngine = class
|
TWDSPEngine = class
|
||||||
private
|
private
|
||||||
@@ -219,6 +249,15 @@ type
|
|||||||
FOnWaterfall: TOnWaterfallReady;
|
FOnWaterfall: TOnWaterfallReady;
|
||||||
FOnTXIQ: TOnTXIQReady;
|
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,
|
// TX mic ring buffer (lock-free: один writer — receive thread,
|
||||||
// один reader — TX DSP thread)
|
// один reader — TX DSP thread)
|
||||||
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
|
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
|
||||||
@@ -311,6 +350,13 @@ type
|
|||||||
|
|
||||||
function ModeToWDSP(Mode: Integer): Integer;
|
function ModeToWDSP(Mode: Integer): Integer;
|
||||||
procedure ApplyDefaultFilter;
|
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 FeedDisplaySample(const I, Q: Double);
|
||||||
procedure ProcessRXBlock;
|
procedure ProcessRXBlock;
|
||||||
procedure PushIQItemToDSP(const Item: TIQQueueItem);
|
procedure PushIQItemToDSP(const Item: TIQQueueItem);
|
||||||
@@ -329,6 +375,10 @@ type
|
|||||||
procedure ApplyNBState;
|
procedure ApplyNBState;
|
||||||
procedure ApplySNBState;
|
procedure ApplySNBState;
|
||||||
procedure ApplyANFState;
|
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 ApplyNoiseFilterState;
|
||||||
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
||||||
procedure FeedTXDisplaySample(const I, Q: Double);
|
procedure FeedTXDisplaySample(const I, Q: Double);
|
||||||
@@ -350,6 +400,26 @@ type
|
|||||||
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
|
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
|
||||||
procedure SetBeaconDecoder(D: TBeaconDecoder);
|
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 пакета
|
// Подача 24-bit big-endian IQ из DDC пакета
|
||||||
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
|
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
|
||||||
procedure PushDDCPacket(const Buf: array of Byte;
|
procedure PushDDCPacket(const Buf: array of Byte;
|
||||||
@@ -475,6 +545,7 @@ type
|
|||||||
property WfAvgMode: Integer read FWfAvgMode;
|
property WfAvgMode: Integer read FWfAvgMode;
|
||||||
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
||||||
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
||||||
|
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
||||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||||
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
||||||
@@ -813,6 +884,12 @@ begin
|
|||||||
FDSPThread := nil;
|
FDSPThread := nil;
|
||||||
FDisplayThread := nil;
|
FDisplayThread := nil;
|
||||||
|
|
||||||
|
// Мультислайсы
|
||||||
|
FSliceLock := TCriticalSection.Create;
|
||||||
|
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||||
|
FOnSliceAudio := nil;
|
||||||
|
AllocSliceBuffers;
|
||||||
|
|
||||||
// TX инициализация
|
// TX инициализация
|
||||||
FTXMicHead := 0;
|
FTXMicHead := 0;
|
||||||
FTXMicTail := 0;
|
FTXMicTail := 0;
|
||||||
@@ -887,6 +964,7 @@ begin
|
|||||||
Close;
|
Close;
|
||||||
RTLEventDestroy(FQueueSem);
|
RTLEventDestroy(FQueueSem);
|
||||||
RTLEventDestroy(FTXMicSem);
|
RTLEventDestroy(FTXMicSem);
|
||||||
|
FSliceLock.Free;
|
||||||
inherited;
|
inherited;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1052,6 +1130,7 @@ end;
|
|||||||
function TWDSPEngine.Open: Boolean;
|
function TWDSPEngine.Open: Boolean;
|
||||||
var
|
var
|
||||||
DllPath: string;
|
DllPath: string;
|
||||||
|
i: Integer;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
FLastError := '';
|
FLastError := '';
|
||||||
@@ -1131,6 +1210,18 @@ begin
|
|||||||
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
||||||
ApplyTXChainSettings;
|
ApplyTXChainSettings;
|
||||||
ApplyNoiseFilterState;
|
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;
|
Result := True;
|
||||||
|
|
||||||
// Запускаем DSP поток после успешной инициализации WDSP
|
// Запускаем DSP поток после успешной инициализации WDSP
|
||||||
@@ -1149,6 +1240,8 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.Close;
|
procedure TWDSPEngine.Close;
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
begin
|
begin
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
|
|
||||||
@@ -1161,6 +1254,15 @@ begin
|
|||||||
FreeAndNil(FDSPThread);
|
FreeAndNil(FDSPThread);
|
||||||
end;
|
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;
|
CloseAnalyzer;
|
||||||
destroy_anbEXT(RXA_CHAN);
|
destroy_anbEXT(RXA_CHAN);
|
||||||
destroy_nobEXT(RXA_CHAN);
|
destroy_nobEXT(RXA_CHAN);
|
||||||
@@ -1195,6 +1297,7 @@ begin
|
|||||||
SetLength(FRXAccQ, FBufSize);
|
SetLength(FRXAccQ, FBufSize);
|
||||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||||
FDispPos := 0;
|
FDispPos := 0;
|
||||||
|
AllocSliceBuffers;
|
||||||
|
|
||||||
// Переинициализируем WDSP с новым rate
|
// Переинициализируем WDSP с новым rate
|
||||||
Open;
|
Open;
|
||||||
@@ -1379,11 +1482,360 @@ begin
|
|||||||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
|
||||||
|
ProcessSlices;
|
||||||
|
|
||||||
except
|
except
|
||||||
// Защита от падения libwdsp в потоке — просто пропускаем блок
|
// Защита от падения libwdsp в потоке — просто пропускаем блок
|
||||||
end;
|
end;
|
||||||
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 управление
|
// RX управление
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1412,42 +1864,7 @@ begin
|
|||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
|
|
||||||
// Точно по piHPSDR receiver.c: set_agc()
|
// Точно по piHPSDR receiver.c: set_agc()
|
||||||
SetRXAAGCMode(RXA_CHAN, Ord(Mode));
|
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
|
||||||
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;
|
|
||||||
|
|
||||||
// Читаем обратно hang level и thresh для линии на спектре
|
// Читаем обратно hang level и thresh для линии на спектре
|
||||||
UpdateAGCLines(FLastSpectrumW);
|
UpdateAGCLines(FLastSpectrumW);
|
||||||
@@ -1502,69 +1919,88 @@ begin
|
|||||||
FSampleRate);
|
FSampleRate);
|
||||||
end;
|
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;
|
procedure TWDSPEngine.ApplyNRState;
|
||||||
begin
|
begin
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
|
ApplyNRToChan(RXA_CHAN, FNRMode);
|
||||||
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));
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.ApplyNBState;
|
procedure TWDSPEngine.ApplyNBState;
|
||||||
begin
|
begin
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
|
ApplyNBToChan(RXA_CHAN, FNBMode);
|
||||||
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));
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.ApplySNBState;
|
procedure TWDSPEngine.ApplySNBState;
|
||||||
begin
|
begin
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
SetRXASNBARun(RXA_CHAN, Ord(FSNBEnabled));
|
ApplySNBToChan(RXA_CHAN, FSNBEnabled);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.ApplyANFState;
|
procedure TWDSPEngine.ApplyANFState;
|
||||||
begin
|
begin
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
SetRXAANFPosition(RXA_CHAN, FILTER_NR_AGC_POSITION);
|
ApplyANFToChan(RXA_CHAN, FANFEnabled);
|
||||||
SetRXAANFRun(RXA_CHAN, Ord(FANFEnabled));
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.ApplyNoiseFilterState;
|
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"/>
|
<UseVersionInfo Value="True"/>
|
||||||
<AutoIncrementBuild Value="True"/>
|
<AutoIncrementBuild Value="True"/>
|
||||||
<MinorVersionNr Value="9"/>
|
<MinorVersionNr Value="9"/>
|
||||||
<BuildNr Value="215"/>
|
<BuildNr Value="232"/>
|
||||||
</VersionInfo>
|
</VersionInfo>
|
||||||
<MacroValues Count="27">
|
<MacroValues Count="50">
|
||||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||||
@@ -47,6 +47,29 @@
|
|||||||
<Macro25 Name="LCLWidgetType" Value="qt6"/>
|
<Macro25 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro26 Name="LCLWidgetType" Value="qt6"/>
|
<Macro26 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro27 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>
|
</MacroValues>
|
||||||
<BuildModes>
|
<BuildModes>
|
||||||
<Item Name="Debug" Default="True"/>
|
<Item Name="Debug" Default="True"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user