mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
Merge feature/beacon-zoom-panel: лупа наведения на маяк QO-100
Окно Beacon получило карточку BEACON TUNING: узкий кусок спектра вокруг маяка в
высоком разрешении плюс мини-водопад. На спектрограмме 576к маяк занимает пару
пикселей и ткнуть в него для наведения декодера почти нельзя — здесь то же
наведение делается по широкому следу. Наведение с главной спектрограммы не
тронуто.
Увеличение делает сам WDSP: второй analyzer на том же IQ главного тракта, со
своей FFT и ассиметричным span-clip, посчитанным прямо из границ окна в герцах.
Вошло:
- лупа с кликом-наведением, зумом колесом, панорамированием и FOLLOW;
- автопоиск маяка (AUTO) по min-hold — критерий «непрерывный след», а не
«самый громкий»;
- состояние захвата прямо в лупе: полоса приёма меняет начертание на локе,
рядом SNR и расхождение с опорной частотой;
- окно лупы переживает перезапуск (сдвиг от опорной, а не абсолют);
- контур больше не принимает за маяк голую несущую — проверка BPSK по
констелляции;
- взведённый клик снимается при любом наведении, не только мышью;
- попутно: пропадавшая расстройка тона в статусе терминала телеграфа ('%+d'
в Format Object Pascal не работает).
На эфире проверено частично: наведение и картинка — да, автопоиск и новый
критерий лока — нет.
This commit is contained in:
+1386
-142
File diff suppressed because it is too large
Load Diff
+18
-2
@@ -1661,7 +1661,19 @@ begin
|
||||
PbSpectrum.Invalidate;
|
||||
end;
|
||||
rfBeaconLock:
|
||||
UpdateBeaconButton; // статус/коррекция QO-100 beacon lock
|
||||
begin
|
||||
// Наведение ставит не только клик по спектру: есть AUTO в окне маяка,
|
||||
// web и CAT. Как только декодер куда-то наведён — снимаем «вооружение»,
|
||||
// иначе следующий случайный ЛКМ по панораме утащил бы маяк на пустое
|
||||
// место. Перерисовку просим только в этот момент: маркеры и так едут с
|
||||
// кадрами, а на стопе спектр статичен.
|
||||
if FBeaconArming and (FController.BeaconDecodeFreqHz > 0) then
|
||||
begin
|
||||
FBeaconArming := False;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
UpdateBeaconButton; // статус/коррекция QO-100 beacon lock
|
||||
end;
|
||||
rfCTun:
|
||||
begin
|
||||
StyleButton(BtnCTun, FController.FCTun);
|
||||
@@ -8280,7 +8292,11 @@ begin
|
||||
begin
|
||||
St := Format('RX %d WPM', [FController.CWDecSpeedWPM]);
|
||||
if FController.CWDecToneOffsetHz <> 0 then
|
||||
St := St + Format(', tone %+d Hz', [FController.CWDecToneOffsetHz]);
|
||||
// Знак руками: флага '+' в Format Object Pascal нет, '%+d' съедается
|
||||
// молча и в строку попадает голая 'd'.
|
||||
St := St + Format(', tone %s%d Hz',
|
||||
[IfThen(FController.CWDecToneOffsetHz > 0, '+', ''),
|
||||
FController.CWDecToneOffsetHz]);
|
||||
if not FController.CWDecSignal then St := St + ', no signal';
|
||||
end
|
||||
else
|
||||
|
||||
+132
-4
@@ -324,6 +324,7 @@ type
|
||||
FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
|
||||
// (наведение + остаток декодера) — для оранж. маркера
|
||||
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
|
||||
FBeaconBal: Double; // EMA доли меньшинства знаков I (BPSK-ность)
|
||||
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
|
||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
|
||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||
@@ -332,6 +333,13 @@ type
|
||||
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
|
||||
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
||||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||||
// Лупа наведения (окно Beacon) — состояние окна, переживающее перезапуск.
|
||||
// Центр хранится СДВИГОМ от FBeaconRefHz: так он не зависит ни от правки
|
||||
// опорной частоты, ни от накопленной калибровки LOError.
|
||||
FBcnZoomSpanHz: Double; // ширина окна лупы, Гц
|
||||
FBcnZoomOffsetHz: Double; // центр = FBeaconRefHz + этот сдвиг
|
||||
FBcnZoomFollow: Boolean; // держать наведение декодера в окне
|
||||
FBcnZoomAuto: Boolean; // автопоиск маяка в окне лупы
|
||||
|
||||
// ---- DMR 4FSK receive decoder ----
|
||||
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||||
@@ -988,6 +996,20 @@ type
|
||||
function BeaconDecodeInvert: Boolean;
|
||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||
// --- Лупа маяка (окно наведения в BeaconScopeForm) -------------------
|
||||
// Отдельный WDSP-анализатор на IQ главного тракта, вырезающий узкое окно
|
||||
// вокруг маяка: на общей спектрограмме 576к маяк — пара пикселей, точно
|
||||
// ткнуть в него нельзя. Все частоты — АБСОЛЮТНЫЕ display-Гц, как у
|
||||
// BeaconSeedAtHz. Зовётся с таймера окна маяка; окно закрыто → Enabled=False
|
||||
// и анализатор уничтожается.
|
||||
procedure SetBeaconZoomWindow(AEnabled: Boolean; ACenterHz, ASpanHz: Double);
|
||||
function GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
function GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
// Полоса захвата (display-Гц): за её пределы лупу не увести.
|
||||
procedure GetCaptureWindow(out ALowHz, AHighHz: Double);
|
||||
function BeaconZoomBinHz: Double; // разрешение лупы, Гц/бин (для подписи)
|
||||
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
|
||||
|
||||
@@ -1303,6 +1325,10 @@ begin
|
||||
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
|
||||
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
|
||||
FBeaconDecInvert := True;
|
||||
FBcnZoomSpanHz := 40000.0;
|
||||
FBcnZoomOffsetHz := 0.0;
|
||||
FBcnZoomFollow := True;
|
||||
FBcnZoomAuto := False;
|
||||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||
|
||||
@@ -3277,6 +3303,17 @@ const
|
||||
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
|
||||
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
|
||||
BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого
|
||||
// --- «это точно BPSK, а не голая несущая» ---
|
||||
// CarrierLock+SNR подтверждают лишь то, что в полосе есть узкая линия и Costas
|
||||
// держит фазу. Ровная несущая (телеграф, пораженка, остаток несущей у цифры) даёт
|
||||
// ровно это, причём с ОТЛИЧНЫМ SNR: захваченная Costas'ом, она лежит целиком на
|
||||
// оси I, Q пустой. Контур тогда честно уводил LOError, подтягивая чужой сигнал к
|
||||
// опорной частоте маяка. Различаем по констелляции: у настоящего BPSK точки ходят
|
||||
// между двумя сгустками ±I (данные после свёрточного кодера равновероятны), у
|
||||
// несущей все точки в одном. Меряем долю меньшинства по знаку I.
|
||||
BEACON_BAL_MIN = 0.15; // ниже — на BPSK не похоже, лок не даём
|
||||
BEACON_BAL_DROP = 0.08; // гистерезис: уже взятый лок гаснет ниже этого
|
||||
BEACON_BAL_MIN_PTS = 64; // меньше точек в кольце — судить рано
|
||||
|
||||
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||||
begin
|
||||
@@ -3296,6 +3333,8 @@ begin
|
||||
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
|
||||
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
|
||||
end;
|
||||
FBeaconBal := 0.0; // новое наведение — BPSK-ность доказывается заново
|
||||
FBeaconLocked := False;
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
@@ -3312,6 +3351,7 @@ begin
|
||||
FBeaconCorrCooldown := 0;
|
||||
FBeaconSaveCooldown := 0;
|
||||
FBeaconLocked := False;
|
||||
FBeaconBal := 0.0;
|
||||
FBeaconSNRdB := -99.0;
|
||||
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
|
||||
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
|
||||
@@ -3331,6 +3371,20 @@ begin
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
function BeaconIQBalance(const S: TBeaconScope): Double;
|
||||
// Доля меньшинства среди знаков I в кольце констелляции: 0.5 — идеально
|
||||
// сбалансированный BPSK, 0 — все точки в одном сгустке (немодулированная
|
||||
// несущая). -1 = точек мало, судить рано.
|
||||
var
|
||||
i, Neg: Integer;
|
||||
begin
|
||||
if S.Count < BEACON_BAL_MIN_PTS then Exit(-1.0);
|
||||
Neg := 0;
|
||||
for i := 0 to S.Count - 1 do
|
||||
if S.PtI[i] < 0 then Inc(Neg);
|
||||
Result := Min(Neg, S.Count - Neg) / S.Count;
|
||||
end;
|
||||
|
||||
procedure TRadioController.ServiceBeaconLock;
|
||||
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
|
||||
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
|
||||
@@ -3342,7 +3396,7 @@ procedure TRadioController.ServiceBeaconLock;
|
||||
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
|
||||
var
|
||||
S: TBeaconScope;
|
||||
residHz, measured, e, dLock: Double;
|
||||
residHz, measured, e, dLock, Bal: Double;
|
||||
begin
|
||||
if not FBeaconLockOn then Exit;
|
||||
if not Assigned(FBeaconDec) then Exit;
|
||||
@@ -3359,9 +3413,17 @@ begin
|
||||
|
||||
FBeaconDec.GetScope(S);
|
||||
FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
|
||||
// Солидный лок: декодер несёт несущую И SNR выше порога (гистерезис против мерцания).
|
||||
if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) then FBeaconLocked := True
|
||||
else if FBeaconSNRdB < BEACON_UNLOCK_SNR_DB then FBeaconLocked := False;
|
||||
// Доля меньшинства по знаку I: у BPSK ≈0.5, у голой несущей ≈0. Кольцо в 256
|
||||
// точек при 400 Bd набирается за ~0.6 с, EMA добавляет ещё столько же — то есть
|
||||
// чужой сигнал отваливается примерно за секунду, а не за десяток (кадр FEC).
|
||||
Bal := BeaconIQBalance(S);
|
||||
if Bal >= 0 then FBeaconBal := FBeaconBal + 0.2 * (Bal - FBeaconBal);
|
||||
// Солидный лок: декодер несёт несущую, SNR выше порога И картинка похожа на
|
||||
// BPSK (гистерезис по обоим — против мерцания).
|
||||
if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB)
|
||||
and (FBeaconBal >= BEACON_BAL_MIN) then FBeaconLocked := True
|
||||
else if (FBeaconSNRdB < BEACON_UNLOCK_SNR_DB)
|
||||
or (FBeaconBal < BEACON_BAL_DROP) then FBeaconLocked := False;
|
||||
|
||||
// Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось
|
||||
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
|
||||
@@ -3486,6 +3548,64 @@ begin
|
||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Лупа маяка: окно наведения высокого разрешения (BeaconScopeForm)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Движок работает в Гц относительно центра DDC, наружу отдаём абсолютные
|
||||
// display-Гц — в них живут BeaconSeedAtHz, маркеры и клики пользователя.
|
||||
|
||||
procedure TRadioController.GetCaptureWindow(out ALowHz, AHighHz: Double);
|
||||
var Half: Double;
|
||||
begin
|
||||
Half := FSpanHz / 2.0;
|
||||
if Half <= 0 then Half := FSampleRate / 2.0;
|
||||
ALowHz := FCenterFreq - Half;
|
||||
AHighHz := FCenterFreq + Half;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconZoomWindow(AEnabled: Boolean;
|
||||
ACenterHz, ASpanHz: Double);
|
||||
begin
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
if AEnabled and (ACenterHz > 0) then
|
||||
FDSPEngine.SetBeaconZoom(True, ACenterHz - FCenterFreq, ASpanHz)
|
||||
else
|
||||
FDSPEngine.SetBeaconZoom(False, 0, 0);
|
||||
end;
|
||||
|
||||
function TRadioController.GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
begin
|
||||
Count := 0;
|
||||
LowHz := 0;
|
||||
HighHz := 0;
|
||||
Result := False;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
Result := FDSPEngine.GetBeaconZoomSpectrum(Pixels, Count, LowHz, HighHz);
|
||||
if Count > 0 then
|
||||
begin
|
||||
LowHz := LowHz + FCenterFreq;
|
||||
HighHz := HighHz + FCenterFreq;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
begin
|
||||
Count := 0;
|
||||
Result := False;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
Result := FDSPEngine.GetBeaconZoomWaterfall(Pixels, Count);
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconZoomBinHz: Double;
|
||||
begin
|
||||
Result := 0;
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
if FDSPEngine.BeaconZoomFFT > 0 then
|
||||
Result := FDSPEngine.SampleRate / FDSPEngine.BeaconZoomFFT;
|
||||
end;
|
||||
|
||||
function TRadioController.GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||
begin
|
||||
Result := Assigned(FDMRDec);
|
||||
@@ -3838,6 +3958,10 @@ begin
|
||||
Result.SpecRefLevel := FSpecRefLevel;
|
||||
Result.SpecRange := FSpecRange;
|
||||
Result.SpecGridStep := FSpecGridStep;
|
||||
Result.BeaconZoomSpanHz := FBcnZoomSpanHz;
|
||||
Result.BeaconZoomOffsetHz := FBcnZoomOffsetHz;
|
||||
Result.BeaconZoomFollow := FBcnZoomFollow;
|
||||
Result.BeaconZoomAuto := FBcnZoomAuto;
|
||||
Result.AudioSampleRate := FAudioOut.SampleRate;
|
||||
Result.ShowSpectrum := FShowSpectrum;
|
||||
Result.ShowWaterfall := FShowWaterfall;
|
||||
@@ -7231,6 +7355,10 @@ begin
|
||||
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
||||
FWfSpeedDiv := EnsureRange(FLoadedGlobal.WfSpeedDiv, 1, 8);
|
||||
FSpecMSAA := EnsureRange(FLoadedGlobal.SpecMSAA, 1, 8);
|
||||
FBcnZoomSpanHz := EnsureRange(FLoadedGlobal.BeaconZoomSpanHz, 600.0, 200000.0);
|
||||
FBcnZoomOffsetHz := EnsureRange(FLoadedGlobal.BeaconZoomOffsetHz, -500000.0, 500000.0);
|
||||
FBcnZoomFollow := FLoadedGlobal.BeaconZoomFollow;
|
||||
FBcnZoomAuto := FLoadedGlobal.BeaconZoomAuto;
|
||||
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
||||
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
||||
|
||||
@@ -614,6 +614,13 @@ type
|
||||
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
|
||||
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
||||
SpecGridStep: Double; // grid step in dB, e.g. 10
|
||||
// --- Лупа наведения на маяк QO-100 (карточка BEACON TUNING) ---
|
||||
// Центр хранится СДВИГОМ от опорной частоты маяка, а не абсолютом: так
|
||||
// окно переживает и правку опорной частоты, и калибровку LOError.
|
||||
BeaconZoomSpanHz: Double; // ширина окна лупы, Гц
|
||||
BeaconZoomOffsetHz: Double; // центр окна = BeaconRefHz + этот сдвиг
|
||||
BeaconZoomFollow: Boolean; // держать наведение декодера в окне
|
||||
BeaconZoomAuto: Boolean; // автопоиск маяка в окне лупы
|
||||
// --- Audio settings ---
|
||||
AudioOutDevice: string; // PortAudio output device name ('' = default)
|
||||
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
|
||||
@@ -979,6 +986,10 @@ begin
|
||||
G.SpecRefLevel := -20.0;
|
||||
G.SpecRange := 110.0;
|
||||
G.SpecGridStep := 10.0;
|
||||
G.BeaconZoomSpanHz := 40000.0;
|
||||
G.BeaconZoomOffsetHz := 0.0;
|
||||
G.BeaconZoomFollow := True;
|
||||
G.BeaconZoomAuto := False;
|
||||
G.AudioOutDevice := '';
|
||||
G.AudioSampleRate := 48000;
|
||||
G.AudioInDevice := '';
|
||||
@@ -1229,6 +1240,10 @@ begin
|
||||
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
||||
G.SpecRange := JD(GObj,'spec_range',110.0);
|
||||
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
|
||||
G.BeaconZoomSpanHz := JD(GObj,'beacon_zoom_span',40000.0);
|
||||
G.BeaconZoomOffsetHz := JD(GObj,'beacon_zoom_offset',0.0);
|
||||
G.BeaconZoomFollow := JB(GObj,'beacon_zoom_follow',True);
|
||||
G.BeaconZoomAuto := JB(GObj,'beacon_zoom_auto',False);
|
||||
// Audio settings
|
||||
G.AudioOutDevice := JS(GObj,'audio_out_device','');
|
||||
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
|
||||
@@ -1376,6 +1391,10 @@ begin
|
||||
JW(O,'spec_ref_level',G.SpecRefLevel);
|
||||
JW(O,'spec_range',G.SpecRange);
|
||||
JW(O,'spec_grid_step',G.SpecGridStep);
|
||||
JW(O,'beacon_zoom_span',G.BeaconZoomSpanHz);
|
||||
JW(O,'beacon_zoom_offset',G.BeaconZoomOffsetHz);
|
||||
JW(O,'beacon_zoom_follow',G.BeaconZoomFollow);
|
||||
JW(O,'beacon_zoom_auto',G.BeaconZoomAuto);
|
||||
// Audio settings
|
||||
JWS(O,'audio_out_device',G.AudioOutDevice);
|
||||
JW(O,'audio_sample_rate',G.AudioSampleRate);
|
||||
|
||||
+356
-2
@@ -142,17 +142,29 @@ const
|
||||
|
||||
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||
// Отдельный analyzer лупы QO-100-маяка (окно BeaconScopeForm). Кормится тем
|
||||
// же IQ главного тракта, но со своим FFT и своим span-clip — узкое окно
|
||||
// вокруг маяка в высоком разрешении. Живёт только пока окно маяка открыто.
|
||||
BCN_DISP_ID = 4;
|
||||
|
||||
// --- Панадаптеры на аппаратных 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.
|
||||
// Analyzer id пана N = PAN_DISP_BASE + N (9..11). ID 4 занят лупой маяка
|
||||
// (BCN_DISP_ID), 5 держим свободным про запас.
|
||||
PAN_DISP_BASE = 8;
|
||||
|
||||
DSP_BUFSIZE = 1024;
|
||||
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
|
||||
// Точек в кадре лупы маяка. Фиксировано (а не по ширине виджета), чтобы
|
||||
// изменение размера окна маяка не переармировало analyzer: переарм сбрасывает
|
||||
// FFT-историю, а на крупных FFT она наполняется почти секунду. Рендер сам
|
||||
// ужимает кадр под свою ширину.
|
||||
BCN_ZOOM_PIXELS = 1024;
|
||||
// Частота кадров лупы. Своя, ниже главного дисплея — см.
|
||||
// ApplyBeaconAnalyzerSettings.
|
||||
BCN_ZOOM_FPS = 15.0;
|
||||
DISPLAY_BLOCK_SIZE = 64;
|
||||
|
||||
// Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны).
|
||||
@@ -336,6 +348,27 @@ type
|
||||
FViewLowHz: Double;
|
||||
FViewHighHz: Double;
|
||||
|
||||
// --- Лупа маяка (BCN_DISP_ID) ---------------------------------------
|
||||
// Второй analyzer на том же IQ главного тракта: своя FFT (крупнее) и свой
|
||||
// span-clip, вырезающий узкое окно вокруг маяка. Создаётся/убивается по
|
||||
// запросу UI (окно маяка открыто/закрыто), поэтому весь доступ к disp'у
|
||||
// сериализуется FBcnLock: SetAnalyzer/Destroy зовёт UI-поток, Spectrum0 —
|
||||
// DSP-поток, GetPixels — display-поток.
|
||||
FBcnLock: TCriticalSection;
|
||||
FBcnOpen: Boolean; // XCreateAnalyzer(BCN_DISP_ID) жив
|
||||
FBcnDispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар)
|
||||
FBcnDispPos: Integer;
|
||||
FBcnReqCenter: Double; // запрошенный центр окна, Гц отн. DDC-центра
|
||||
FBcnReqSpan: Double; // запрошенная ширина окна, Гц
|
||||
FBcnLowHz: Double; // фактическое окно (снаппится по бинам FFT)
|
||||
FBcnHighHz: Double;
|
||||
FBcnFFT: Integer; // размер FFT лупы (свой, не FFFTSize)
|
||||
FBcnPixCount: Integer; // точек в кадре лупы
|
||||
FBcnSpecPix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
FBcnWfPix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
FBcnSpecFresh: Boolean; // в кэше есть непрочитанный кадр спектра
|
||||
FBcnWfFresh: Boolean;
|
||||
|
||||
FOnAudio: TOnAudioReady;
|
||||
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
|
||||
FOnSpectrum: TOnSpectrumReady;
|
||||
@@ -572,6 +605,12 @@ type
|
||||
procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc
|
||||
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
||||
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
|
||||
// --- Лупа маяка ---
|
||||
procedure ApplyBeaconAnalyzerSettings; // span-clip окна маяка; под FBcnLock
|
||||
function BeaconFFTFor(SpanHz: Double): Integer; // FFT под запрошенную ширину
|
||||
procedure CloseBeaconAnalyzer; // Destroy + сброс (сам берёт FBcnLock)
|
||||
procedure RearmBeaconAnalyzer; // полный переарм (смена rate)
|
||||
procedure ReapplyBeaconDisplayModes; // детектор/усреднение без переарма
|
||||
|
||||
public
|
||||
constructor Create(SampleRate: Integer = 192000;
|
||||
@@ -771,6 +810,23 @@ type
|
||||
property ViewLowHz: Double read FViewLowHz;
|
||||
property ViewHighHz: Double read FViewHighHz;
|
||||
|
||||
// --- Лупа маяка (BCN_DISP_ID) ---------------------------------------
|
||||
// Включает/выключает второй analyzer на IQ главного тракта и задаёт его
|
||||
// окно: ACenterHz/ASpanHz — В ГЕРЦАХ ОТНОСИТЕЛЬНО ЦЕНТРА DDC (как
|
||||
// ViewLowHz/ViewHighHz). Окно клампится в полосу захвата. Вызывать из
|
||||
// UI-потока. Enabled=False — analyzer уничтожается (нулевая цена, когда
|
||||
// окно маяка закрыто).
|
||||
procedure SetBeaconZoom(AEnabled: Boolean; ACenterHz, ASpanHz: Double);
|
||||
// Забрать последний кадр лупы. Fresh=False, если нового кадра не было —
|
||||
// вызывающий рисует предыдущий. Low/High — фактическое окно (Гц отн.
|
||||
// центра DDC), уже снаппенное по бинам FFT.
|
||||
function GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
function GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
property BeaconZoomOpen: Boolean read FBcnOpen;
|
||||
property BeaconZoomFFT: Integer read FBcnFFT;
|
||||
|
||||
property Initialized: Boolean read FInitialized;
|
||||
property SampleRate: Integer read FSampleRate;
|
||||
property TXSampleRate: Integer read FTXSampleRate;
|
||||
@@ -1427,6 +1483,16 @@ begin
|
||||
FMainGated := False;
|
||||
FMainLock := TCriticalSection.Create;
|
||||
|
||||
// Лупа маяка — analyzer создаётся по требованию UI (SetBeaconZoom)
|
||||
FBcnLock := TCriticalSection.Create;
|
||||
FBcnOpen := False;
|
||||
FBcnDispPos := 0;
|
||||
FBcnReqCenter := 0.0;
|
||||
FBcnReqSpan := 40000.0;
|
||||
FBcnFFT := 0;
|
||||
FBcnPixCount := 0;
|
||||
SetLength(FBcnDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
|
||||
// Мультислайсы
|
||||
FSliceLock := TCriticalSection.Create;
|
||||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||
@@ -1526,6 +1592,7 @@ begin
|
||||
RTLEventDestroy(FTXMicSem);
|
||||
FSliceLock.Free;
|
||||
FMainLock.Free;
|
||||
FBcnLock.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
@@ -1757,12 +1824,241 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Лупа маяка (BCN_DISP_ID)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TWDSPEngine.BeaconFFTFor(SpanHz: Double): Integer;
|
||||
// FFT лупы выбираем ОТ ШИРИНЫ ОКНА, а не от настройки дисплея: цель — держать
|
||||
// ~BINS_PER_PIXEL бинов на точку кадра, чтобы узкое окно оставалось детальным
|
||||
// (в этом весь смысл лупы). Span-clip НЕ удешевляет FFT — она всё равно по всей
|
||||
// полосе захвата, поэтому потолок жёсткий: 262144 (= max_size в
|
||||
// XCreateAnalyzer), а частота кадров лупы понижена (BCN_ZOOM_FPS).
|
||||
const
|
||||
BINS_PER_PIXEL = 2.0;
|
||||
var
|
||||
Want: Double;
|
||||
begin
|
||||
if SpanHz <= 0 then Exit(65536);
|
||||
Want := BINS_PER_PIXEL * BCN_ZOOM_PIXELS * FSampleRate / SpanHz;
|
||||
Result := 16384;
|
||||
while (Result < Want) and (Result < 262144) do
|
||||
Result := Result shl 1;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyBeaconAnalyzerSettings;
|
||||
// Переармирует analyzer лупы под FBcnReqCenter/FBcnReqSpan. Вызывать ПОД
|
||||
// FBcnLock при FBcnOpen=True.
|
||||
//
|
||||
// Окно вырезается тем же span-clip'ом, что и зум главного спектра, но здесь
|
||||
// клипы считаются НЕ из зум-слайдера, а прямо из желаемых границ в Гц —
|
||||
// анализатор умеет ассиметричный клип, поэтому окно можно поставить в любое
|
||||
// место полосы захвата, а не только вокруг центра DDC.
|
||||
var
|
||||
AvgCount, WfAvgCount, MaxW, Overlap: Integer;
|
||||
Backmult, WfBackmult: Double;
|
||||
Bins, ClipL, ClipH: Integer;
|
||||
BinWidth, BW, WantLo, WantHi: Double;
|
||||
begin
|
||||
FBcnFFT := BeaconFFTFor(FBcnReqSpan);
|
||||
FBcnDispPos := 0;
|
||||
BinWidth := FSampleRate / FBcnFFT;
|
||||
Bins := FBcnFFT;
|
||||
BW := Bins * BinWidth;
|
||||
|
||||
// Желаемые границы, зажатые в полосу захвата с запасом в один бин.
|
||||
WantLo := FBcnReqCenter - FBcnReqSpan / 2.0;
|
||||
WantHi := FBcnReqCenter + FBcnReqSpan / 2.0;
|
||||
if WantLo < -0.5 * BW + BinWidth then WantLo := -0.5 * BW + BinWidth;
|
||||
if WantHi > 0.5 * BW - BinWidth then WantHi := 0.5 * BW - BinWidth;
|
||||
if WantHi - WantLo < 4 * BinWidth then WantHi := WantLo + 4 * BinWidth;
|
||||
|
||||
ClipL := Floor((WantLo + 0.5 * BW + BinWidth / 2.0) / BinWidth);
|
||||
ClipH := Floor((0.5 * BW - BinWidth / 2.0 - WantHi) / BinWidth);
|
||||
ClipL := EnsureRange(ClipL, 0, Bins - 4);
|
||||
ClipH := EnsureRange(ClipH, 0, Bins - 4 - ClipL);
|
||||
// Фактическое окно — то же выражение, что в ApplyRXAnalyzerSettings.
|
||||
FBcnLowHz := -(0.5 * BW - ClipL * BinWidth + BinWidth / 2.0);
|
||||
FBcnHighHz := (0.5 * BW - ClipH * BinWidth - BinWidth / 2.0);
|
||||
|
||||
// Кадры лупы считаем реже главного дисплея: FFT здесь крупная (до 262144) и
|
||||
// идёт по ВСЕЙ полосе захвата, а для наведения на маяк 15 кадров/с с запасом.
|
||||
// На 60 fps это была бы полноценная загрузка ядра ради картинки, в которой
|
||||
// ничего не меняется.
|
||||
Overlap := Max(0, Ceil(FBcnFFT - FSampleRate / BCN_ZOOM_FPS));
|
||||
MaxW := FBcnFFT + Round(Min(0.10 * FSampleRate, 0.10 * FBcnFFT * BCN_ZOOM_FPS));
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||
FBcnPixCount := BCN_ZOOM_PIXELS;
|
||||
|
||||
SetAnalyzer(BCN_DISP_ID, 2, 1, 1, @FFlp[0], FBcnFFT, DISPLAY_BLOCK_SIZE,
|
||||
FWindowType, 14.0, Overlap, 0, ClipL, ClipH, BCN_ZOOM_PIXELS,
|
||||
1, 0, 0.0, 0.0, MaxW);
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 0, FSpecDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 0, FSpecAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 0, AvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 0, Backmult);
|
||||
SetDisplayNormOneHz (BCN_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 1, WfAvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 1, WfBackmult);
|
||||
SetDisplayNormOneHz (BCN_DISP_ID, 1, 0);
|
||||
SetDisplaySampleRate (BCN_DISP_ID, FSampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.CloseBeaconAnalyzer;
|
||||
begin
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnOpen := False; // DSP/display-потоки перестают трогать disp
|
||||
DestroyAnalyzer(BCN_DISP_ID);
|
||||
FBcnSpecFresh := False;
|
||||
FBcnWfFresh := False;
|
||||
FBcnPixCount := 0;
|
||||
FBcnDispPos := 0;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.RearmBeaconAnalyzer;
|
||||
// Полный переарм лупы — после смены sample rate: и окно, и выбор FFT считаются
|
||||
// от FSampleRate.
|
||||
begin
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if FBcnOpen then ApplyBeaconAnalyzerSettings;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ReapplyBeaconDisplayModes;
|
||||
// Детектор/усреднение с главного дисплея на лупу БЕЗ SetAnalyzer: переарм
|
||||
// сбросил бы FFT-историю (на 256к бинах это ~0.5с замершего кадра), а сменой
|
||||
// детектора/усреднения окно лупы не меняется.
|
||||
var
|
||||
AvgCount, WfAvgCount: Integer;
|
||||
Backmult, WfBackmult: Double;
|
||||
begin
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 0, FSpecDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 0, FSpecAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 0, AvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 0, Backmult);
|
||||
SetDisplayNormOneHz (BCN_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||
SetDisplayDetectorMode(BCN_DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode (BCN_DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage (BCN_DISP_ID, 1, WfAvgCount);
|
||||
SetDisplayAvBackmult (BCN_DISP_ID, 1, WfBackmult);
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetBeaconZoom(AEnabled: Boolean; ACenterHz, ASpanHz: Double);
|
||||
var
|
||||
Success: Integer;
|
||||
SameWindow: Boolean;
|
||||
begin
|
||||
if (not AEnabled) or (not FInitialized) or (not FAnalyzerOpen) then
|
||||
begin
|
||||
CloseBeaconAnalyzer;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
ASpanHz := EnsureRange(ASpanHz, 200.0, FSampleRate);
|
||||
ACenterHz := EnsureRange(ACenterHz, -FSampleRate / 2.0, FSampleRate / 2.0);
|
||||
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
// Переарм сбрасывает историю FFT (кадр «замирает» на время наполнения) —
|
||||
// на неизменившемся окне не дёргаем, лупа зовётся из таймера UI.
|
||||
SameWindow := FBcnOpen and
|
||||
(Abs(ACenterHz - FBcnReqCenter) < 0.5) and
|
||||
(Abs(ASpanHz - FBcnReqSpan) < 0.5);
|
||||
FBcnReqCenter := ACenterHz;
|
||||
FBcnReqSpan := ASpanHz;
|
||||
if SameWindow then Exit;
|
||||
|
||||
if not FBcnOpen then
|
||||
begin
|
||||
Success := -1;
|
||||
XCreateAnalyzer(BCN_DISP_ID, @Success, 262144, 1, 1, nil);
|
||||
if Success <> 0 then
|
||||
begin
|
||||
FLastError := Format('XCreateAnalyzer(BCN) failed: %d', [Success]);
|
||||
Exit; // лупа не поднялась — окно маяка просто останется пустым
|
||||
end;
|
||||
FBcnOpen := True;
|
||||
end;
|
||||
ApplyBeaconAnalyzerSettings;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||||
var N: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
Count := 0;
|
||||
LowHz := 0;
|
||||
HighHz := 0;
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
LowHz := FBcnLowHz;
|
||||
HighHz := FBcnHighHz;
|
||||
N := Min(FBcnPixCount, Length(Pixels));
|
||||
if N <= 0 then Exit;
|
||||
Move(FBcnSpecPix[0], Pixels[0], N * SizeOf(Single));
|
||||
Count := N;
|
||||
Result := FBcnSpecFresh;
|
||||
FBcnSpecFresh := False;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||||
out Count: Integer): Boolean;
|
||||
var N: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
Count := 0;
|
||||
if not FBcnOpen then Exit;
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if not FBcnOpen then Exit;
|
||||
N := Min(FBcnPixCount, Length(Pixels));
|
||||
if N <= 0 then Exit;
|
||||
Move(FBcnWfPix[0], Pixels[0], N * SizeOf(Single));
|
||||
Count := N;
|
||||
Result := FBcnWfFresh;
|
||||
FBcnWfFresh := False;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам.
|
||||
begin
|
||||
ApplyRXAnalyzerSettings;
|
||||
ApplyTXAnalyzerSettings;
|
||||
ReapplyPanDisplaySettings;
|
||||
ReapplyBeaconDisplayModes;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
|
||||
@@ -1796,6 +2092,9 @@ begin
|
||||
end;
|
||||
DestroyAnalyzer(RX_DISP_ID);
|
||||
DestroyAnalyzer(TX_DISP_ID);
|
||||
// Лупа маяка: не пересоздаётся в OpenAnalyzer — окно маяка запросит её сама
|
||||
// (SetBeaconZoom зовётся с таймера формы).
|
||||
CloseBeaconAnalyzer;
|
||||
// Анализаторы доп. панов (записи FPans сохраняются — пересоздание в OpenAnalyzer).
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
@@ -2085,6 +2384,8 @@ begin
|
||||
// главного дисплея сбрасывается (при смене rate это неизбежно), паны
|
||||
// не затронуты.
|
||||
if FAnalyzerOpen then ApplyRXAnalyzerSettings;
|
||||
// Лупа маяка тоже считает окно и FFT от rate — переармируем.
|
||||
RearmBeaconAnalyzer;
|
||||
|
||||
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
|
||||
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
|
||||
@@ -2310,6 +2611,26 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// Лупа маяка — второй analyzer на том же потоке сэмплов. Живёт только пока
|
||||
// окно маяка открыто, поэтому в обычном режиме это одна проверка Boolean.
|
||||
if FBcnOpen then
|
||||
begin
|
||||
FBcnDispBuf[FBcnDispPos * 2] := DI;
|
||||
FBcnDispBuf[FBcnDispPos * 2 + 1] := DQ;
|
||||
Inc(FBcnDispPos);
|
||||
if FBcnDispPos >= DISPLAY_BLOCK_SIZE then
|
||||
begin
|
||||
FBcnDispPos := 0;
|
||||
FBcnLock.Enter; // UI-поток может в этот момент рвать analyzer
|
||||
try
|
||||
if FBcnOpen then
|
||||
Spectrum0(1, BCN_DISP_ID, 0, 0, @FBcnDispBuf[0]);
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Bec <> nil then Bec.Feed(DI, DQ);
|
||||
|
||||
FRXAccI[FRXAccPos] := DI;
|
||||
@@ -4058,6 +4379,7 @@ var
|
||||
i, N, p: Integer;
|
||||
DispID: Integer;
|
||||
Cal: Single;
|
||||
MainCal: Single; // калибровка главного тракта (Cal ниже перетирается панами)
|
||||
GotSpec, GotWf: Boolean;
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
@@ -4068,6 +4390,7 @@ begin
|
||||
// На TX-дисплее коррекция входного тракта не при чём.
|
||||
if Assigned(FOnDispCal) and (DispID = RX_DISP_ID) then Cal := FOnDispCal(0)
|
||||
else Cal := 0;
|
||||
MainCal := Cal;
|
||||
|
||||
Flag := 0;
|
||||
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||||
@@ -4128,6 +4451,37 @@ begin
|
||||
if GotWf and Assigned(FOnPanWaterfall) then
|
||||
FOnPanWaterfall(p, WfBuf, N);
|
||||
end;
|
||||
|
||||
// Лупа маяка: кадр кладём в кэш, а не в колбэк — окно маяка тянет его своим
|
||||
// таймером (форма живёт отдельно от главного рендера и может быть закрыта).
|
||||
if FBcnOpen then
|
||||
begin
|
||||
FBcnLock.Enter;
|
||||
try
|
||||
if FBcnOpen then
|
||||
begin
|
||||
N := EnsureRange(FBcnPixCount, 0, SPECTRUM_PIXELS);
|
||||
Flag := 0;
|
||||
GetPixels(BCN_DISP_ID, 0, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
FBcnSpecPix[i] := PixBuf[i] + MainCal;
|
||||
FBcnSpecFresh := True;
|
||||
end;
|
||||
Flag := 0;
|
||||
GetPixels(BCN_DISP_ID, 1, @WfBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
FBcnWfPix[i] := WfBuf[i] + MainCal;
|
||||
FBcnWfFresh := True;
|
||||
end;
|
||||
end;
|
||||
finally
|
||||
FBcnLock.Leave;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
||||
|
||||
Reference in New Issue
Block a user