mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
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:
+62
-26
@@ -53,10 +53,8 @@ uses
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
|
||||
const
|
||||
// Панадаптеры (этап 3): потолок панов = главный + до 3 доп. аппаратных DDC
|
||||
// (кап по Ethernet/CPU, см. doc/SLICES_PLAN.md 3.1). Фактический лимит
|
||||
// бэкенда — Caps.MaxPans (Pluto = 1).
|
||||
MAX_PANS = 4;
|
||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||||
// Допустимые rate доп. пан-DDC, кГц (дефолт 384).
|
||||
PAN_DDC_RATES: array[0..2] of Word = (192, 384, 768);
|
||||
|
||||
@@ -136,6 +134,7 @@ type
|
||||
Active: Boolean;
|
||||
Id: Integer; // = id слайса в TWDSPEngine
|
||||
Letter: Char; // буква флага ('A'..'F'), пока для UI
|
||||
PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан
|
||||
TargetHz: Double; // абсолютная частота слайса
|
||||
Mode: Integer;
|
||||
FilterLo: Integer;
|
||||
@@ -166,9 +165,10 @@ type
|
||||
function FindSliceIndex(Id: Integer): Integer; // -1 если нет
|
||||
procedure OnSliceAudioReady(SliceId: Integer;
|
||||
const Left, Right: array of Single; Count: Integer);
|
||||
procedure RefreshSliceShifts; // пересчёт ShiftHz всех слайсов при смене центра
|
||||
// --- Панадаптеры на аппаратных DDC (этап 3.1) ---
|
||||
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
|
||||
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
|
||||
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
|
||||
function PanCenterHz(PanId: Integer): Double; // центр пана (0 = FCenterFreq)
|
||||
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||||
@@ -534,10 +534,12 @@ type
|
||||
// ---- Мультислайсы (софт-fan-out) ----
|
||||
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
|
||||
// на устройстве DevIndex (-1=default). Возвращает Id слайса или -1 при отказе
|
||||
// (нет слотов / TargetHz вне захваченной полосы). DevName для персиста/UI.
|
||||
// (нет слотов / TargetHz вне полосы пана). DevName для персиста/UI.
|
||||
// APanId>0 — слайс внутри доп. DDC-пана (тот должен быть создан AddPanDDC).
|
||||
function AddSlice(TargetHz: Double; Mode, FilterLo, FilterHi: Integer;
|
||||
AGC: TWDSPAGCMode; Vol: Double;
|
||||
DevIndex: Integer; const DevName: string): Integer;
|
||||
DevIndex: Integer; const DevName: string;
|
||||
APanId: Integer = 0): Integer;
|
||||
procedure RemoveSlice(Id: Integer);
|
||||
procedure SetSliceTarget(Id: Integer; TargetHz: Double);
|
||||
procedure SetSliceMode(Id, Mode: Integer);
|
||||
@@ -554,9 +556,9 @@ type
|
||||
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
|
||||
function SliceCount: Integer;
|
||||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||||
// Диапазон захваченной полосы вокруг центра, куда можно ставить софт-слайс.
|
||||
procedure CapturedSpan(out LoHz, HiHz: Double);
|
||||
function SliceFitsCapture(TargetHz: Double): Boolean;
|
||||
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
||||
procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
|
||||
function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean;
|
||||
|
||||
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
|
||||
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр (сырые Гц
|
||||
@@ -1128,27 +1130,41 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.RefreshSliceShifts;
|
||||
// Смена центра ГЛАВНОГО захвата: пересчитываем только слайсы пана 0 —
|
||||
// слайсы доп. панов привязаны к центрам своих DDC (SetPanDDCFreq).
|
||||
var i: Integer;
|
||||
begin
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
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);
|
||||
end;
|
||||
|
||||
procedure TRadioController.CapturedSpan(out LoHz, HiHz: Double);
|
||||
// Полезная полоса ±47.5% от захвата (запас на краях фильтра/анти-алиасинга).
|
||||
var Half: Double;
|
||||
function TRadioController.PanCenterHz(PanId: Integer): Double;
|
||||
begin
|
||||
Half := FSampleRate * 0.475;
|
||||
LoHz := FCenterFreq - Half;
|
||||
HiHz := FCenterFreq + Half;
|
||||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0) then
|
||||
Result := FPanFreqHz[PanId]
|
||||
else
|
||||
Result := FCenterFreq;
|
||||
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;
|
||||
begin
|
||||
CapturedSpan(Lo, Hi);
|
||||
CapturedSpan(Lo, Hi, PanId);
|
||||
Result := (TargetHz >= Lo) and (TargetHz <= Hi);
|
||||
end;
|
||||
|
||||
@@ -1191,6 +1207,11 @@ begin
|
||||
if (i < MAX_DDCS) and (FDDCPanMap[i] < 0) then begin ddc := i; Break; end;
|
||||
if ddc < 0 then Exit;
|
||||
|
||||
// Движок: аккумулятор + analyzer пана (до включения DDC — к приходу первого
|
||||
// пакета пан уже готов; PushPanIQ без пана просто дропает).
|
||||
if FWDSPReady and not FDSPEngine.AddPan(PanId, Integer(RateKHz) * 1000) then
|
||||
Exit;
|
||||
|
||||
FPanDDCIdx[PanId] := ddc;
|
||||
FPanFreqHz[PanId] := FreqHz;
|
||||
FPanRateKHz[PanId] := RateKHz;
|
||||
@@ -1200,27 +1221,38 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.RemovePanDDC(PanId: Integer);
|
||||
var ddc: Integer;
|
||||
var ddc, i: Integer;
|
||||
begin
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
ddc := FPanDDCIdx[PanId];
|
||||
if ddc < 0 then Exit;
|
||||
// Сначала снимаем роутинг (OnDDCIQ в сетевом потоке перестаёт брать пакеты
|
||||
// этого DDC), потом гасим DDC в железе.
|
||||
// этого DDC), потом слайсы пана (их звуковухи), потом движок и DDC в железе.
|
||||
FDDCPanMap[ddc] := -1;
|
||||
FPanDDCIdx[PanId] := -1;
|
||||
FPanFreqHz[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);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetPanDDCFreq(PanId: Integer; FreqHz: Double);
|
||||
var i: Integer;
|
||||
begin
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
if FPanDDCIdx[PanId] < 0 then Exit;
|
||||
FPanFreqHz[PanId] := FreqHz;
|
||||
if Assigned(FNetwork) then
|
||||
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;
|
||||
|
||||
function TRadioController.PanDDCActive(PanId: Integer): Boolean;
|
||||
@@ -1242,14 +1274,16 @@ end;
|
||||
|
||||
function TRadioController.AddSlice(TargetHz: Double;
|
||||
Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double;
|
||||
DevIndex: Integer; const DevName: string): Integer;
|
||||
DevIndex: Integer; const DevName: string; APanId: Integer): Integer;
|
||||
var
|
||||
i, slot, Id: Integer;
|
||||
AO: TAudioOutput;
|
||||
begin
|
||||
Result := -1;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
if not SliceFitsCapture(TargetHz) then Exit; // вне захваченной полосы (Этап 3=HW DDC)
|
||||
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;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
@@ -1258,7 +1292,8 @@ begin
|
||||
|
||||
Id := FNextSliceId;
|
||||
if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi,
|
||||
TargetHz - FCenterFreq, Vol, AGC) then Exit;
|
||||
TargetHz - PanCenterHz(APanId), Vol, AGC,
|
||||
APanId) then Exit;
|
||||
Inc(FNextSliceId);
|
||||
|
||||
// Свой аудио-выход слайса на выбранной звуковухе.
|
||||
@@ -1272,6 +1307,7 @@ begin
|
||||
// Буква флага. Слайс A — это главный приёмник (не в FSlices[]), поэтому доп.
|
||||
// слайсы нумеруются с 'B' (slot 0 → B, 1 → C, …), иначе получалось два «A».
|
||||
FSlices[slot].Letter := Chr(Ord('B') + slot);
|
||||
FSlices[slot].PanId := APanId;
|
||||
FSlices[slot].TargetHz := TargetHz;
|
||||
FSlices[slot].Mode := Mode;
|
||||
FSlices[slot].FilterLo := FilterLo;
|
||||
@@ -1311,7 +1347,7 @@ begin
|
||||
if idx < 0 then Exit;
|
||||
FSlices[idx].TargetHz := TargetHz;
|
||||
if Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetSliceShift(Id, TargetHz - FCenterFreq);
|
||||
FDSPEngine.SetSliceShift(Id, TargetHz - PanCenterHz(FSlices[idx].PanId));
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceMode(Id, Mode: Integer);
|
||||
|
||||
+480
-56
@@ -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.
|
||||
|
||||
@@ -283,6 +283,43 @@ static/глобального. (CPU-LUT палитры общий — это о
|
||||
создание/удаление пана только под локом, как переоткрытие слайсов при
|
||||
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: второй панадаптер в главном окне
|
||||
|
||||
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
|
||||
|
||||
Reference in New Issue
Block a user