feat(slices): этап 3.2 — WDSPEngine per-pan (аккумулятор, анализаторы, слайсы в панах)

- общая IQ-очередь с TIQQueueItem.PanId (64→256 слотов); DSP-поток
  диспетчеризует пан-пакеты в PushPanItemToDSP: декод, feed анализатора
  PAN_DISP_BASE+PanId, аккумулятор, ProcessSlicesFor(PanId) под FSliceLock
- пан-анализаторы: Create/DestroyPanAnalyzer + ApplyPanAnalyzerSettings
  (rate пана, без зума — пан-зум в 3.3+), живут в OpenAnalyzer/CloseAnalyzer,
  общие настройки дисплея перекатываются (ReapplyPanDisplaySettings)
- слайсы в панах: TSliceChan.PanId, AddSlice(..., APanId), OpenSliceChannel
  от rate/блока своего пана, SliceDSPRateFor (WFM от rate пана); блок пана
  = FAudioBufSize*Rate/FAudioRate => аудио-блок слайсов неизменен
- выдача пикселей: OnPanSpectrum/OnPanWaterfall(PanId,...) из display-потока,
  GetPixels пана под FSliceLock, колбэки вне лока
- MAX_PANS=4 переехал в WDSPEngine (единый движок/контроллер/UI)
- контроллер: AddPanDDC создаёт пан в движке до включения DDC, RemovePanDDC
  убирает слайсы (со звуковухами)+движок+DDC, гейт/NCO слайсов от центра
  своего пана, ретюн пана двигает его слайсы
