mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(slices): multi-slice receivers (FlexRadio-style) — soft fan-out + per-slice flag/audio
Independent RX slices sharing one panadapter: each has its own freq/mode/filter/AGC/NR-NB-SNB-ANF, S-meter, volume and sound card. VFO A = main receiver; B+ are soft fan-out WDSP channels within the captured span (ideal for Pluto/QO-100; HW DDC path is Stage 3). - WDSPEngine: soft fan-out (TSliceChan, ProcessSlices in DSP thread, per-slice NCO/mode/filter/AGC/NR/NB/SNB/ANF + S-meter, OnSliceAudio), FSliceLock serialises add/remove/set vs DSP; reopened on rate change. - RadioController: TCtrlSlice with its own TAudioOutput/device, Add/Remove/SetSlice*, RefreshSliceShifts keeps slices on target Hz across centre changes. Slice letters start at 'B' (A = main receiver). - UI: per-slice flags (VfoOverlay) with letter badge/close, filter-band markers in BOTH GL and CPU renderers, drag-retune, wheel; flags placed beside the filter band with flip, like the main flag. Ctrl+click adds. - Main flag (slice A) visible whenever the left panel is hidden OR any slice exists; synced with panel-driven DSP/mode/AGC changes. - Perf: S-meter decoupled from flag chrome cache (FMeterDirty + meter-only RefreshMeter via EnsureRendered); InvalidateVfoOverlay avoids global overlay-cache churn on meter ticks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+126
-7
@@ -52,6 +52,7 @@ type
|
||||
FTx: Boolean; // передача идёт (красный)
|
||||
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
|
||||
FSlice: Char; // буква слайса ('A')
|
||||
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
|
||||
|
||||
FDevNames: TStringList; // имена устройств вывода
|
||||
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
|
||||
@@ -72,12 +73,21 @@ type
|
||||
FOnTxSelect: TNotifyEvent;
|
||||
FOnInvalidate: TNotifyEvent;
|
||||
|
||||
FSliceId: Integer; // логический id слайса (владелец — MainForm/контроллер)
|
||||
FIsMain: Boolean; // главный приёмник (слайс A) — без кнопки закрытия
|
||||
FOnSliceSelect: TNotifyEvent; // клик по бейджу буквы — сделать слайс активным
|
||||
FOnClose: TNotifyEvent; // клик по × (только неглавные) — удалить слайс
|
||||
|
||||
FHitRects: array[0..63] of TRect;
|
||||
FHitActions: array[0..63] of Integer;
|
||||
FHitCount: Integer;
|
||||
FHotIdx: Integer;
|
||||
FCacheBitmap: TBitmap;
|
||||
FCacheDirty: Boolean;
|
||||
FMeterDirty: Boolean; // изменился только S-метр — перерисовать полоску, не весь флаг
|
||||
FMeterRect: TRect; // прямоугольник сигнал-бара (запоминается при полном рендере)
|
||||
FSRowTop: Integer; // верх строки частоты (для S-метки справа)
|
||||
FLastSTextRect: TRect; // последний прямоугольник S-метки (для аккуратной очистки)
|
||||
|
||||
procedure DrawSelf(C: TCanvas; W, H: Integer);
|
||||
procedure DrawButton(C: TCanvas; const R: TRect; const Lbl: string;
|
||||
@@ -100,6 +110,8 @@ type
|
||||
procedure SetPanel(P: TFlyPanel);
|
||||
procedure ApplyVolFromX(LX: Integer);
|
||||
procedure RequestInvalidate;
|
||||
procedure RequestMeterInvalidate;
|
||||
procedure RefreshMeter(C: TCanvas);
|
||||
procedure DoHit(HV: Integer);
|
||||
|
||||
protected
|
||||
@@ -114,6 +126,10 @@ type
|
||||
destructor Destroy; override;
|
||||
procedure DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
procedure DrawOverlayBitmap(Target: TBitmap);
|
||||
// Готовит внутренний кэш-битмап (полный рендер по CacheDirty либо дешёвая
|
||||
// перерисовка одной полоски метра по MeterDirty) и возвращает его. Один путь
|
||||
// для CPU (BlendBitmapKey) и GL (UploadBitmap).
|
||||
function EnsureRendered: TBitmap;
|
||||
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
||||
function HandleMouseMove(X, Y: Integer): Boolean;
|
||||
function HandleMouseUp(X, Y: Integer): Boolean;
|
||||
@@ -126,6 +142,20 @@ type
|
||||
const CurName: string);
|
||||
procedure UpdateSMeter(DB: Double);
|
||||
procedure UpdateVfo(AVfoHz: Double);
|
||||
// Привязка к слайсу: id, буква флага, признак главного (слайс A).
|
||||
procedure SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
|
||||
property SliceId: Integer read FSliceId;
|
||||
property IsMain: Boolean read FIsMain;
|
||||
// Для маркера фильтра слайса на спектре (читает GL-вьюха).
|
||||
property DisplayVfoHz: Double read FVfoHz;
|
||||
property DisplayMode: Integer read FMode;
|
||||
property DisplayBW: Integer read FFilterBW;
|
||||
// Битмап-кэш требует перерисовки (GL-вьюха решает, перезаливать ли текстуру).
|
||||
property CacheDirty: Boolean read FCacheDirty;
|
||||
// Изменился только S-метр (перезалить текстуру, но рендер дешёвый — одна полоска).
|
||||
property MeterDirty: Boolean read FMeterDirty;
|
||||
// Порог изменения S-метра (дБ) для перерисовки: у слайсов выше (меньше рендеров).
|
||||
property SMeterThreshold: Double read FSMeterThreshold write FSMeterThreshold;
|
||||
property HotIdx: Integer read FHotIdx;
|
||||
property BaseHeight: Integer read ComputeHeight;
|
||||
property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect;
|
||||
@@ -136,6 +166,8 @@ type
|
||||
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
||||
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
||||
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
||||
property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect;
|
||||
property OnClose: TNotifyEvent read FOnClose write FOnClose;
|
||||
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
|
||||
end;
|
||||
|
||||
@@ -162,6 +194,8 @@ const
|
||||
HIT_TX = -26;
|
||||
HIT_AUD_UP = -27;
|
||||
HIT_AUD_DN = -28;
|
||||
HIT_SLICE_SEL = -29; // клик по бейджу буквы (выбрать слайс активным)
|
||||
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
|
||||
HIT_AGC_PICK_BASE = 100000;
|
||||
HIT_DEV_BASE = 200000;
|
||||
|
||||
@@ -338,6 +372,8 @@ begin
|
||||
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
|
||||
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
|
||||
FTxSel := True; FSlice := 'A';
|
||||
FSliceId := 0; FIsMain := True; // по умолчанию — главный приёмник (слайс A)
|
||||
FSMeterThreshold := 0.4; // главный флаг — чувствительный метр
|
||||
FPanel := fpNone;
|
||||
FAudScroll := 0;
|
||||
FVolDrag := False;
|
||||
@@ -346,6 +382,8 @@ begin
|
||||
FCacheBitmap := TBitmap.Create;
|
||||
FCacheBitmap.PixelFormat := pf32bit;
|
||||
FCacheDirty := True;
|
||||
FMeterDirty := False;
|
||||
FLastSTextRect := Rect(0, 0, 0, 0);
|
||||
Width := OVL_W;
|
||||
Height := OVL_BASE_H;
|
||||
Cursor := crHandPoint;
|
||||
@@ -373,6 +411,53 @@ begin
|
||||
if Assigned(FOnInvalidate) then FOnInvalidate(Self);
|
||||
end;
|
||||
|
||||
// Лёгкая инвалидация: изменился только S-метр. НЕ трогает FCacheDirty, поэтому
|
||||
// хром флага (частота/режим/кнопки) не перерисовывается — перезаливается только
|
||||
// полоска метра (RefreshMeter в EnsureRendered).
|
||||
procedure TVfoOverlay.RequestMeterInvalidate;
|
||||
begin
|
||||
FMeterDirty := True;
|
||||
inherited Invalidate;
|
||||
if Assigned(FOnInvalidate) then FOnInvalidate(Self);
|
||||
end;
|
||||
|
||||
// Дешёвая перерисовка одной полоски метра + S-метки поверх уже отрендеренного
|
||||
// хрома. Обе зоны самодостаточны и целиком очищаются под фон карточки.
|
||||
procedure TVfoOverlay.RefreshMeter(C: TCanvas);
|
||||
var
|
||||
SStr: string;
|
||||
STextW: Integer;
|
||||
ClearR, NewR: TRect;
|
||||
begin
|
||||
// Сигнал-бар — самодостаточный прямоугольник, перерисовывается целиком.
|
||||
C.Brush.Color := CLR_CARD_BG;
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Pen.Style := psClear;
|
||||
C.FillRect(FMeterRect);
|
||||
DrawMeterBar(C, FMeterRect.Left, FMeterRect.Top,
|
||||
FMeterRect.Right - FMeterRect.Left, MET_H);
|
||||
|
||||
// S-метка в строке частоты (справа). Очищаем объединение старого и нового
|
||||
// прямоугольника, чтобы не оставалось «хвостов» при сужении текста.
|
||||
SStr := DBmToSLabel(FSMeterDB);
|
||||
C.Font.Name := 'Courier New';
|
||||
C.Font.Style := [fsBold];
|
||||
C.Font.Size := 9;
|
||||
STextW := C.TextWidth(SStr);
|
||||
NewR := Rect(FMeterRect.Right - STextW - 3, FSRowTop + 4, FMeterRect.Right, FSRowTop + FRQ_H);
|
||||
ClearR := NewR;
|
||||
if FLastSTextRect.Left < ClearR.Left then ClearR.Left := FLastSTextRect.Left;
|
||||
if FLastSTextRect.Top < ClearR.Top then ClearR.Top := FLastSTextRect.Top;
|
||||
C.Brush.Color := CLR_CARD_BG;
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Pen.Style := psClear;
|
||||
C.FillRect(ClearR);
|
||||
C.Font.Color := CLR_SVAL;
|
||||
C.Brush.Style := bsClear;
|
||||
C.TextOut(FMeterRect.Right - STextW - 2, FSRowTop + 6, SStr);
|
||||
FLastSTextRect := NewR;
|
||||
end;
|
||||
|
||||
function TVfoOverlay.FormatFreq(Hz: Double): string;
|
||||
var
|
||||
T, iM, iK, iH: Int64;
|
||||
@@ -800,8 +885,16 @@ begin
|
||||
RW := W - PAD*2;
|
||||
CurY := PAD;
|
||||
|
||||
{ --- Шапка: [A] BW ... SPLIT [TX] --- }
|
||||
{ --- Шапка: [A] BW ... [×] SPLIT [TX] --- }
|
||||
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, CLR_ACCENT, CLR_CARD_BG);
|
||||
RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
|
||||
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
|
||||
if not FIsMain then
|
||||
begin
|
||||
R := Rect(W - PAD - 26 - 44 - 6 - 14, CurY, W - PAD - 26 - 44 - 6, CurY + HDR_BADGE_H);
|
||||
DrawBadge(C, R, #$C3#$97, CLR_BTN_NORM, CLR_DIM); // UTF-8 '×'
|
||||
RegisterHit(R, HIT_CLOSE);
|
||||
end;
|
||||
// ширина фильтра
|
||||
if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0])
|
||||
else BWStr := IntToStr(FFilterBW);
|
||||
@@ -828,10 +921,12 @@ begin
|
||||
DrawIndicator(C, X + 48, CurY + 2, 'ANF', FANFOn);
|
||||
DrawIndicator(C, X + 80, CurY + 2, 'SNB', FSNBOn);
|
||||
MeterX := X + 112;
|
||||
FMeterRect := Rect(MeterX, CurY, W - PAD, CurY + MET_H); // для RefreshMeter
|
||||
DrawMeterBar(C, MeterX, CurY, W - PAD - MeterX, MET_H);
|
||||
Inc(CurY, MET_H);
|
||||
|
||||
{ --- Частота + S --- }
|
||||
FSRowTop := CurY; // для RefreshMeter
|
||||
FreqStr := FormatFreq(FVfoHz);
|
||||
C.Font.Name := 'Courier New'; C.Font.Size := 13; C.Font.Style := [fsBold];
|
||||
C.Font.Color := CLR_FREQ; C.Brush.Style := bsClear;
|
||||
@@ -839,6 +934,7 @@ begin
|
||||
SStr := DBmToSLabel(FSMeterDB);
|
||||
C.Font.Size := 9; C.Font.Color := CLR_SVAL;
|
||||
C.TextOut(W - PAD - C.TextWidth(SStr) - 2, CurY + 6, SStr);
|
||||
FLastSTextRect := Rect(W - PAD - C.TextWidth(SStr) - 3, CurY + 4, W - PAD, CurY + FRQ_H);
|
||||
Inc(CurY, FRQ_H);
|
||||
|
||||
{ --- Нижний бар: 🔊 vol | MODE DSP AUD --- }
|
||||
@@ -899,11 +995,8 @@ begin
|
||||
DrawSelf(Canvas, Width, Height);
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
function TVfoOverlay.EnsureRendered: TBitmap;
|
||||
begin
|
||||
if (not Visible) or (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
|
||||
if (Left >= W) or (Top >= H) then Exit;
|
||||
|
||||
if (FCacheBitmap.Width <> Width) or (FCacheBitmap.Height <> Height) then
|
||||
begin
|
||||
FCacheBitmap.SetSize(Width, Height);
|
||||
@@ -917,7 +1010,21 @@ begin
|
||||
FCacheBitmap.Canvas.FillRect(Rect(0, 0, Width, Height));
|
||||
DrawSelf(FCacheBitmap.Canvas, Width, Height);
|
||||
FCacheDirty := False;
|
||||
FMeterDirty := False;
|
||||
end
|
||||
else if FMeterDirty then
|
||||
begin
|
||||
RefreshMeter(FCacheBitmap.Canvas); // дешёвый апдейт: только полоска метра
|
||||
FMeterDirty := False;
|
||||
end;
|
||||
Result := FCacheBitmap;
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
begin
|
||||
if (not Visible) or (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
|
||||
if (Left >= W) or (Top >= H) then Exit;
|
||||
EnsureRendered;
|
||||
BlendBitmapKey(Target, FCacheBitmap, Left, Top, VFO_OVERLAY_ALPHA);
|
||||
end;
|
||||
|
||||
@@ -932,6 +1039,7 @@ begin
|
||||
Target.Canvas.FillRect(Rect(0, 0, Width, Height));
|
||||
DrawSelf(Target.Canvas, Width, Height);
|
||||
FCacheDirty := False;
|
||||
FMeterDirty := False;
|
||||
end;
|
||||
|
||||
{ ---- Обработка хитов ---- }
|
||||
@@ -949,6 +1057,8 @@ begin
|
||||
if Assigned(FOnMute) then FOnMute(Self); Exit; end;
|
||||
HIT_SPLIT: begin if Assigned(FOnSplit) then FOnSplit(Self); Exit; end;
|
||||
HIT_TX: begin if Assigned(FOnTxSelect) then FOnTxSelect(Self); Exit; end;
|
||||
HIT_SLICE_SEL: begin if Assigned(FOnSliceSelect) then FOnSliceSelect(Self); Exit; end;
|
||||
HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end;
|
||||
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
|
||||
RequestInvalidate; Exit; end;
|
||||
HIT_AUD_DN: begin if FAudScroll < FDevNames.Count - AUD_MAX_VIS then
|
||||
@@ -1174,6 +1284,15 @@ begin
|
||||
RequestInvalidate;
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
|
||||
begin
|
||||
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;
|
||||
FSliceId := ASliceId;
|
||||
FSlice := ALetter;
|
||||
FIsMain := AIsMain;
|
||||
RequestInvalidate;
|
||||
end;
|
||||
|
||||
procedure TVfoOverlay.SetAudioDevices(Names: TStrings; const Indices: array of Integer;
|
||||
const CurName: string);
|
||||
var
|
||||
@@ -1190,10 +1309,10 @@ end;
|
||||
|
||||
procedure TVfoOverlay.UpdateSMeter(DB: Double);
|
||||
begin
|
||||
if Abs(DB - FSMeterDB) > 0.4 then
|
||||
if Abs(DB - FSMeterDB) > FSMeterThreshold then
|
||||
begin
|
||||
FSMeterDB := DB;
|
||||
RequestInvalidate;
|
||||
RequestMeterInvalidate; // только полоска метра, без ре-рендера всего флага
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user