mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(fm): add universal FM RAW decoder output
This commit is contained in:
+165
-5
@@ -21,7 +21,7 @@ uses
|
||||
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
|
||||
AppTheme,
|
||||
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
|
||||
WaterfallView, SMeterView, RulerView;
|
||||
WaterfallView, SMeterView, RulerView, RadioModes;
|
||||
|
||||
const
|
||||
SV_CLR_BG = TColor($00101010);
|
||||
@@ -36,6 +36,16 @@ type
|
||||
LastUse: Int64;
|
||||
end;
|
||||
|
||||
// Маркер 2TON/IMD-измерения на спектре: кружок на пике продукта + подпись.
|
||||
// Тона подписываются уровнем шкалы (dB), IMD-продукты — в dBc от среднего
|
||||
// уровня тонов.
|
||||
TIMDMarker = record
|
||||
FreqHz: Double; // абсолютная частота продукта
|
||||
LevelDB: Double; // измеренный пик (шкала дисплея)
|
||||
Caption: string;
|
||||
IsTone: Boolean; // True = основной тон, False = IMD-продукт
|
||||
end;
|
||||
|
||||
TSpectrumView = class
|
||||
protected
|
||||
// ── Off-screen bitmaps ────────────────────────────────────────────────────
|
||||
@@ -99,6 +109,11 @@ type
|
||||
FBeaconDecActive: Boolean;
|
||||
FBeaconDecFreq: Double;
|
||||
FBeaconDecHalf: Double; // полуширина фильтра, Гц
|
||||
// ── 2TON/IMD-маркеры (пики тонов и продуктов IMD3 при двухтональнике) ────
|
||||
FIMDActive: Boolean;
|
||||
FIMDTone1Hz: Double; // абс. частоты тонов (TX freq + аудио-оффсет)
|
||||
FIMDTone2Hz: Double;
|
||||
FIMDSummary: string; // сводка «IMD3 −NN dBc» (обновляет ComputeIMDMarkers)
|
||||
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
||||
FSpectrumBuf: array[0..WF_MAX_PIXELS-1] of Single;
|
||||
FSpectrumBufCount: Integer;
|
||||
@@ -149,6 +164,12 @@ type
|
||||
procedure DrawSliceFilterLinesRaw(W, H: Integer);
|
||||
procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor);
|
||||
procedure DrawBeaconMarkersRaw(W, H: Integer);
|
||||
// 2TON/IMD: замер пиков по TX-буферу (общий для CPU/GL рендеров).
|
||||
// Возвращает число валидных маркеров в M (0 если измерять нечего):
|
||||
// [0..1] тона, [2..3] IMD3 (2f1−f2, 2f2−f1). Обновляет FIMDSummary.
|
||||
function ComputeIMDMarkers(var M: array of TIMDMarker): Integer;
|
||||
procedure DrawIMDMarkersRaw(W, H: Integer; DBmax, InvRange: Double);
|
||||
procedure RawCircle(CX, CY, R: Integer; Color: TColor);
|
||||
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
procedure FillBandRaw(X1, X2, H: Integer; Color: TColor);
|
||||
procedure CopyGridToSpectrum(W, H: Integer);
|
||||
@@ -244,6 +265,12 @@ type
|
||||
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
||||
procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double);
|
||||
|
||||
// 2TON/IMD-маркеры: кружки с уровнями на пиках тонов и продуктов IMD3
|
||||
// (замер линейности/PureSignal). Включает MainForm при 2TON+TX+DUP:
|
||||
// дисплей на RX показывает СВОЙ сигнал после PA — настоящие IMD-плечи
|
||||
// (на цифровом TX-спектре продуктов PA нет, там замер бессмыслен).
|
||||
procedure SetIMDMarkers(Active: Boolean; Tone1Hz, Tone2Hz: Double);
|
||||
|
||||
// ── S-метр ────────────────────────────────────────────────────────────────
|
||||
property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
|
||||
property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak;
|
||||
@@ -653,6 +680,136 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.SetIMDMarkers(Active: Boolean; Tone1Hz, Tone2Hz: Double);
|
||||
begin
|
||||
FIMDActive := Active;
|
||||
FIMDTone1Hz := Tone1Hz;
|
||||
FIMDTone2Hz := Tone2Hz;
|
||||
if not Active then FIMDSummary := '';
|
||||
end;
|
||||
|
||||
function TSpectrumView.ComputeIMDMarkers(var M: array of TIMDMarker): Integer;
|
||||
// Пики меряются по FSpectrumBuf. Основной сценарий — DUP: дисплей на RX
|
||||
// (TXMode=False), буфер покрывает видимое окно FCenterFreq ± FSpanHz/2, и мы
|
||||
// видим СВОЙ сигнал после PA (реальные IMD-плечи). При TXMode буфер покрывает
|
||||
// FTXFreq ± FTXSpanHz/2 (цифровой TX-тракт — оставлено для общности).
|
||||
// Окно поиска ±250 Гц вокруг ожидаемой частоты — ловим пик независимо от
|
||||
// джиттера FFT.
|
||||
var
|
||||
SrcCount, i: Integer;
|
||||
ToneAvg, MapLo, MapSpan: Double;
|
||||
|
||||
function PeakDB(FreqHz: Double): Double;
|
||||
var C, Win, k, i1, i2: Integer;
|
||||
begin
|
||||
Result := -999.0;
|
||||
C := Round((FreqHz - MapLo) / MapSpan * (SrcCount - 1));
|
||||
// Ожидаемая частота вне окна буфера (зум/узкий span) — маркер невалиден,
|
||||
// иначе намеряли бы уровень краевого бина.
|
||||
if (C < 0) or (C > SrcCount - 1) then Exit;
|
||||
Win := Max(2, Round(250.0 * (SrcCount - 1) / MapSpan));
|
||||
i1 := Max(0, C - Win);
|
||||
i2 := Min(SrcCount - 1, C + Win);
|
||||
for k := i1 to i2 do
|
||||
if FSpectrumBuf[k] > Result then Result := FSpectrumBuf[k];
|
||||
end;
|
||||
|
||||
begin
|
||||
Result := 0;
|
||||
FIMDSummary := '';
|
||||
if not FIMDActive then Exit;
|
||||
if (FSpectrumBufCount < 2) or (Length(M) < 4) then Exit;
|
||||
if FTXMode then
|
||||
begin
|
||||
if FTXSpanHz <= 0 then Exit;
|
||||
MapLo := FTXFreq - FTXSpanHz * 0.5;
|
||||
MapSpan := FTXSpanHz;
|
||||
end
|
||||
else
|
||||
begin
|
||||
if FSpanHz <= 0 then Exit;
|
||||
MapLo := FCenterFreq - FSpanHz * 0.5;
|
||||
MapSpan := FSpanHz;
|
||||
end;
|
||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, WF_MAX_PIXELS);
|
||||
|
||||
M[0].FreqHz := FIMDTone1Hz; M[0].IsTone := True;
|
||||
M[1].FreqHz := FIMDTone2Hz; M[1].IsTone := True;
|
||||
M[2].FreqHz := 2 * FIMDTone1Hz - FIMDTone2Hz; M[2].IsTone := False;
|
||||
M[3].FreqHz := 2 * FIMDTone2Hz - FIMDTone1Hz; M[3].IsTone := False;
|
||||
for i := 0 to 3 do
|
||||
M[i].LevelDB := PeakDB(M[i].FreqHz);
|
||||
|
||||
ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5;
|
||||
M[0].Caption := Format('%.0f', [M[0].LevelDB]);
|
||||
M[1].Caption := Format('%.0f', [M[1].LevelDB]);
|
||||
// IMD-продукты — в dBc относительно среднего уровня тонов
|
||||
M[2].Caption := Format('%.0fc', [M[2].LevelDB - ToneAvg]);
|
||||
M[3].Caption := Format('%.0fc', [M[3].LevelDB - ToneAvg]);
|
||||
// Сводка только когда оба тона и хотя бы один продукт в окне буфера
|
||||
if (M[0].LevelDB > -500) and (M[1].LevelDB > -500) and
|
||||
(Max(M[2].LevelDB, M[3].LevelDB) > -500) then
|
||||
FIMDSummary := Format('IMD3 %.0f dBc',
|
||||
[Max(M[2].LevelDB, M[3].LevelDB) - ToneAvg]);
|
||||
Result := 4;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.RawCircle(CX, CY, R: Integer; Color: TColor);
|
||||
// Залитый кружок радиуса R: по строкам, ширина из уравнения окружности.
|
||||
var
|
||||
DY, HalfW, Y, X1, X2, K: Integer;
|
||||
Px: LongWord;
|
||||
P: PLongWord;
|
||||
begin
|
||||
if FRawBase = nil then Exit;
|
||||
Px := RawPack(Color);
|
||||
for DY := -R to R do
|
||||
begin
|
||||
Y := CY + DY;
|
||||
if (Y < 0) or (Y >= FRawH) then Continue;
|
||||
HalfW := Round(Sqrt(R * R - DY * DY));
|
||||
X1 := Max(0, CX - HalfW);
|
||||
X2 := Min(FRawW - 1, CX + HalfW);
|
||||
if X2 < X1 then Continue;
|
||||
P := PLongWord(FRawBase + Y * FRawBPL + X1 * 4);
|
||||
for K := X1 to X2 do begin P^ := Px; Inc(P); end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawIMDMarkersRaw(W, H: Integer; DBmax, InvRange: Double);
|
||||
const
|
||||
CLR_TONE = TColor($0050FF50); // зелёный — основные тона
|
||||
CLR_IMD = TColor($000A8CFF); // оранжевый — продукты IMD3
|
||||
var
|
||||
M: array[0..3] of TIMDMarker;
|
||||
N, i, X, Y, TY: Integer;
|
||||
Col: TColor;
|
||||
begin
|
||||
if FSpanHz <= 0 then Exit;
|
||||
N := ComputeIMDMarkers(M);
|
||||
if N = 0 then Exit;
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
if M[i].LevelDB < -500 then Continue; // частота вне окна буфера
|
||||
X := Round((M[i].FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
if (X < 0) or (X >= W) then Continue;
|
||||
Y := Max(0, Min(H - 1, Round((DBmax - M[i].LevelDB) * InvRange * H)));
|
||||
if M[i].IsTone then Col := CLR_TONE else Col := CLR_IMD;
|
||||
RawCircle(X, Y, 4, Col);
|
||||
// Подпись правее-выше кружка; у верхней кромки — вниз, у правой — влево.
|
||||
TY := Y - 14;
|
||||
if TY < 2 then TY := Y + 6;
|
||||
if X + 6 + RawTextWidth(M[i].Caption, 7, True) < W then
|
||||
RawText(X + 6, TY, M[i].Caption, Col, 7, True, clNone)
|
||||
else
|
||||
RawText(X - 6 - RawTextWidth(M[i].Caption, 7, True), TY,
|
||||
M[i].Caption, Col, 7, True, clNone);
|
||||
end;
|
||||
// Сводка в левом верхнем углу
|
||||
if FIMDSummary <> '' then
|
||||
RawText(6, 6, FIMDSummary, CLR_IMD, 9, True, clNone);
|
||||
end;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Full-raw примитивы кадра (валидны только между RawBegin/RawEnd)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1098,10 +1255,10 @@ begin
|
||||
Half := BW / 2;
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
case Mode of
|
||||
0: begin Lo_Hz := -BW; Hi_Hz := -100; end; // LSB
|
||||
1: begin Lo_Hz := 100; Hi_Hz := BW; end; // USB
|
||||
9: begin Lo_Hz := -BW; Hi_Hz := 0; end; // DIGL — полоса от нуля
|
||||
8: begin Lo_Hz := 0; Hi_Hz := BW; end; // DIGU
|
||||
MODE_LSB: begin Lo_Hz := -BW; Hi_Hz := -100; end;
|
||||
MODE_USB: begin Lo_Hz := 100; Hi_Hz := BW; end;
|
||||
MODE_DIGL: begin Lo_Hz := -BW; Hi_Hz := 0; end; // полоса от нуля
|
||||
MODE_DIGU: begin Lo_Hz := 0; Hi_Hz := BW; end;
|
||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||
end;
|
||||
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||
@@ -1546,6 +1703,9 @@ begin
|
||||
|
||||
if FMarkerActive then DrawMarkerLineRaw(W, H);
|
||||
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(W, H);
|
||||
// 2TON: кружки с уровнями на пиках тонов/IMD3 (активирует MainForm при
|
||||
// 2TON+TX+DUP — реальный сигнал после PA виден на RX-спектре)
|
||||
if FIMDActive then DrawIMDMarkersRaw(W, H, DBmax, InvRange);
|
||||
|
||||
|
||||
// Композиты оверлеев — внутри внешнего лока: их собственные BeginUpdate/
|
||||
|
||||
Reference in New Issue
Block a user