- смоук-тест на libwdsp без железа ALL PASS: пик спектра пана точно на месте,
  слайс пана выдал 1 кГц RMS 0.68, главный тракт жив, RemovePan чисто

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 11:45:12 +03:00
co-authored by Claude Fable 5
parent 5dfd7bbb6c
commit 3db823474d
3 changed files with 579 additions and 82 deletions
+62 -26
View File
@@ -53,10 +53,8 @@ uses
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore; Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
const const
// Панадаптеры (этап 3): потолок панов = главный + до 3 доп. аппаратных DDC // Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
// (кап по Ethernet/CPU, см. doc/SLICES_PLAN.md 3.1). Фактический лимит // движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
// бэкенда — Caps.MaxPans (Pluto = 1).
MAX_PANS = 4;
// Допустимые rate доп. пан-DDC, кГц (дефолт 384). // Допустимые rate доп. пан-DDC, кГц (дефолт 384).
PAN_DDC_RATES: array[0..2] of Word = (192, 384, 768); PAN_DDC_RATES: array[0..2] of Word = (192, 384, 768);
@@ -136,6 +134,7 @@ type
Active: Boolean; Active: Boolean;
Id: Integer; // = id слайса в TWDSPEngine Id: Integer; // = id слайса в TWDSPEngine
Letter: Char; // буква флага ('A'..'F'), пока для UI Letter: Char; // буква флага ('A'..'F'), пока для UI
PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан
TargetHz: Double; // абсолютная частота слайса TargetHz: Double; // абсолютная частота слайса
Mode: Integer; Mode: Integer;
FilterLo: Integer; FilterLo: Integer;
@@ -166,9 +165,10 @@ type
function FindSliceIndex(Id: Integer): Integer; // -1 если нет function FindSliceIndex(Id: Integer): Integer; // -1 если нет
procedure OnSliceAudioReady(SliceId: Integer; procedure OnSliceAudioReady(SliceId: Integer;
const Left, Right: array of Single; Count: Integer); const Left, Right: array of Single; Count: Integer);
procedure RefreshSliceShifts; // пересчёт ShiftHz всех слайсов при смене центра procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
// --- Панадаптеры на аппаратных DDC (этап 3.1) --- // --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
function PanCenterHz(PanId: Integer): Double; // центр пана (0 = FCenterFreq)
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)
@@ -534,10 +534,12 @@ type
// ---- Мультислайсы (софт-fan-out) ---- // ---- Мультислайсы (софт-fan-out) ----
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход // Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
// на устройстве DevIndex (-1=default). Возвращает Id слайса или -1 при отказе // на устройстве DevIndex (-1=default). Возвращает Id слайса или -1 при отказе
// (нет слотов / TargetHz вне захваченной полосы). DevName для персиста/UI. // (нет слотов / TargetHz вне полосы пана). DevName для персиста/UI.
// APanId>0 — слайс внутри доп. DDC-пана (тот должен быть создан AddPanDDC).
function AddSlice(TargetHz: Double; Mode, FilterLo, FilterHi: Integer; function AddSlice(TargetHz: Double; Mode, FilterLo, FilterHi: Integer;
AGC: TWDSPAGCMode; Vol: Double; AGC: TWDSPAGCMode; Vol: Double;
DevIndex: Integer; const DevName: string): Integer; DevIndex: Integer; const DevName: string;
APanId: Integer = 0): Integer;
procedure RemoveSlice(Id: Integer); procedure RemoveSlice(Id: Integer);
procedure SetSliceTarget(Id: Integer; TargetHz: Double); procedure SetSliceTarget(Id: Integer; TargetHz: Double);
procedure SetSliceMode(Id, Mode: Integer); procedure SetSliceMode(Id, Mode: Integer);
@@ -554,9 +556,9 @@ type
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string); procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
function SliceCount: Integer; function SliceCount: Integer;
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean; function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
// Диапазон захваченной полосы вокруг центра, куда можно ставить софт-слайс. // Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
procedure CapturedSpan(out LoHz, HiHz: Double); procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
function SliceFitsCapture(TargetHz: Double): Boolean; function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean;
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ---- // ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр (сырые Гц // Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр (сырые Гц
@@ -1128,27 +1130,41 @@ begin
end; end;
procedure TRadioController.RefreshSliceShifts; procedure TRadioController.RefreshSliceShifts;
// Смена центра ГЛАВНОГО захвата: пересчитываем только слайсы пана 0 —
// слайсы доп. панов привязаны к центрам своих DDC (SetPanDDCFreq).
var i: Integer; var i: Integer;
begin begin
if not Assigned(FDSPEngine) then Exit; if not Assigned(FDSPEngine) then Exit;
for i := 0 to MAX_SLICES - 1 do for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active then if FSlices[i].Active and (FSlices[i].PanId = 0) then
FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FCenterFreq); FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FCenterFreq);
end; end;
procedure TRadioController.CapturedSpan(out LoHz, HiHz: Double); function TRadioController.PanCenterHz(PanId: Integer): Double;
// Полезная полоса ±47.5% от захвата (запас на краях фильтра/анти-алиасинга).
var Half: Double;
begin begin
Half := FSampleRate * 0.475; if (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0) then
LoHz := FCenterFreq - Half; Result := FPanFreqHz[PanId]
HiHz := FCenterFreq + Half; else
Result := FCenterFreq;
end; end;
function TRadioController.SliceFitsCapture(TargetHz: Double): Boolean; procedure TRadioController.CapturedSpan(out LoHz, HiHz: Double; PanId: Integer);
// Полезная полоса ±47.5% от захвата (запас на краях фильтра/анти-алиасинга).
var Half, Rate: Double;
begin
if (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0) then
Rate := FPanRateKHz[PanId] * 1000.0
else
Rate := FSampleRate;
Half := Rate * 0.475;
LoHz := PanCenterHz(PanId) - Half;
HiHz := PanCenterHz(PanId) + Half;
end;
function TRadioController.SliceFitsCapture(TargetHz: Double; PanId: Integer): Boolean;
var Lo, Hi: Double; var Lo, Hi: Double;
begin begin
CapturedSpan(Lo, Hi); CapturedSpan(Lo, Hi, PanId);
Result := (TargetHz >= Lo) and (TargetHz <= Hi); Result := (TargetHz >= Lo) and (TargetHz <= Hi);
end; end;
@@ -1191,6 +1207,11 @@ begin
if (i < MAX_DDCS) and (FDDCPanMap[i] < 0) then begin ddc := i; Break; end; if (i < MAX_DDCS) and (FDDCPanMap[i] < 0) then begin ddc := i; Break; end;
if ddc < 0 then Exit; if ddc < 0 then Exit;
// Движок: аккумулятор + analyzer пана (до включения DDC — к приходу первого
// пакета пан уже готов; PushPanIQ без пана просто дропает).
if FWDSPReady and not FDSPEngine.AddPan(PanId, Integer(RateKHz) * 1000) then
Exit;
FPanDDCIdx[PanId] := ddc; FPanDDCIdx[PanId] := ddc;
FPanFreqHz[PanId] := FreqHz; FPanFreqHz[PanId] := FreqHz;
FPanRateKHz[PanId] := RateKHz; FPanRateKHz[PanId] := RateKHz;
@@ -1200,27 +1221,38 @@ begin
end; end;
procedure TRadioController.RemovePanDDC(PanId: Integer); procedure TRadioController.RemovePanDDC(PanId: Integer);
var ddc: Integer; var ddc, i: Integer;
begin begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit; if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
ddc := FPanDDCIdx[PanId]; ddc := FPanDDCIdx[PanId];
if ddc < 0 then Exit; if ddc < 0 then Exit;
// Сначала снимаем роутинг (OnDDCIQ в сетевом потоке перестаёт брать пакеты // Сначала снимаем роутинг (OnDDCIQ в сетевом потоке перестаёт брать пакеты
// этого DDC), потом гасим DDC в железе. // этого DDC), потом слайсы пана (их звуковухи), потом движок и DDC в железе.
FDDCPanMap[ddc] := -1; FDDCPanMap[ddc] := -1;
FPanDDCIdx[PanId] := -1; FPanDDCIdx[PanId] := -1;
FPanFreqHz[PanId] := 0; FPanFreqHz[PanId] := 0;
FPanRateKHz[PanId] := 0; FPanRateKHz[PanId] := 0;
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
RemoveSlice(FSlices[i].Id);
if FWDSPReady then FDSPEngine.RemovePan(PanId);
if Assigned(FNetwork) then FNetwork.SetPanDDC(ddc, False, 0, 0, 0); if Assigned(FNetwork) then FNetwork.SetPanDDC(ddc, False, 0, 0, 0);
end; end;
procedure TRadioController.SetPanDDCFreq(PanId: Integer; FreqHz: Double); procedure TRadioController.SetPanDDCFreq(PanId: Integer; FreqHz: Double);
var i: Integer;
begin begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit; if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
if FPanDDCIdx[PanId] < 0 then Exit; if FPanDDCIdx[PanId] < 0 then Exit;
FPanFreqHz[PanId] := FreqHz; FPanFreqHz[PanId] := FreqHz;
if Assigned(FNetwork) then if Assigned(FNetwork) then
FNetwork.SetPanDDCFreq(FPanDDCIdx[PanId], FreqHz); FNetwork.SetPanDDCFreq(FPanDDCIdx[PanId], FreqHz);
// Слайсы пана держат абсолютный TargetHz — обновляем их NCO-сдвиги
// относительно нового центра (аналог RefreshSliceShifts главного).
if Assigned(FDSPEngine) then
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FreqHz);
end; end;
function TRadioController.PanDDCActive(PanId: Integer): Boolean; function TRadioController.PanDDCActive(PanId: Integer): Boolean;
@@ -1242,14 +1274,16 @@ end;
function TRadioController.AddSlice(TargetHz: Double; function TRadioController.AddSlice(TargetHz: Double;
Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double; Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double;
DevIndex: Integer; const DevName: string): Integer; DevIndex: Integer; const DevName: string; APanId: Integer): Integer;
var var
i, slot, Id: Integer; i, slot, Id: Integer;
AO: TAudioOutput; AO: TAudioOutput;
begin begin
Result := -1; Result := -1;
if not Assigned(FDSPEngine) then Exit; if not Assigned(FDSPEngine) then Exit;
if not SliceFitsCapture(TargetHz) then Exit; // вне захваченной полосы (Этап 3=HW DDC) if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан
if not SliceFitsCapture(TargetHz, APanId) then Exit; // вне полосы пана
slot := -1; slot := -1;
for i := 0 to MAX_SLICES - 1 do for i := 0 to MAX_SLICES - 1 do
@@ -1258,7 +1292,8 @@ begin
Id := FNextSliceId; Id := FNextSliceId;
if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi, if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi,
TargetHz - FCenterFreq, Vol, AGC) then Exit; TargetHz - PanCenterHz(APanId), Vol, AGC,
APanId) then Exit;
Inc(FNextSliceId); Inc(FNextSliceId);
// Свой аудио-выход слайса на выбранной звуковухе. // Свой аудио-выход слайса на выбранной звуковухе.
@@ -1272,6 +1307,7 @@ begin
// Буква флага. Слайс A — это главный приёмник (не в FSlices[]), поэтому доп. // Буква флага. Слайс A — это главный приёмник (не в FSlices[]), поэтому доп.
// слайсы нумеруются с 'B' (slot 0 → B, 1 → C, …), иначе получалось два «A». // слайсы нумеруются с 'B' (slot 0 → B, 1 → C, …), иначе получалось два «A».
FSlices[slot].Letter := Chr(Ord('B') + slot); FSlices[slot].Letter := Chr(Ord('B') + slot);
FSlices[slot].PanId := APanId;
FSlices[slot].TargetHz := TargetHz; FSlices[slot].TargetHz := TargetHz;
FSlices[slot].Mode := Mode; FSlices[slot].Mode := Mode;
FSlices[slot].FilterLo := FilterLo; FSlices[slot].FilterLo := FilterLo;
@@ -1311,7 +1347,7 @@ begin
if idx < 0 then Exit; if idx < 0 then Exit;
FSlices[idx].TargetHz := TargetHz; FSlices[idx].TargetHz := TargetHz;
if Assigned(FDSPEngine) then if Assigned(FDSPEngine) then
FDSPEngine.SetSliceShift(Id, TargetHz - FCenterFreq); FDSPEngine.SetSliceShift(Id, TargetHz - PanCenterHz(FSlices[idx].PanId));
end; end;
procedure TRadioController.SetSliceMode(Id, Mode: Integer); procedure TRadioController.SetSliceMode(Id, Mode: Integer);
+480 -56
View File
@@ -132,12 +132,23 @@ const
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
// --- Панадаптеры на аппаратных DDC (этап 3) ---
// Пан 0 = главный тракт (RXA/RX_DISP_ID), доп. паны 1..MAX_PANS-1 живут на
// своих DDC. Единый потолок для движка/контроллера/UI.
MAX_PANS = 4;
// Analyzer id пана N = PAN_DISP_BASE + N (9..11). ID 4/5 заняты веткой
// feature/qo100-zoom-panel — не пересекаемся при будущем merge.
PAN_DISP_BASE = 8;
DSP_BUFSIZE = 1024; DSP_BUFSIZE = 1024;
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
DISPLAY_BLOCK_SIZE = 64; DISPLAY_BLOCK_SIZE = 64;
// Очередь IQ пакетов между сетевым и DSP потоком // Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны).
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски) // 256: главный 1.536М (~6.5к пкт/с) + 3 пана по 768к (~10к пкт/с) должны
// переживать scheduling-паузы ~15мс без дропов.
IQ_QUEUE_SIZE = 256; // кол-во слотов (степень двойки для AND-маски)
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6) IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
// TX mic ring buffer — должен быть степенью двойки // TX mic ring buffer — должен быть степенью двойки
@@ -155,6 +166,7 @@ type
Data: array[0..IQ_PKT_MAXBYTES - 1] of Byte; Data: array[0..IQ_PKT_MAXBYTES - 1] of Byte;
DataLen: Integer; // реальная длина данных DataLen: Integer; // реальная длина данных
IQPairs: Integer; // количество IQ пар IQPairs: Integer; // количество IQ пар
PanId: Integer; // 0 = главный тракт, 1..MAX_PANS-1 = доп. пан
end; end;
TWDSPAGCMode = (agcOff = 0, agcLong = 1, agcSlow = 2, TWDSPAGCMode = (agcOff = 0, agcLong = 1, agcSlow = 2,
@@ -173,6 +185,10 @@ type
// SliceId — логический id (из RadioController), не WDSP-канал. // SliceId — логический id (из RadioController), не WDSP-канал.
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single; TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
Count: Integer) of object; Count: Integer) of object;
// Пиксели спектра/водопада доп. пана — из display-потока (как FOnSpectrum/
// FOnWaterfall главного). Внутри колбэка НЕ звать slice/pan API движка.
TOnPanPixels = procedure(PanId: Integer; const Pixels: array of Single;
Count: Integer) of object;
// Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком, // Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком,
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio). // чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов. // Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
@@ -188,6 +204,7 @@ type
Active: Boolean; // слот занят Active: Boolean; // слот занят
Id: Integer; // логический id слайса Id: Integer; // логический id слайса
Chan: Integer; // WDSP-канал (SLICE_CHAN_BASE + слот) Chan: Integer; // WDSP-канал (SLICE_CHAN_BASE + слот)
PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан
ShiftHz: Double; // NCO-сдвиг = TargetHz - CapturedCenterHz ShiftHz: Double; // NCO-сдвиг = TargetHz - CapturedCenterHz
Mode: Integer; // индекс режима (MODE_*) Mode: Integer; // индекс режима (MODE_*)
FilterLo: Integer; FilterLo: Integer;
@@ -205,6 +222,21 @@ type
Opened: Boolean; // WDSP-канал реально открыт (после Open движка) Opened: Boolean; // WDSP-канал реально открыт (после Open движка)
end; end;
// Доп. панадаптер (этап 3): свой DDC-поток (rate 192-768к), свой analyzer
// (PAN_DISP_BASE+PanId), свои слайсы (TSliceChan.PanId). Доступ к записи —
// только под FSliceLock (кроме read-only Active в PushPanIQ).
TPanChan = record
Active: Boolean;
Rate: Integer; // входной rate пан-DDC, Гц (кратен FAudioRate)
BufSize: Integer; // блок @ Rate = FAudioBufSize * Rate / FAudioRate
AccI: array of Double; // аккумулятор входных IQ до BufSize
AccQ: array of Double;
AccPos: Integer;
DispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар)
DispPos: Integer;
DispOpen: Boolean; // XCreateAnalyzer(PAN_DISP_BASE+PanId) жив
end;
{ TWDSPEngine } { TWDSPEngine }
TWDSPEngine = class TWDSPEngine = class
private private
@@ -294,6 +326,11 @@ type
FSliceOutR: array of Single; // R FSliceOutR: array of Single; // R
FOnSliceAudio: TOnSliceAudio; FOnSliceAudio: TOnSliceAudio;
// --- Панадаптеры на аппаратных DDC (этап 3.2) ---
FPans: array[1..MAX_PANS-1] of TPanChan;
FOnPanSpectrum: TOnPanPixels;
FOnPanWaterfall: TOnPanPixels;
// 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;
@@ -395,7 +432,10 @@ type
// WFM требует dsp_rate >= 192k (девиация 75 кГц не влезает в 48 кГц). // WFM требует dsp_rate >= 192k (девиация 75 кГц не влезает в 48 кГц).
function WantWideDSP(Mode: Integer): Boolean; function WantWideDSP(Mode: Integer): Boolean;
function WFMDSPRate: Integer; function WFMDSPRate: Integer;
function WFMDSPRateFor(InRate: Integer): Integer; // WFMDSPRate для произвольного входа
function SliceDSPRate(Mode: Integer): Integer; function SliceDSPRate(Mode: Integer): Integer;
// dsp_rate слайса от режима и ЕГО входного rate (у пан-слайсов вход = rate пана)
function SliceDSPRateFor(Mode, InRate: Integer): Integer;
function ClampToNyquist(Hz, Rate: Integer): Integer; function ClampToNyquist(Hz, Rate: Integer): Integer;
function EffectiveTXNC: Integer; function EffectiveTXNC: Integer;
procedure ApplyDSPRate(Mode: Integer); procedure ApplyDSPRate(Mode: Integer);
@@ -403,11 +443,22 @@ type
procedure ApplyWFMSettings; procedure ApplyWFMSettings;
// --- Мультислайсы --- // --- Мультислайсы ---
function FindSliceIdx(Id: Integer): Integer; // -1 если нет function FindSliceIdx(Id: Integer): Integer; // -1 если нет
function SliceInRate(Idx: Integer): Integer; // входной rate слайса (его пан)
function SliceInSize(Idx: Integer): Integer; // входной блок слайса (его пан)
procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки
procedure CloseSliceChannel(Idx: Integer); // CloseChannel (запись хранит) procedure CloseSliceChannel(Idx: Integer); // CloseChannel (запись хранит)
procedure ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; FixedGain: Double); procedure ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; FixedGain: Double);
procedure ProcessSlices; // fan-out в ProcessRXBlock procedure ProcessSlices; // fan-out в ProcessRXBlock
// Прогон блока IQ пана через его слайсы. Вызывать ПОД FSliceLock.
procedure ProcessSlicesFor(PanId: Integer;
const AccI, AccQ: array of Double; N: Integer);
procedure AllocSliceBuffers; // (пере)аллокация буферов procedure AllocSliceBuffers; // (пере)аллокация буферов
// --- Панадаптеры (этап 3.2) ---
procedure CreatePanAnalyzer(PanId: Integer); // XCreateAnalyzer + настройки
procedure DestroyPanAnalyzer(PanId: Integer);
procedure ApplyPanAnalyzerSettings(PanId: Integer);
procedure ReapplyPanDisplaySettings; // все активные паны, под локом
procedure PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem);
procedure ProcessRXBlock; procedure ProcessRXBlock;
procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer; procedure OpenAnalyzer;
@@ -451,11 +502,22 @@ type
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет). // QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
procedure SetBeaconDecoder(D: TBeaconDecoder); procedure SetBeaconDecoder(D: TBeaconDecoder);
// --- Панадаптеры на аппаратных DDC (этап 3.2) ---
// Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer
// PAN_DISP_BASE+PanId. RateHz — rate его DDC, кратен FAudioRate.
// Слайсы пана создаются отдельно (AddSlice с APanId).
function AddPan(PanId, RateHz: Integer): Boolean;
// Убирает пан вместе с его слайсами (аудио-выходы слайсов — забота вызывающего).
procedure RemovePan(PanId: Integer);
function PanActive(PanId: Integer): Boolean;
// --- Мультислайсы (софт-fan-out) --- // --- Мультислайсы (софт-fan-out) ---
// Добавляет слайс с логическим Id (уникальным). ShiftHz = TargetHz - центр // Добавляет слайс с логическим Id (уникальным). ShiftHz = TargetHz - центр
// захвата. Возвращает False если нет свободных слотов или Id уже есть. // захвата ЕГО пана. APanId>0 требует активного пана (AddPan).
// Возвращает False если нет свободных слотов или Id уже есть.
function AddSlice(Id, Mode, FilterLo, FilterHi: Integer; function AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean; ShiftHz, Volume: Double; AGC: TWDSPAGCMode;
APanId: Integer = 0): Boolean;
procedure RemoveSlice(Id: Integer); procedure RemoveSlice(Id: Integer);
procedure SetSliceShift(Id: Integer; ShiftHz: Double); procedure SetSliceShift(Id: Integer; ShiftHz: Double);
procedure SetSliceMode(Id, Mode: Integer); procedure SetSliceMode(Id, Mode: Integer);
@@ -606,6 +668,9 @@ type
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio; 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;
// Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3).
property OnPanSpectrum: TOnPanPixels read FOnPanSpectrum write FOnPanSpectrum;
property OnPanWaterfall: TOnPanPixels read FOnPanWaterfall write FOnPanWaterfall;
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ; property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
property TXActive: Boolean read FTXActive; property TXActive: Boolean read FTXActive;
// Включает full-duplex поведение RX-тракта: RXA не останавливается на TX, // Включает full-duplex поведение RX-тракта: RXA не останавливается на TX,
@@ -804,11 +869,14 @@ begin
RTLEventWaitFor(FEngine.FQueueSem, 100); RTLEventWaitFor(FEngine.FQueueSem, 100);
if Terminated then Break; if Terminated then Break;
// Разбираем все накопившиеся пакеты // Разбираем все накопившиеся пакеты (общая очередь: главный + паны)
while FEngine.FQueueTail <> FEngine.FQueueHead do while FEngine.FQueueTail <> FEngine.FQueueHead do
begin begin
Item := @FEngine.FQueue[FEngine.FQueueTail]; Item := @FEngine.FQueue[FEngine.FQueueTail];
FEngine.PushIQItemToDSP(Item^); if Item^.PanId = 0 then
FEngine.PushIQItemToDSP(Item^)
else
FEngine.PushPanItemToDSP(Item^.PanId, Item^);
FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1); FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1);
end; end;
end; end;
@@ -850,9 +918,9 @@ begin
Result := (Mode = MODE_WFM) and (WFMDSPRate > FAudioRate); Result := (Mode = MODE_WFM) and (WFMDSPRate > FAudioRate);
end; end;
function TWDSPEngine.WFMDSPRate: Integer; function TWDSPEngine.WFMDSPRateFor(InRate: Integer): Integer;
// Наибольший rate вида FAudioRate * 2^k, который не выше WFM_DSP_RATE и делит // Наибольший rate вида FAudioRate * 2^k, который не выше WFM_DSP_RATE и делит
// FSampleRate нацело. Оба требования жёсткие: // InRate нацело. Оба требования жёсткие:
// * pre_main_build считает dsp_insize через ЦЕЛОЧИСЛЕННОЕ in_rate/dsp_rate, // * pre_main_build считает dsp_insize через ЦЕЛОЧИСЛЕННОЕ in_rate/dsp_rate,
// поэтому отношение обязано быть точным; // поэтому отношение обязано быть точным;
// * dsp_size = FAudioBufSize * (rate/FAudioRate) должен остаться степенью // * dsp_size = FAudioBufSize * (rate/FAudioRate) должен остаться степенью
@@ -863,17 +931,30 @@ begin
R := WFM_DSP_RATE div FAudioRate; // 4 при 48 кГц аудио R := WFM_DSP_RATE div FAudioRate; // 4 при 48 кГц аудио
while R > 1 do while R > 1 do
begin begin
if (FAudioRate * R <= FSampleRate) and (FSampleRate mod (FAudioRate * R) = 0) then if (FAudioRate * R <= InRate) and (InRate mod (FAudioRate * R) = 0) then
Break; Break;
R := R div 2; R := R div 2;
end; end;
Result := FAudioRate * R; Result := FAudioRate * R;
end; end;
function TWDSPEngine.WFMDSPRate: Integer;
begin
Result := WFMDSPRateFor(FSampleRate);
end;
function TWDSPEngine.SliceDSPRate(Mode: Integer): Integer; function TWDSPEngine.SliceDSPRate(Mode: Integer): Integer;
begin begin
if WantWideDSP(Mode) then Result := WFMDSPRate Result := SliceDSPRateFor(Mode, FSampleRate);
else Result := FAudioRate; end;
function TWDSPEngine.SliceDSPRateFor(Mode, InRate: Integer): Integer;
// Пан-слайс поднимается в широкий WFM-режим от rate СВОЕГО пана, а не главного.
begin
if (Mode = MODE_WFM) and (WFMDSPRateFor(InRate) > FAudioRate) then
Result := WFMDSPRateFor(InRate)
else
Result := FAudioRate;
end; end;
procedure TWDSPEngine.ApplyDSPRate(Mode: Integer); procedure TWDSPEngine.ApplyDSPRate(Mode: Integer);
@@ -1137,7 +1218,7 @@ end;
procedure TWDSPEngine.OpenAnalyzer; procedure TWDSPEngine.OpenAnalyzer;
var var
Success: Integer; Success, p: Integer;
begin begin
if FAnalyzerOpen then Exit; if FAnalyzerOpen then Exit;
@@ -1160,6 +1241,17 @@ begin
ApplyAnalyzerSettings; ApplyAnalyzerSettings;
FAnalyzerOpen := True; FAnalyzerOpen := True;
// Анализаторы активных доп. панов (пересоздание после ChangeSampleRate:
// Close их разрушил, записи FPans живы).
FSliceLock.Enter;
try
for p := 1 to MAX_PANS - 1 do
if FPans[p].Active then CreatePanAnalyzer(p);
finally
FSliceLock.Leave;
end;
if FDisplayThread = nil then if FDisplayThread = nil then
FDisplayThread := TDisplayThread.Create(Self); FDisplayThread := TDisplayThread.Create(Self);
end; end;
@@ -1257,11 +1349,91 @@ begin
SetDisplaySampleRate (TX_DISP_ID, FTXSampleRate); SetDisplaySampleRate (TX_DISP_ID, FTXSampleRate);
end; end;
procedure TWDSPEngine.ApplyPanAnalyzerSettings(PanId: Integer);
// Параметризованная копия ApplyRXAnalyzerSettings для analyzer'а доп. пана:
// rate пана, БЕЗ span-clip/zoom (пан-зум — этап 3.3+), FFT/детектор/усреднение
// пока общие с главным. Вызывать под FSliceLock.
var
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels, Disp: Integer;
Backmult, WfBackmult: Double;
EffFPS: Double;
const
KEEP_TIME_SEC = 0.10;
begin
if not FPans[PanId].DispOpen then Exit;
Disp := PAN_DISP_BASE + PanId;
AnalyzerFFT := CalcDisplayFFTSize;
FPans[PanId].DispPos := 0;
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FPans[PanId].Rate,
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffFPS);
DisplayPixels := EnsureRange(FLastSpectrumW, 64,
EnsureRange(FDisplayResLimit, 64, SPECTRUM_PIXELS));
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
SetAnalyzer(Disp, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
FWindowType, 14.0, Overlap, 0, 0, 0, DisplayPixels,
1, 0, 0.0, 0.0, MaxW);
SetDisplayDetectorMode(Disp, 0, FSpecDetector);
SetDisplayAverageMode (Disp, 0, FSpecAvgMode);
SetDisplayNumAverage (Disp, 0, AvgCount);
SetDisplayAvBackmult (Disp, 0, Backmult);
SetDisplayNormOneHz (Disp, 0, UsePanNormOneHz(FSpecDetector));
SetDisplayDetectorMode(Disp, 1, FWfDetector);
SetDisplayAverageMode (Disp, 1, FWfAvgMode);
SetDisplayNumAverage (Disp, 1, WfAvgCount);
SetDisplayAvBackmult (Disp, 1, WfBackmult);
SetDisplayNormOneHz (Disp, 1, 0);
SetDisplaySampleRate (Disp, FPans[PanId].Rate);
end;
procedure TWDSPEngine.CreatePanAnalyzer(PanId: Integer);
// Вызывать под FSliceLock, при живом WDSP (OpenAnalyzer уже прошёл).
var
Success: Integer;
begin
if FPans[PanId].DispOpen then Exit;
Success := -1;
XCreateAnalyzer(PAN_DISP_BASE + PanId, @Success, 262144, 1, 1, nil);
if Success <> 0 then
begin
FLastError := Format('XCreateAnalyzer(PAN%d) failed: %d', [PanId, Success]);
Exit; // пан без спектра: слайсы работают, DispOpen=False гейтит Spectrum0
end;
FPans[PanId].DispOpen := True;
ApplyPanAnalyzerSettings(PanId);
end;
procedure TWDSPEngine.DestroyPanAnalyzer(PanId: Integer);
// Вызывать под FSliceLock (display-поток читает DispOpen/зовёт GetPixels под ним).
begin
if not FPans[PanId].DispOpen then Exit;
FPans[PanId].DispOpen := False;
DestroyAnalyzer(PAN_DISP_BASE + PanId);
end;
procedure TWDSPEngine.ReapplyPanDisplaySettings;
// Перекатывает общие настройки дисплея (FFT/детектор/усреднение/пиксели) на
// анализаторы всех активных панов.
var p: Integer;
begin
FSliceLock.Enter;
try
for p := 1 to MAX_PANS - 1 do
if FPans[p].Active then ApplyPanAnalyzerSettings(p);
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.ApplyAnalyzerSettings; procedure TWDSPEngine.ApplyAnalyzerSettings;
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у. // Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам.
begin begin
ApplyRXAnalyzerSettings; ApplyRXAnalyzerSettings;
ApplyTXAnalyzerSettings; ApplyTXAnalyzerSettings;
ReapplyPanDisplaySettings;
end; end;
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double); procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
@@ -1275,6 +1447,7 @@ begin
end; end;
procedure TWDSPEngine.CloseAnalyzer; procedure TWDSPEngine.CloseAnalyzer;
var p: Integer;
begin begin
if not FAnalyzerOpen then Exit; if not FAnalyzerOpen then Exit;
if Assigned(FDisplayThread) then if Assigned(FDisplayThread) then
@@ -1285,6 +1458,14 @@ begin
end; end;
DestroyAnalyzer(RX_DISP_ID); DestroyAnalyzer(RX_DISP_ID);
DestroyAnalyzer(TX_DISP_ID); DestroyAnalyzer(TX_DISP_ID);
// Анализаторы доп. панов (записи FPans сохраняются — пересоздание в OpenAnalyzer).
FSliceLock.Enter;
try
for p := 1 to MAX_PANS - 1 do
DestroyPanAnalyzer(p);
finally
FSliceLock.Leave;
end;
FAnalyzerOpen := False; FAnalyzerOpen := False;
end; end;
@@ -1465,6 +1646,10 @@ end;
procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer); procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open. // Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open.
// Панов не трогает: их rate свой; аккумуляторы сбрасываем (очередь общая,
// их пакеты умерли вместе с ней), анализаторы пересоздаст OpenAnalyzer.
var
p: Integer;
begin begin
if NewRate = FSampleRate then Exit; if NewRate = FSampleRate then Exit;
@@ -1476,6 +1661,12 @@ begin
FQueueHead := 0; FQueueHead := 0;
FQueueTail := 0; FQueueTail := 0;
FRXAccPos := 0; FRXAccPos := 0;
for p := 1 to MAX_PANS - 1 do
if FPans[p].Active then
begin
FPans[p].AccPos := 0;
FPans[p].DispPos := 0;
end;
// Обновляем параметры // Обновляем параметры
FSampleRate := NewRate; FSampleRate := NewRate;
@@ -1523,6 +1714,7 @@ begin
Move(Buf[DataOffset], Item^.Data[0], DataBytes); Move(Buf[DataOffset], Item^.Data[0], DataBytes);
Item^.DataLen := DataBytes; Item^.DataLen := DataBytes;
Item^.IQPairs := IQPairs; Item^.IQPairs := IQPairs;
Item^.PanId := 0;
FQueueHead := NextHead; FQueueHead := NextHead;
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR) RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
@@ -1530,9 +1722,102 @@ end;
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte; procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
DataOffset: Integer; IQPairs: Integer); DataOffset: Integer; IQPairs: Integer);
// Сетевой поток. Этап 3.2 положит пакет в аккумулятор пана PanId; до тех пор // Сетевой поток: пакет пан-DDC в ОБЩУЮ очередь с меткой пана (порядок прихода
// поток доп. пан-DDC отбрасывается (панов ещё никто не создаёт). // сохраняется, один семафор). Active читаем без лока: Boolean, худший случай —
// один лишний пакет в очереди, который DSP-поток отсеет уже под локом.
var
NextHead: Integer;
Item: ^TIQQueueItem;
DataBytes: Integer;
begin begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
if not FPans[PanId].Active then Exit;
NextHead := (FQueueHead + 1) and (IQ_QUEUE_SIZE - 1);
if NextHead = FQueueTail then Exit; // очередь полна — пропускаем пакет
Item := @FQueue[FQueueHead];
DataBytes := IQPairs * 6;
if DataBytes > IQ_PKT_MAXBYTES then DataBytes := IQ_PKT_MAXBYTES;
Move(Buf[DataOffset], Item^.Data[0], DataBytes);
Item^.DataLen := DataBytes;
Item^.IQPairs := IQPairs;
Item^.PanId := PanId;
FQueueHead := NextHead;
RTLEventSetEvent(FQueueSem);
end;
procedure TWDSPEngine.PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem);
// DSP-поток: декод 24-бит IQ пан-DDC → feed анализатора пана + аккумулятор;
// по накоплению блока — слайсы этого пана. Всё под FSliceLock: Add/RemovePan
// и слайс-API меняют состояние из UI-потока. TX-гейты зеркалят главный тракт.
var
i, Pos: Integer;
IR, QR: LongInt;
DI, DQ: Double;
P: PByte;
Pan: ^TPanChan;
const
SCALE = 1.0 / 8388608.0;
begin
if FTXActive and not FKeepRXDuringTX then Exit;
if (FPostTXMuteUntil <> 0) and (GetTickCount64 < FPostTXMuteUntil) then Exit;
if not FInitialized then Exit;
FSliceLock.Enter;
try
Pan := @FPans[PanId];
if not Pan^.Active then Exit;
Pos := 0;
P := @Item.Data[0];
for i := 0 to Item.IQPairs - 1 do
begin
if Pos + 5 >= Item.DataLen then Break;
IR := (LongInt(P[0]) shl 16) or (LongInt(P[1]) shl 8) or LongInt(P[2]);
QR := (LongInt(P[3]) shl 16) or (LongInt(P[4]) shl 8) or LongInt(P[5]);
Inc(P, 6);
Inc(Pos, 6);
if (IR and $800000) <> 0 then IR := IR or LongInt($FF000000);
if (QR and $800000) <> 0 then QR := QR or LongInt($FF000000);
DI := IR * SCALE;
DQ := QR * SCALE;
if Pan^.DispOpen then
begin
Pan^.DispBuf[Pan^.DispPos * 2] := DI;
Pan^.DispBuf[Pan^.DispPos * 2 + 1] := DQ;
Inc(Pan^.DispPos);
if Pan^.DispPos >= DISPLAY_BLOCK_SIZE then
begin
Spectrum0(1, PAN_DISP_BASE + PanId, 0, 0, @Pan^.DispBuf[0]);
Pan^.DispPos := 0;
end;
end;
Pan^.AccI[Pan^.AccPos] := DI;
Pan^.AccQ[Pan^.AccPos] := DQ;
Inc(Pan^.AccPos);
if Pan^.AccPos >= Pan^.BufSize then
begin
Pan^.AccPos := 0;
if Assigned(FOnSliceAudio) then
try
ProcessSlicesFor(PanId, Pan^.AccI, Pan^.AccQ, Pan^.BufSize);
except
// Защита от падения libwdsp в потоке — пропускаем блок (как ProcessRXBlock)
end;
end;
end;
finally
FSliceLock.Leave;
end;
end; end;
procedure TWDSPEngine.FlushRX; procedure TWDSPEngine.FlushRX;
@@ -1699,9 +1984,16 @@ end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TWDSPEngine.AllocSliceBuffers; procedure TWDSPEngine.AllocSliceBuffers;
// FSliceIn/Out должны вмещать наибольший входной блок среди главного тракта
// и активных панов (пан на 768к при главном 192к имеет блок больше FBufSize).
var
Need, p: Integer;
begin begin
SetLength(FSliceIn, FBufSize * 2); Need := FBufSize;
SetLength(FSliceOut, FBufSize * 2); // с запасом (>= FAudioBufSize*2) for p := 1 to MAX_PANS - 1 do
if FPans[p].Active and (FPans[p].BufSize > Need) then Need := FPans[p].BufSize;
SetLength(FSliceIn, Need * 2);
SetLength(FSliceOut, Need * 2); // с запасом (>= FAudioBufSize*2)
SetLength(FSliceOutL, FAudioBufSize); SetLength(FSliceOutL, FAudioBufSize);
SetLength(FSliceOutR, FAudioBufSize); SetLength(FSliceOutR, FAudioBufSize);
end; end;
@@ -1714,6 +2006,29 @@ begin
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i); if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
end; end;
function TWDSPEngine.SliceInRate(Idx: Integer): Integer;
// Входной rate слайса = rate его пана (пан 0 = главный захват).
var p: Integer;
begin
p := FSlices[Idx].PanId;
if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then
Result := FPans[p].Rate
else
Result := FSampleRate;
end;
function TWDSPEngine.SliceInSize(Idx: Integer): Integer;
// Входной блок слайса = блок его пана. Выходной блок при этом всегда
// FAudioBufSize (in_size/in_rate = const по построению BufSize панов).
var p: Integer;
begin
p := FSlices[Idx].PanId;
if (p >= 1) and (p < MAX_PANS) and FPans[p].Active then
Result := FPans[p].BufSize
else
Result := FBufSize;
end;
// Применяет AGC к произвольному каналу — вынесено из SetAGC чтобы делить с слайсами. // Применяет AGC к произвольному каналу — вынесено из SetAGC чтобы делить с слайсами.
procedure TWDSPEngine.ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; procedure TWDSPEngine.ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode;
FixedGain: Double); FixedGain: Double);
@@ -1746,20 +2061,23 @@ end;
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать // Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
// под FSliceLock и только при FInitialized. // под FSliceLock и только при FInitialized.
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer); procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
var Chan, DSPRate, DSPSize: Integer; var Chan, DSPRate, DSPSize, InRate, InSize: Integer;
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
if FSlices[Idx].Opened then Exit; if FSlices[Idx].Opened then Exit;
Chan := FSlices[Idx].Chan; Chan := FSlices[Idx].Chan;
// Вход канала = rate/блок ПАНА слайса (пан 0 = главный захват).
InRate := SliceInRate(Idx);
InSize := SliceInSize(Idx);
// WFM-слайс, как и главный канал, живёт на WFMDSPRate: rsmpin децимирует // WFM-слайс, как и главный канал, живёт на WFMDSPRate: rsmpin децимирует
// вход, rsmpout сводит к FAudioRate, так что out_size остаётся FAudioBufSize. // вход, rsmpout сводит к FAudioRate, так что out_size остаётся FAudioBufSize.
DSPRate := SliceDSPRate(FSlices[Idx].Mode); DSPRate := SliceDSPRateFor(FSlices[Idx].Mode, InRate);
DSPSize := FAudioBufSize * (DSPRate div FAudioRate); DSPSize := FAudioBufSize * (DSPRate div FAudioRate);
OpenChannel(Chan, FBufSize, DSPSize, FSampleRate, DSPRate, FAudioRate, OpenChannel(Chan, InSize, DSPSize, InRate, DSPRate, FAudioRate,
0, 0, 0.010, 0.010, 0.010, 0.010, 0); 0, 0, 0.010, 0.010, 0.010, 0.010, 0);
// NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open. // NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open.
create_anbEXT(Chan, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20); create_anbEXT(Chan, 1, InSize, InRate, 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); create_nobEXT(Chan, 1, 0, InSize, InRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
SetRXABandpassWindow(Chan, 1); SetRXABandpassWindow(Chan, 1);
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan); if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
@@ -1791,51 +2109,123 @@ begin
FSlices[Idx].Opened := False; FSlices[Idx].Opened := False;
end; end;
// Fan-out: прогоняет тот же входной IQ-блок через каждый активный слайс-канал. // Fan-out слайсов главного пана: вызывается из ProcessRXBlock (DSP-поток)
// Вызывается из ProcessRXBlock (DSP-поток) после основного канала. // после основного канала.
procedure TWDSPEngine.ProcessSlices; procedure TWDSPEngine.ProcessSlices;
var
s, i, k, Err: Integer;
V: Double;
begin begin
if not Assigned(FOnSliceAudio) then Exit; if not Assigned(FOnSliceAudio) then Exit;
FSliceLock.Enter; FSliceLock.Enter;
try try
for s := 0 to MAX_SLICES - 1 do ProcessSlicesFor(0, FRXAccI, FRXAccQ, FBufSize);
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 finally
FSliceLock.Leave; FSliceLock.Leave;
end; end;
end; end;
// Прогоняет входной IQ-блок пана PanId через каждый его активный слайс-канал.
// Вызывать ПОД FSliceLock (общая дисциплина слайсов/панов vs DSP).
procedure TWDSPEngine.ProcessSlicesFor(PanId: Integer;
const AccI, AccQ: array of Double; N: Integer);
var
s, i, k, Err: Integer;
V: Double;
begin
for s := 0 to MAX_SLICES - 1 do
begin
if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
if FSlices[s].PanId <> PanId then Continue;
if FSlices[s].Muted then Continue;
// Свежая копия входного IQ — fexchange0 портит вход in-place.
for i := 0 to N - 1 do
begin
FSliceIn[i * 2] := AccI[i];
FSliceIn[i * 2 + 1] := AccQ[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;
end;
// ---------------------------------------------------------------------------
// Панадаптеры на аппаратных DDC (этап 3.2)
// ---------------------------------------------------------------------------
function TWDSPEngine.AddPan(PanId, RateHz: Integer): Boolean;
begin
Result := False;
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
// Кратность rate: и BufSize (блок = FAudioBufSize*Rate/FAudioRate), и WDSP
// pre_main_build (целочисленное in_rate/dsp_rate) требуют точного отношения.
if (RateHz < FAudioRate) or (RateHz mod FAudioRate <> 0) then Exit;
FSliceLock.Enter;
try
if FPans[PanId].Active then Exit;
FPans[PanId].Rate := RateHz;
FPans[PanId].BufSize := FAudioBufSize * (RateHz div FAudioRate);
SetLength(FPans[PanId].AccI, FPans[PanId].BufSize);
SetLength(FPans[PanId].AccQ, FPans[PanId].BufSize);
SetLength(FPans[PanId].DispBuf, DISPLAY_BLOCK_SIZE * 2);
FPans[PanId].AccPos := 0;
FPans[PanId].DispPos := 0;
FPans[PanId].DispOpen := False;
FPans[PanId].Active := True;
AllocSliceBuffers; // FSliceIn/Out под новый макс. блок
if FAnalyzerOpen then CreatePanAnalyzer(PanId);
Result := True;
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.RemovePan(PanId: Integer);
var s: Integer;
begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
FSliceLock.Enter;
try
if not FPans[PanId].Active then Exit;
// Слайсы пана: обычно контроллер уже убрал их (вместе со звуковухами);
// здесь — страховка, чтобы канал не остался кормиться из мёртвого пана.
for s := 0 to MAX_SLICES - 1 do
if FSlices[s].Active and (FSlices[s].PanId = PanId) then
begin
CloseSliceChannel(s);
FSlices[s].Active := False;
end;
DestroyPanAnalyzer(PanId);
FPans[PanId].Active := False;
FPans[PanId].AccI := nil;
FPans[PanId].AccQ := nil;
FPans[PanId].DispBuf := nil;
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.PanActive(PanId: Integer): Boolean;
begin
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;
end;
function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer; function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean; ShiftHz, Volume: Double; AGC: TWDSPAGCMode; APanId: Integer): Boolean;
var i, slot: Integer; var i, slot: Integer;
begin begin
Result := False; Result := False;
FSliceLock.Enter; FSliceLock.Enter;
try try
if FindSliceIdx(Id) >= 0 then Exit; // уже существует if FindSliceIdx(Id) >= 0 then Exit; // уже существует
if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
if (APanId > 0) and not FPans[APanId].Active then Exit; // пан не создан
slot := -1; slot := -1;
for i := 0 to MAX_SLICES - 1 do for i := 0 to MAX_SLICES - 1 do
if not FSlices[i].Active then begin slot := i; Break; end; if not FSlices[i].Active then begin slot := i; Break; end;
@@ -1843,6 +2233,7 @@ begin
FSlices[slot].Active := True; FSlices[slot].Active := True;
FSlices[slot].Id := Id; FSlices[slot].Id := Id;
FSlices[slot].Chan := SLICE_CHAN_BASE + slot; FSlices[slot].Chan := SLICE_CHAN_BASE + slot;
FSlices[slot].PanId := APanId;
FSlices[slot].Mode := Mode; FSlices[slot].Mode := Mode;
FSlices[slot].FilterLo := FilterLo; FSlices[slot].FilterLo := FilterLo;
FSlices[slot].FilterHi := FilterHi; FSlices[slot].FilterHi := FilterHi;
@@ -1899,7 +2290,8 @@ begin
// Уход в/из WFM меняет dsp_rate канала — проще пересоздать его целиком, // Уход в/из WFM меняет dsp_rate канала — проще пересоздать его целиком,
// чем гонять SetDSPSamplerate: слайс всё равно перестраивает все фильтры. // чем гонять SetDSPSamplerate: слайс всё равно перестраивает все фильтры.
Rerate := FSlices[idx].Opened and Rerate := FSlices[idx].Opened and
(SliceDSPRate(Mode) <> SliceDSPRate(FSlices[idx].Mode)); (SliceDSPRateFor(Mode, SliceInRate(idx)) <>
SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx)));
FSlices[idx].Mode := Mode; FSlices[idx].Mode := Mode;
if Rerate then if Rerate then
begin begin
@@ -1926,7 +2318,7 @@ begin
FSlices[idx].FilterHi := High; FSlices[idx].FilterHi := High;
if FSlices[idx].Opened then if FSlices[idx].Opened then
begin begin
Rate := SliceDSPRate(FSlices[idx].Mode); Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx));
RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate), RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate),
ClampToNyquist(High, Rate)); ClampToNyquist(High, Rate));
end; end;
@@ -2843,10 +3235,12 @@ procedure TWDSPEngine.UpdateSpectrum;
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save // при FFT=16384 WDSP использует уже накопленные данные из overlap-save
// буфера, а не ждёт следующего полного блока. // буфера, а не ждёт следующего полного блока.
var var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single; PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer; WfBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
i, N: Integer; Flag: Integer;
DispID: Integer; i, N, p: Integer;
DispID: Integer;
GotSpec, GotWf: Boolean;
begin begin
if not FInitialized or not FAnalyzerOpen then Exit; if not FInitialized or not FAnalyzerOpen then Exit;
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
@@ -2871,6 +3265,34 @@ begin
if Assigned(FOnWaterfall) then if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, N); FOnWaterfall(FWaterfallPixels, N);
end; end;
// Доп. паны: GetPixels под FSliceLock (гонка с DestroyPanAnalyzer из
// RemovePan в UI-потоке), колбэки — уже без лока (UI в них может звать
// slice/pan API). Пиксели пана не кэшируем (аналог FSpectrumPixels не нужен).
if Assigned(FOnPanSpectrum) or Assigned(FOnPanWaterfall) then
for p := 1 to MAX_PANS - 1 do
begin
GotSpec := False;
GotWf := False;
FSliceLock.Enter;
try
if FPans[p].Active and FPans[p].DispOpen then
begin
Flag := 0;
GetPixels(PAN_DISP_BASE + p, 0, @PixBuf[0], @Flag);
GotSpec := Flag <> 0;
Flag := 0;
GetPixels(PAN_DISP_BASE + p, 1, @WfBuf[0], @Flag);
GotWf := Flag <> 0;
end;
finally
FSliceLock.Leave;
end;
if GotSpec and Assigned(FOnPanSpectrum) then
FOnPanSpectrum(p, PixBuf, N);
if GotWf and Assigned(FOnPanWaterfall) then
FOnPanWaterfall(p, WfBuf, N);
end;
end; end;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single; procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
@@ -3015,6 +3437,7 @@ begin
SetDisplayNumAverage(RX_DISP_ID, 0, AvgCount); SetDisplayNumAverage(RX_DISP_ID, 0, AvgCount);
SetDisplayAvBackmult(RX_DISP_ID, 0, Backmult); SetDisplayAvBackmult(RX_DISP_ID, 0, Backmult);
SetDisplayNormOneHz(RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector)); SetDisplayNormOneHz(RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
ReapplyPanDisplaySettings; // детектор/усреднение общие с панами
end; end;
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer; procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
@@ -3039,6 +3462,7 @@ begin
SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount); SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount);
SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult); SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult);
SetDisplayNormOneHz(RX_DISP_ID, 1, 0); SetDisplayNormOneHz(RX_DISP_ID, 1, 0);
ReapplyPanDisplaySettings; // детектор/усреднение общие с панами
end; end;
end. end.
+37
View File
@@ -283,6 +283,43 @@ static/глобального. (CPU-LUT палитры общий — это о
создание/удаление пана только под локом, как переоткрытие слайсов при создание/удаление пана только под локом, как переоткрытие слайсов при
ChangeSampleRate). ChangeSampleRate).
**СТАТУС 3.2 (2026-07-11): РЕАЛИЗОВАНО + СМОУК-ТЕСТ БЕЗ ЖЕЛЕЗА ПРОШЁЛ
ПОЛНОСТЬЮ.** Отличия/уточнения против текста плана:
- Пан-пакеты идут в ОБЩУЮ очередь IQ с меткой `TIQQueueItem.PanId` (0 =
главный) — порядок прихода сохранён, один семафор; `IQ_QUEUE_SIZE` 64→256.
Диспетчер в `TDSPThread``PushPanItemToDSP` (декод + feed анализатора +
аккумулятор + `ProcessSlicesFor(PanId)`), всё под `FSliceLock`.
- `MAX_PANS=4` переехал в WDSPEngine (единый для движка/контроллера/UI);
`PAN_DISP_BASE=8` (id панов 9..11).
- Блок пана `BufSize = FAudioBufSize·Rate/FAudioRate` ⇒ выходной аудио-блок
слайсов ЛЮБОГО пана всегда `FAudioBufSize` (буферы не перевыделяются);
`AllocSliceBuffers` берёт максимум блока по всем панам. Rate пана обязан
быть кратен 48к (гейт в `AddPan`).
- `TSliceChan.PanId` + `AddSlice(..., APanId)`; `OpenSliceChannel` открывает
канал от rate/блока СВОЕГО пана; `SliceDSPRateFor(Mode, InRate)` — WFM-слайс
поднимается от rate пана. Слайсы панов переживают `ChangeSampleRate`
главного (пересоздаются в Open, как раньше).
- Анализатор пана: `CreatePanAnalyzer`/`ApplyPanAnalyzerSettings` (без
span-clip/zoom — пан-зум в 3.3+), живёт в OpenAnalyzer/CloseAnalyzer
(пересоздание при смене rate главного). Смена детектора/усреднения/FFT/
пикселей главного перекатывается на паны (`ReapplyPanDisplaySettings`).
- Выдача пикселей: `OnPanSpectrum`/`OnPanWaterfall(PanId, Pixels, N)` из
display-потока; `GetPixels` пана под `FSliceLock` (гонка с RemovePan),
колбэки — вне лока.
- Контроллер: `TCtrlSlice.PanId`, `AddSlice(..., APanId)`, гейт/`CapturedSpan`
per-pan, `SetSliceTarget`/`RefreshSliceShifts` относительно центра СВОЕГО
пана, ретюн пана двигает NCO его слайсов; `AddPanDDC` создаёт пан в движке
(до включения DDC), `RemovePanDDC` убирает слайсы (со звуковухами) + движок
+ DDC.
- Смоук-тест (scratchpad `pantest.pas`, консольный, libwdsp): AddPan-валидации,
спектр/водопад пана 384к с тоном +52к — пик ровно на месте (пиксель 650/651),
слайс пана (USB, NCO +51к) выдал аудио-тон 1043 Гц RMS 0.68, главный тракт
параллельно жив (пик −30к на месте), RemovePan мгновенно глушит слайс, без
крэшей. Попутно зафиксирована конвенция синтетического IQ: «+Гц на дисплее»
= I=cos, Q=−sin (полезно для будущих тестов).
- НЕ проверено на эфире (нужен UI 3.3, панов никто не создаёт из GUI).
### 3.3 — UI: второй панадаптер в главном окне ### 3.3 — UI: второй панадаптер в главном окне
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с - **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с