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
+480 -56
View File
@@ -132,12 +132,23 @@ const
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
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;
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
DISPLAY_BLOCK_SIZE = 64;
// Очередь IQ пакетов между сетевым и DSP потоком
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
// Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны).
// 256: главный 1.536М (~6.5к пкт/с) + 3 пана по 768к (~10к пкт/с) должны
// переживать scheduling-паузы ~15мс без дропов.
IQ_QUEUE_SIZE = 256; // кол-во слотов (степень двойки для AND-маски)
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
// TX mic ring buffer — должен быть степенью двойки
@@ -155,6 +166,7 @@ type
Data: array[0..IQ_PKT_MAXBYTES - 1] of Byte;
DataLen: Integer; // реальная длина данных
IQPairs: Integer; // количество IQ пар
PanId: Integer; // 0 = главный тракт, 1..MAX_PANS-1 = доп. пан
end;
TWDSPAGCMode = (agcOff = 0, agcLong = 1, agcSlow = 2,
@@ -173,6 +185,10 @@ type
// SliceId — логический id (из RadioController), не WDSP-канал.
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
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 дёргает этот колбэк перед каждым тиком,
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
@@ -188,6 +204,7 @@ type
Active: Boolean; // слот занят
Id: Integer; // логический id слайса
Chan: Integer; // WDSP-канал (SLICE_CHAN_BASE + слот)
PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан
ShiftHz: Double; // NCO-сдвиг = TargetHz - CapturedCenterHz
Mode: Integer; // индекс режима (MODE_*)
FilterLo: Integer;
@@ -205,6 +222,21 @@ type
Opened: Boolean; // WDSP-канал реально открыт (после Open движка)
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 = class
private
@@ -294,6 +326,11 @@ type
FSliceOutR: array of Single; // R
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,
// один reader — TX DSP thread)
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
@@ -395,7 +432,10 @@ type
// WFM требует dsp_rate >= 192k (девиация 75 кГц не влезает в 48 кГц).
function WantWideDSP(Mode: Integer): Boolean;
function WFMDSPRate: Integer;
function WFMDSPRateFor(InRate: Integer): Integer; // WFMDSPRate для произвольного входа
function SliceDSPRate(Mode: Integer): Integer;
// dsp_rate слайса от режима и ЕГО входного rate (у пан-слайсов вход = rate пана)
function SliceDSPRateFor(Mode, InRate: Integer): Integer;
function ClampToNyquist(Hz, Rate: Integer): Integer;
function EffectiveTXNC: Integer;
procedure ApplyDSPRate(Mode: Integer);
@@ -403,11 +443,22 @@ type
procedure ApplyWFMSettings;
// --- Мультислайсы ---
function FindSliceIdx(Id: Integer): Integer; // -1 если нет
function SliceInRate(Idx: Integer): Integer; // входной rate слайса (его пан)
function SliceInSize(Idx: Integer): Integer; // входной блок слайса (его пан)
procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки
procedure CloseSliceChannel(Idx: Integer); // CloseChannel (запись хранит)
procedure ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode; FixedGain: Double);
procedure ProcessSlices; // fan-out в ProcessRXBlock
// Прогон блока IQ пана через его слайсы. Вызывать ПОД FSliceLock.
procedure ProcessSlicesFor(PanId: Integer;
const AccI, AccQ: array of Double; N: Integer);
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 PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer;
@@ -451,11 +502,22 @@ type
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
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) ---
// Добавляет слайс с логическим Id (уникальным). ShiftHz = TargetHz - центр
// захвата. Возвращает False если нет свободных слотов или Id уже есть.
// захвата ЕГО пана. APanId>0 требует активного пана (AddPan).
// Возвращает False если нет свободных слотов или Id уже есть.
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 SetSliceShift(Id: Integer; ShiftHz: Double);
procedure SetSliceMode(Id, Mode: Integer);
@@ -606,6 +668,9 @@ type
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
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 TXActive: Boolean read FTXActive;
// Включает full-duplex поведение RX-тракта: RXA не останавливается на TX,
@@ -804,11 +869,14 @@ begin
RTLEventWaitFor(FEngine.FQueueSem, 100);
if Terminated then Break;
// Разбираем все накопившиеся пакеты
// Разбираем все накопившиеся пакеты (общая очередь: главный + паны)
while FEngine.FQueueTail <> FEngine.FQueueHead do
begin
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);
end;
end;
@@ -850,9 +918,9 @@ begin
Result := (Mode = MODE_WFM) and (WFMDSPRate > FAudioRate);
end;
function TWDSPEngine.WFMDSPRate: Integer;
function TWDSPEngine.WFMDSPRateFor(InRate: Integer): Integer;
// Наибольший rate вида FAudioRate * 2^k, который не выше WFM_DSP_RATE и делит
// FSampleRate нацело. Оба требования жёсткие:
// InRate нацело. Оба требования жёсткие:
// * pre_main_build считает dsp_insize через ЦЕЛОЧИСЛЕННОЕ in_rate/dsp_rate,
// поэтому отношение обязано быть точным;
// * dsp_size = FAudioBufSize * (rate/FAudioRate) должен остаться степенью
@@ -863,17 +931,30 @@ begin
R := WFM_DSP_RATE div FAudioRate; // 4 при 48 кГц аудио
while R > 1 do
begin
if (FAudioRate * R <= FSampleRate) and (FSampleRate mod (FAudioRate * R) = 0) then
if (FAudioRate * R <= InRate) and (InRate mod (FAudioRate * R) = 0) then
Break;
R := R div 2;
end;
Result := FAudioRate * R;
end;
function TWDSPEngine.WFMDSPRate: Integer;
begin
Result := WFMDSPRateFor(FSampleRate);
end;
function TWDSPEngine.SliceDSPRate(Mode: Integer): Integer;
begin
if WantWideDSP(Mode) then Result := WFMDSPRate
else Result := FAudioRate;
Result := SliceDSPRateFor(Mode, FSampleRate);
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;
procedure TWDSPEngine.ApplyDSPRate(Mode: Integer);
@@ -1137,7 +1218,7 @@ end;
procedure TWDSPEngine.OpenAnalyzer;
var
Success: Integer;
Success, p: Integer;
begin
if FAnalyzerOpen then Exit;
@@ -1160,6 +1241,17 @@ begin
ApplyAnalyzerSettings;
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
FDisplayThread := TDisplayThread.Create(Self);
end;
@@ -1257,11 +1349,91 @@ begin
SetDisplaySampleRate (TX_DISP_ID, FTXSampleRate);
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;
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у.
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам.
begin
ApplyRXAnalyzerSettings;
ApplyTXAnalyzerSettings;
ReapplyPanDisplaySettings;
end;
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
@@ -1275,6 +1447,7 @@ begin
end;
procedure TWDSPEngine.CloseAnalyzer;
var p: Integer;
begin
if not FAnalyzerOpen then Exit;
if Assigned(FDisplayThread) then
@@ -1285,6 +1458,14 @@ begin
end;
DestroyAnalyzer(RX_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;
end;
@@ -1465,6 +1646,10 @@ end;
procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open.
// Панов не трогает: их rate свой; аккумуляторы сбрасываем (очередь общая,
// их пакеты умерли вместе с ней), анализаторы пересоздаст OpenAnalyzer.
var
p: Integer;
begin
if NewRate = FSampleRate then Exit;
@@ -1476,6 +1661,12 @@ begin
FQueueHead := 0;
FQueueTail := 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;
@@ -1523,6 +1714,7 @@ begin
Move(Buf[DataOffset], Item^.Data[0], DataBytes);
Item^.DataLen := DataBytes;
Item^.IQPairs := IQPairs;
Item^.PanId := 0;
FQueueHead := NextHead;
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
@@ -1530,9 +1722,102 @@ end;
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
DataOffset: Integer; IQPairs: Integer);
// Сетевой поток. Этап 3.2 положит пакет в аккумулятор пана PanId; до тех пор
// поток доп. пан-DDC отбрасывается (панов ещё никто не создаёт).
// Сетевой поток: пакет пан-DDC в ОБЩУЮ очередь с меткой пана (порядок прихода
// сохраняется, один семафор). Active читаем без лока: Boolean, худший случай —
// один лишний пакет в очереди, который DSP-поток отсеет уже под локом.
var
NextHead: Integer;
Item: ^TIQQueueItem;
DataBytes: Integer;
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;
procedure TWDSPEngine.FlushRX;
@@ -1699,9 +1984,16 @@ end;
// ---------------------------------------------------------------------------
procedure TWDSPEngine.AllocSliceBuffers;
// FSliceIn/Out должны вмещать наибольший входной блок среди главного тракта
// и активных панов (пан на 768к при главном 192к имеет блок больше FBufSize).
var
Need, p: Integer;
begin
SetLength(FSliceIn, FBufSize * 2);
SetLength(FSliceOut, FBufSize * 2); // с запасом (>= FAudioBufSize*2)
Need := FBufSize;
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(FSliceOutR, FAudioBufSize);
end;
@@ -1714,6 +2006,29 @@ begin
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
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 чтобы делить с слайсами.
procedure TWDSPEngine.ApplyAGCToChan(Chan: Integer; Mode: TWDSPAGCMode;
FixedGain: Double);
@@ -1746,20 +2061,23 @@ end;
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
// под FSliceLock и только при FInitialized.
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
var Chan, DSPRate, DSPSize: Integer;
var Chan, DSPRate, DSPSize, InRate, InSize: Integer;
begin
if not FInitialized then Exit;
if FSlices[Idx].Opened then Exit;
Chan := FSlices[Idx].Chan;
// Вход канала = rate/блок ПАНА слайса (пан 0 = главный захват).
InRate := SliceInRate(Idx);
InSize := SliceInSize(Idx);
// WFM-слайс, как и главный канал, живёт на WFMDSPRate: rsmpin децимирует
// вход, rsmpout сводит к FAudioRate, так что out_size остаётся FAudioBufSize.
DSPRate := SliceDSPRate(FSlices[Idx].Mode);
DSPRate := SliceDSPRateFor(FSlices[Idx].Mode, InRate);
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);
// 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);
create_anbEXT(Chan, 1, InSize, InRate, 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);
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
@@ -1791,51 +2109,123 @@ begin
FSlices[Idx].Opened := False;
end;
// Fan-out: прогоняет тот же входной IQ-блок через каждый активный слайс-канал.
// Вызывается из ProcessRXBlock (DSP-поток) после основного канала.
// Fan-out слайсов главного пана: вызывается из 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;
ProcessSlicesFor(0, FRXAccI, FRXAccQ, FBufSize);
finally
FSliceLock.Leave;
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;
ShiftHz, Volume: Double; AGC: TWDSPAGCMode): Boolean;
ShiftHz, Volume: Double; AGC: TWDSPAGCMode; APanId: Integer): Boolean;
var i, slot: Integer;
begin
Result := False;
FSliceLock.Enter;
try
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;
for i := 0 to MAX_SLICES - 1 do
if not FSlices[i].Active then begin slot := i; Break; end;
@@ -1843,6 +2233,7 @@ begin
FSlices[slot].Active := True;
FSlices[slot].Id := Id;
FSlices[slot].Chan := SLICE_CHAN_BASE + slot;
FSlices[slot].PanId := APanId;
FSlices[slot].Mode := Mode;
FSlices[slot].FilterLo := FilterLo;
FSlices[slot].FilterHi := FilterHi;
@@ -1899,7 +2290,8 @@ begin
// Уход в/из WFM меняет dsp_rate канала — проще пересоздать его целиком,
// чем гонять SetDSPSamplerate: слайс всё равно перестраивает все фильтры.
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;
if Rerate then
begin
@@ -1926,7 +2318,7 @@ begin
FSlices[idx].FilterHi := High;
if FSlices[idx].Opened then
begin
Rate := SliceDSPRate(FSlices[idx].Mode);
Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx));
RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate),
ClampToNyquist(High, Rate));
end;
@@ -2843,10 +3235,12 @@ procedure TWDSPEngine.UpdateSpectrum;
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
// буфера, а не ждёт следующего полного блока.
var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer;
i, N: Integer;
DispID: Integer;
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
WfBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer;
i, N, p: Integer;
DispID: Integer;
GotSpec, GotWf: Boolean;
begin
if not FInitialized or not FAnalyzerOpen then Exit;
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
@@ -2871,6 +3265,34 @@ begin
if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, N);
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;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
@@ -3015,6 +3437,7 @@ begin
SetDisplayNumAverage(RX_DISP_ID, 0, AvgCount);
SetDisplayAvBackmult(RX_DISP_ID, 0, Backmult);
SetDisplayNormOneHz(RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
ReapplyPanDisplaySettings; // детектор/усреднение общие с панами
end;
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
@@ -3039,6 +3462,7 @@ begin
SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount);
SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult);
SetDisplayNormOneHz(RX_DISP_ID, 1, 0);
ReapplyPanDisplaySettings; // детектор/усреднение общие с панами
end;
end.