mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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;
|
PbSpectrum.Invalidate;
|
||||||
end;
|
end;
|
||||||
rfBeaconLock:
|
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:
|
rfCTun:
|
||||||
begin
|
begin
|
||||||
StyleButton(BtnCTun, FController.FCTun);
|
StyleButton(BtnCTun, FController.FCTun);
|
||||||
@@ -8280,7 +8292,11 @@ begin
|
|||||||
begin
|
begin
|
||||||
St := Format('RX %d WPM', [FController.CWDecSpeedWPM]);
|
St := Format('RX %d WPM', [FController.CWDecSpeedWPM]);
|
||||||
if FController.CWDecToneOffsetHz <> 0 then
|
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';
|
if not FController.CWDecSignal then St := St + ', no signal';
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|||||||
+132
-4
@@ -324,6 +324,7 @@ type
|
|||||||
FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
|
FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
|
||||||
// (наведение + остаток декодера) — для оранж. маркера
|
// (наведение + остаток декодера) — для оранж. маркера
|
||||||
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
|
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
|
||||||
|
FBeaconBal: Double; // EMA доли меньшинства знаков I (BPSK-ность)
|
||||||
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
|
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
|
||||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
|
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
|
||||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||||
@@ -332,6 +333,13 @@ type
|
|||||||
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
|
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
|
||||||
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
||||||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||||||
|
// Лупа наведения (окно Beacon) — состояние окна, переживающее перезапуск.
|
||||||
|
// Центр хранится СДВИГОМ от FBeaconRefHz: так он не зависит ни от правки
|
||||||
|
// опорной частоты, ни от накопленной калибровки LOError.
|
||||||
|
FBcnZoomSpanHz: Double; // ширина окна лупы, Гц
|
||||||
|
FBcnZoomOffsetHz: Double; // центр = FBeaconRefHz + этот сдвиг
|
||||||
|
FBcnZoomFollow: Boolean; // держать наведение декодера в окне
|
||||||
|
FBcnZoomAuto: Boolean; // автопоиск маяка в окне лупы
|
||||||
|
|
||||||
// ---- DMR 4FSK receive decoder ----
|
// ---- DMR 4FSK receive decoder ----
|
||||||
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||||||
@@ -988,6 +996,20 @@ type
|
|||||||
function BeaconDecodeInvert: Boolean;
|
function BeaconDecodeInvert: Boolean;
|
||||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): 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 GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||||
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
|
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
|
||||||
|
|
||||||
@@ -1303,6 +1325,10 @@ begin
|
|||||||
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
|
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
|
||||||
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
|
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
|
||||||
FBeaconDecInvert := True;
|
FBeaconDecInvert := True;
|
||||||
|
FBcnZoomSpanHz := 40000.0;
|
||||||
|
FBcnZoomOffsetHz := 0.0;
|
||||||
|
FBcnZoomFollow := True;
|
||||||
|
FBcnZoomAuto := False;
|
||||||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||||
|
|
||||||
@@ -3277,6 +3303,17 @@ const
|
|||||||
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
|
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
|
||||||
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
|
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
|
||||||
BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого
|
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);
|
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||||||
begin
|
begin
|
||||||
@@ -3296,6 +3333,8 @@ begin
|
|||||||
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
|
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
|
||||||
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
|
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
|
||||||
end;
|
end;
|
||||||
|
FBeaconBal := 0.0; // новое наведение — BPSK-ность доказывается заново
|
||||||
|
FBeaconLocked := False;
|
||||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
|
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
|
||||||
Changed(rfBeaconLock);
|
Changed(rfBeaconLock);
|
||||||
end;
|
end;
|
||||||
@@ -3312,6 +3351,7 @@ begin
|
|||||||
FBeaconCorrCooldown := 0;
|
FBeaconCorrCooldown := 0;
|
||||||
FBeaconSaveCooldown := 0;
|
FBeaconSaveCooldown := 0;
|
||||||
FBeaconLocked := False;
|
FBeaconLocked := False;
|
||||||
|
FBeaconBal := 0.0;
|
||||||
FBeaconSNRdB := -99.0;
|
FBeaconSNRdB := -99.0;
|
||||||
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
|
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
|
||||||
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
|
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
|
||||||
@@ -3331,6 +3371,20 @@ begin
|
|||||||
Changed(rfBeaconLock);
|
Changed(rfBeaconLock);
|
||||||
end;
|
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;
|
procedure TRadioController.ServiceBeaconLock;
|
||||||
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
|
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
|
||||||
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
|
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
|
||||||
@@ -3342,7 +3396,7 @@ procedure TRadioController.ServiceBeaconLock;
|
|||||||
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
|
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
|
||||||
var
|
var
|
||||||
S: TBeaconScope;
|
S: TBeaconScope;
|
||||||
residHz, measured, e, dLock: Double;
|
residHz, measured, e, dLock, Bal: Double;
|
||||||
begin
|
begin
|
||||||
if not FBeaconLockOn then Exit;
|
if not FBeaconLockOn then Exit;
|
||||||
if not Assigned(FBeaconDec) then Exit;
|
if not Assigned(FBeaconDec) then Exit;
|
||||||
@@ -3359,9 +3413,17 @@ begin
|
|||||||
|
|
||||||
FBeaconDec.GetScope(S);
|
FBeaconDec.GetScope(S);
|
||||||
FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
|
FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
|
||||||
// Солидный лок: декодер несёт несущую И SNR выше порога (гистерезис против мерцания).
|
// Доля меньшинства по знаку I: у BPSK ≈0.5, у голой несущей ≈0. Кольцо в 256
|
||||||
if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) then FBeaconLocked := True
|
// точек при 400 Bd набирается за ~0.6 с, EMA добавляет ещё столько же — то есть
|
||||||
else if FBeaconSNRdB < BEACON_UNLOCK_SNR_DB then FBeaconLocked := False;
|
// чужой сигнал отваливается примерно за секунду, а не за десяток (кадр 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 выравнивает ось
|
// Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось
|
||||||
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
|
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
|
||||||
@@ -3486,6 +3548,64 @@ begin
|
|||||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||||
end;
|
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;
|
function TRadioController.GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||||
begin
|
begin
|
||||||
Result := Assigned(FDMRDec);
|
Result := Assigned(FDMRDec);
|
||||||
@@ -3838,6 +3958,10 @@ begin
|
|||||||
Result.SpecRefLevel := FSpecRefLevel;
|
Result.SpecRefLevel := FSpecRefLevel;
|
||||||
Result.SpecRange := FSpecRange;
|
Result.SpecRange := FSpecRange;
|
||||||
Result.SpecGridStep := FSpecGridStep;
|
Result.SpecGridStep := FSpecGridStep;
|
||||||
|
Result.BeaconZoomSpanHz := FBcnZoomSpanHz;
|
||||||
|
Result.BeaconZoomOffsetHz := FBcnZoomOffsetHz;
|
||||||
|
Result.BeaconZoomFollow := FBcnZoomFollow;
|
||||||
|
Result.BeaconZoomAuto := FBcnZoomAuto;
|
||||||
Result.AudioSampleRate := FAudioOut.SampleRate;
|
Result.AudioSampleRate := FAudioOut.SampleRate;
|
||||||
Result.ShowSpectrum := FShowSpectrum;
|
Result.ShowSpectrum := FShowSpectrum;
|
||||||
Result.ShowWaterfall := FShowWaterfall;
|
Result.ShowWaterfall := FShowWaterfall;
|
||||||
@@ -7231,6 +7355,10 @@ begin
|
|||||||
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
||||||
FWfSpeedDiv := EnsureRange(FLoadedGlobal.WfSpeedDiv, 1, 8);
|
FWfSpeedDiv := EnsureRange(FLoadedGlobal.WfSpeedDiv, 1, 8);
|
||||||
FSpecMSAA := EnsureRange(FLoadedGlobal.SpecMSAA, 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);
|
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||||
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
||||||
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
||||||
|
|||||||
@@ -614,6 +614,13 @@ type
|
|||||||
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
|
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
|
||||||
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
||||||
SpecGridStep: Double; // grid step in dB, e.g. 10
|
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 ---
|
// --- Audio settings ---
|
||||||
AudioOutDevice: string; // PortAudio output device name ('' = default)
|
AudioOutDevice: string; // PortAudio output device name ('' = default)
|
||||||
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
|
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
|
||||||
@@ -979,6 +986,10 @@ begin
|
|||||||
G.SpecRefLevel := -20.0;
|
G.SpecRefLevel := -20.0;
|
||||||
G.SpecRange := 110.0;
|
G.SpecRange := 110.0;
|
||||||
G.SpecGridStep := 10.0;
|
G.SpecGridStep := 10.0;
|
||||||
|
G.BeaconZoomSpanHz := 40000.0;
|
||||||
|
G.BeaconZoomOffsetHz := 0.0;
|
||||||
|
G.BeaconZoomFollow := True;
|
||||||
|
G.BeaconZoomAuto := False;
|
||||||
G.AudioOutDevice := '';
|
G.AudioOutDevice := '';
|
||||||
G.AudioSampleRate := 48000;
|
G.AudioSampleRate := 48000;
|
||||||
G.AudioInDevice := '';
|
G.AudioInDevice := '';
|
||||||
@@ -1229,6 +1240,10 @@ begin
|
|||||||
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
||||||
G.SpecRange := JD(GObj,'spec_range',110.0);
|
G.SpecRange := JD(GObj,'spec_range',110.0);
|
||||||
G.SpecGridStep := JD(GObj,'spec_grid_step',10.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
|
// Audio settings
|
||||||
G.AudioOutDevice := JS(GObj,'audio_out_device','');
|
G.AudioOutDevice := JS(GObj,'audio_out_device','');
|
||||||
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
|
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
|
||||||
@@ -1376,6 +1391,10 @@ begin
|
|||||||
JW(O,'spec_ref_level',G.SpecRefLevel);
|
JW(O,'spec_ref_level',G.SpecRefLevel);
|
||||||
JW(O,'spec_range',G.SpecRange);
|
JW(O,'spec_range',G.SpecRange);
|
||||||
JW(O,'spec_grid_step',G.SpecGridStep);
|
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
|
// Audio settings
|
||||||
JWS(O,'audio_out_device',G.AudioOutDevice);
|
JWS(O,'audio_out_device',G.AudioOutDevice);
|
||||||
JW(O,'audio_sample_rate',G.AudioSampleRate);
|
JW(O,'audio_sample_rate',G.AudioSampleRate);
|
||||||
|
|||||||
+356
-2
@@ -142,17 +142,29 @@ const
|
|||||||
|
|
||||||
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||||
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||||
|
// Отдельный analyzer лупы QO-100-маяка (окно BeaconScopeForm). Кормится тем
|
||||||
|
// же IQ главного тракта, но со своим FFT и своим span-clip — узкое окно
|
||||||
|
// вокруг маяка в высоком разрешении. Живёт только пока окно маяка открыто.
|
||||||
|
BCN_DISP_ID = 4;
|
||||||
|
|
||||||
// --- Панадаптеры на аппаратных DDC (этап 3) ---
|
// --- Панадаптеры на аппаратных DDC (этап 3) ---
|
||||||
// Пан 0 = главный тракт (RXA/RX_DISP_ID), доп. паны 1..MAX_PANS-1 живут на
|
// Пан 0 = главный тракт (RXA/RX_DISP_ID), доп. паны 1..MAX_PANS-1 живут на
|
||||||
// своих DDC. Единый потолок для движка/контроллера/UI.
|
// своих DDC. Единый потолок для движка/контроллера/UI.
|
||||||
MAX_PANS = 4;
|
MAX_PANS = 4;
|
||||||
// Analyzer id пана N = PAN_DISP_BASE + N (9..11). ID 4/5 заняты веткой
|
// Analyzer id пана N = PAN_DISP_BASE + N (9..11). ID 4 занят лупой маяка
|
||||||
// feature/qo100-zoom-panel — не пересекаемся при будущем merge.
|
// (BCN_DISP_ID), 5 держим свободным про запас.
|
||||||
PAN_DISP_BASE = 8;
|
PAN_DISP_BASE = 8;
|
||||||
|
|
||||||
DSP_BUFSIZE = 1024;
|
DSP_BUFSIZE = 1024;
|
||||||
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
|
SPECTRUM_PIXELS = 4096; // потолок точек дисплея; синхронно с WaterfallView.WF_MAX_PIXELS
|
||||||
|
// Точек в кадре лупы маяка. Фиксировано (а не по ширине виджета), чтобы
|
||||||
|
// изменение размера окна маяка не переармировало analyzer: переарм сбрасывает
|
||||||
|
// FFT-историю, а на крупных FFT она наполняется почти секунду. Рендер сам
|
||||||
|
// ужимает кадр под свою ширину.
|
||||||
|
BCN_ZOOM_PIXELS = 1024;
|
||||||
|
// Частота кадров лупы. Своя, ниже главного дисплея — см.
|
||||||
|
// ApplyBeaconAnalyzerSettings.
|
||||||
|
BCN_ZOOM_FPS = 15.0;
|
||||||
DISPLAY_BLOCK_SIZE = 64;
|
DISPLAY_BLOCK_SIZE = 64;
|
||||||
|
|
||||||
// Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны).
|
// Очередь IQ пакетов между сетевым и DSP потоком (общая: главный + паны).
|
||||||
@@ -336,6 +348,27 @@ type
|
|||||||
FViewLowHz: Double;
|
FViewLowHz: Double;
|
||||||
FViewHighHz: 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;
|
FOnAudio: TOnAudioReady;
|
||||||
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
|
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
|
||||||
FOnSpectrum: TOnSpectrumReady;
|
FOnSpectrum: TOnSpectrumReady;
|
||||||
@@ -572,6 +605,12 @@ type
|
|||||||
procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc
|
procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc
|
||||||
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
||||||
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_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
|
public
|
||||||
constructor Create(SampleRate: Integer = 192000;
|
constructor Create(SampleRate: Integer = 192000;
|
||||||
@@ -771,6 +810,23 @@ type
|
|||||||
property ViewLowHz: Double read FViewLowHz;
|
property ViewLowHz: Double read FViewLowHz;
|
||||||
property ViewHighHz: Double read FViewHighHz;
|
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 Initialized: Boolean read FInitialized;
|
||||||
property SampleRate: Integer read FSampleRate;
|
property SampleRate: Integer read FSampleRate;
|
||||||
property TXSampleRate: Integer read FTXSampleRate;
|
property TXSampleRate: Integer read FTXSampleRate;
|
||||||
@@ -1427,6 +1483,16 @@ begin
|
|||||||
FMainGated := False;
|
FMainGated := False;
|
||||||
FMainLock := TCriticalSection.Create;
|
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;
|
FSliceLock := TCriticalSection.Create;
|
||||||
FillChar(FSlices, SizeOf(FSlices), 0);
|
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||||
@@ -1526,6 +1592,7 @@ begin
|
|||||||
RTLEventDestroy(FTXMicSem);
|
RTLEventDestroy(FTXMicSem);
|
||||||
FSliceLock.Free;
|
FSliceLock.Free;
|
||||||
FMainLock.Free;
|
FMainLock.Free;
|
||||||
|
FBcnLock.Free;
|
||||||
inherited;
|
inherited;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1757,12 +1824,241 @@ begin
|
|||||||
end;
|
end;
|
||||||
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;
|
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||||
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам.
|
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у, и к панам.
|
||||||
begin
|
begin
|
||||||
ApplyRXAnalyzerSettings;
|
ApplyRXAnalyzerSettings;
|
||||||
ApplyTXAnalyzerSettings;
|
ApplyTXAnalyzerSettings;
|
||||||
ReapplyPanDisplaySettings;
|
ReapplyPanDisplaySettings;
|
||||||
|
ReapplyBeaconDisplayModes;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
|
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
|
||||||
@@ -1796,6 +2092,9 @@ begin
|
|||||||
end;
|
end;
|
||||||
DestroyAnalyzer(RX_DISP_ID);
|
DestroyAnalyzer(RX_DISP_ID);
|
||||||
DestroyAnalyzer(TX_DISP_ID);
|
DestroyAnalyzer(TX_DISP_ID);
|
||||||
|
// Лупа маяка: не пересоздаётся в OpenAnalyzer — окно маяка запросит её сама
|
||||||
|
// (SetBeaconZoom зовётся с таймера формы).
|
||||||
|
CloseBeaconAnalyzer;
|
||||||
// Анализаторы доп. панов (записи FPans сохраняются — пересоздание в OpenAnalyzer).
|
// Анализаторы доп. панов (записи FPans сохраняются — пересоздание в OpenAnalyzer).
|
||||||
FSliceLock.Enter;
|
FSliceLock.Enter;
|
||||||
try
|
try
|
||||||
@@ -2085,6 +2384,8 @@ begin
|
|||||||
// главного дисплея сбрасывается (при смене rate это неизбежно), паны
|
// главного дисплея сбрасывается (при смене rate это неизбежно), паны
|
||||||
// не затронуты.
|
// не затронуты.
|
||||||
if FAnalyzerOpen then ApplyRXAnalyzerSettings;
|
if FAnalyzerOpen then ApplyRXAnalyzerSettings;
|
||||||
|
// Лупа маяка тоже считает окно и FFT от rate — переармируем.
|
||||||
|
RearmBeaconAnalyzer;
|
||||||
|
|
||||||
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
|
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
|
||||||
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
|
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
|
||||||
@@ -2310,6 +2611,26 @@ begin
|
|||||||
end;
|
end;
|
||||||
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);
|
if Bec <> nil then Bec.Feed(DI, DQ);
|
||||||
|
|
||||||
FRXAccI[FRXAccPos] := DI;
|
FRXAccI[FRXAccPos] := DI;
|
||||||
@@ -4058,6 +4379,7 @@ var
|
|||||||
i, N, p: Integer;
|
i, N, p: Integer;
|
||||||
DispID: Integer;
|
DispID: Integer;
|
||||||
Cal: Single;
|
Cal: Single;
|
||||||
|
MainCal: Single; // калибровка главного тракта (Cal ниже перетирается панами)
|
||||||
GotSpec, GotWf: Boolean;
|
GotSpec, GotWf: Boolean;
|
||||||
begin
|
begin
|
||||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||||
@@ -4068,6 +4390,7 @@ begin
|
|||||||
// На TX-дисплее коррекция входного тракта не при чём.
|
// На TX-дисплее коррекция входного тракта не при чём.
|
||||||
if Assigned(FOnDispCal) and (DispID = RX_DISP_ID) then Cal := FOnDispCal(0)
|
if Assigned(FOnDispCal) and (DispID = RX_DISP_ID) then Cal := FOnDispCal(0)
|
||||||
else Cal := 0;
|
else Cal := 0;
|
||||||
|
MainCal := Cal;
|
||||||
|
|
||||||
Flag := 0;
|
Flag := 0;
|
||||||
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||||||
@@ -4128,6 +4451,37 @@ begin
|
|||||||
if GotWf and Assigned(FOnPanWaterfall) then
|
if GotWf and Assigned(FOnPanWaterfall) then
|
||||||
FOnPanWaterfall(p, WfBuf, N);
|
FOnPanWaterfall(p, WfBuf, N);
|
||||||
end;
|
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;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
||||||
|
|||||||
Reference in New Issue
Block a user