mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
TX: фиксированный sample rate, per-device grid, CTUN-aware overlay, alpha-blend filter bands
Декаплинг TX от RX sample rate (как в Thetis):
- WDSPEngine.pas: константа TX_SAMPLE_RATE=192000, поля FTXSampleRate/FTXOutBufSize.
TXA OpenChannel теперь использует FTXSampleRate как out_rate, TX-анализатор
через SetDisplaySampleRate(TX_DISP_ID, FTXSampleRate). ProcessTXBlock работает
с FTXOutBufSize вместо FBufSize. Public TXSampleRate для MainForm.
- MainForm.SendDUCSpecificFromSettings: DUC0Rate теперь берётся из
FDSPEngine.TXSampleRate, а не из RX-настроек — TX всегда 192k независимо от
выбранного RX-rate (48/96/192/384k).
DUC Specific cache + keepalive resend (HPSDRNetwork.pas):
- FCachedDUCSpec/FCachedDUCValid параллельно DDC-кэшу.
- KeepAlive thread в первые 5s после Run=1 переотправляет и DDC, и DUC
Specific — устраняет гонку, когда DUC настройки терялись железом.
- Disconnect сбрасывает оба valid-флага.
Per-device TX grid settings (Thetis-style):
- Settings.pas: TXSpecRefLevel/TXSpecRange/TXSpecGridStep в TTXSettings
(defaults 0.0/80.0/10.0). LoadTX/SaveTX — ключи tx_spec_ref_level/range/grid_step.
- SettingsForm.pas: новая группа \"TX Grid\" во вкладке Transmit
(RefLevel edit, Range edit, GridStep combo). CollectTXFromUI читает значения,
LoadTXSettings заполняет UI.
- MainForm.pas: поля FTXSpecRefLevel/Range/GridStep, ApplySpecViewGridFromState
пушит в FSpecView либо TX, либо RX параметры в зависимости от FTransmitting.
ApplyGridParams (RX-путь) уважает FTransmitting и не перебивает TX-grid.
OnTXSettingsChange при активном TX моментально применяет новый grid.
TX-aware spectrum/waterfall с CTUN-выравниванием (SpectrumView.pas):
- Поля FTXMode/FTXFreq/FTXSpanHz/FTXVfoIndex. TXSpanHz=192000 при создании.
- DrawSpectrum/DrawWaterfall в FTXMode мапят данные TX-анализатора (192k вокруг
FTXFreq) в RX-координаты FCenterFreq/FSpanHz. Формула:
freq = FCenterFreq - FSpanHz/2 + x*FSpanHz/W
idx = (freq-FTXFreq + TXSpanHz/2)/TXSpanHz * (SrcCount-1)
Вне диапазона — спектр уходит в -200 dB; водопад копирует пиксель из
предыдущей строки (FWfPixels[X+W]) — вместо чёрной полосы продолжает
скроллиться старая RX-картинка.
- Эффект: при CTUN ON TX-сигнал рисуется на полосе фильтра (на VFO), а не в
центре. При RX rate ≠ 192k частотная сетка совпадает между RX и TX
автоматически (ruler всегда в RX-координатах).
- MainForm.SyncSpecViewFreq/ApplyMOX обновляют TXFreq/TXVfoIndex/TXMode при
каждом PTT и смене VFO; учитывается split (FSplitTxB → TXVfoIndex=1).
Alpha-blended filter bands (визуальный feedback TX/RX):
- SpectrumView: PixelFormat:=pf32bit, процедура BlendBand через ScanLine[Y]
внутри BeginUpdate/EndUpdate. Альфа-смешение по формуле
(a*src + (255-a)*dst) / 255 для каждого канала BGR.
- В FTXMode полоса фильтра становится полупрозрачно красной. Если split
(FTXVfoIndex<>FActiveVfo): на RX-частоте полоса зелёная, на TX-частоте —
красная (CalcFilterBandX вычисляет границы для произвольной VFO-freq).
- При FTXMode=False — старое поведение (сплошная заливка SpecFilter).
Build: lazbuild --ws=qt6 ewsdr.lpi — clean, 0 errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+21
-8
@@ -121,9 +121,13 @@ type
|
|||||||
FPortDUCIQ: Word;
|
FPortDUCIQ: Word;
|
||||||
FDirectIP: string; // для unicast discovery
|
FDirectIP: string; // для unicast discovery
|
||||||
|
|
||||||
// Кэш DDC Specific — keepalive повторно шлёт первые 5 сек после старта
|
// Кэш DDC/DUC Specific — keepalive повторно шлёт первые 5 сек после старта.
|
||||||
|
// duc_specific_thread эмулятора биндится на 1026 только после HP Run=1,
|
||||||
|
// поэтому первый DUC Specific может прийти раньше bind() и быть потерян.
|
||||||
FCachedDDCSpec: TDDCSpecificPacket;
|
FCachedDDCSpec: TDDCSpecificPacket;
|
||||||
FCachedDDCValid: Boolean;
|
FCachedDDCValid: Boolean;
|
||||||
|
FCachedDUCSpec: TDUCSpecificPacket;
|
||||||
|
FCachedDUCValid: Boolean;
|
||||||
FResendCount: Integer;
|
FResendCount: Integer;
|
||||||
|
|
||||||
// Текущее состояние для построения HP пакетов
|
// Текущее состояние для построения HP пакетов
|
||||||
@@ -403,14 +407,18 @@ begin
|
|||||||
if not FNet.FConnected then Continue;
|
if not FNet.FConnected then Continue;
|
||||||
// Отправляем полный HP с частотами и ALEX — как piHPSDR
|
// Отправляем полный HP с частотами и ALEX — как piHPSDR
|
||||||
FNet.SendFullHP;
|
FNet.SendFullHP;
|
||||||
// Повторно отправляем DDC Specific первые 5 сек после Run=1 (каждые 500 мс).
|
// Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс).
|
||||||
// ddc_specific_thread на эмуляторе/железе стартует ПОСЛЕ получения Run=1,
|
// ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют
|
||||||
// поэтому первый ConfigureDDCs может прийти раньше чем порт 1025 откроется.
|
// ПОСЛЕ получения Run=1, поэтому первые ConfigureDDCs / SendDUCSpecific
|
||||||
if FNet.FRunning and FNet.FCachedDDCValid then
|
// могут прийти раньше чем порты 1025/1026 откроются.
|
||||||
|
if FNet.FRunning then
|
||||||
begin
|
begin
|
||||||
Inc(FNet.FResendCount);
|
Inc(FNet.FResendCount);
|
||||||
if (FNet.FResendCount <= 100) and (FNet.FResendCount mod 10 = 2) then
|
if (FNet.FResendCount <= 100) and (FNet.FResendCount mod 10 = 2) then
|
||||||
FNet.SendDDCSpecific(FNet.FCachedDDCSpec);
|
begin
|
||||||
|
if FNet.FCachedDDCValid then FNet.SendDDCSpecific(FNet.FCachedDDCSpec);
|
||||||
|
if FNet.FCachedDUCValid then FNet.SendDUCSpecific(FNet.FCachedDUCSpec);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -715,8 +723,10 @@ begin
|
|||||||
FRunning := False;
|
FRunning := False;
|
||||||
StopThreads;
|
StopThreads;
|
||||||
DoCloseSocket(FSocket);
|
DoCloseSocket(FSocket);
|
||||||
FConnected := False;
|
FConnected := False;
|
||||||
FDevice.Valid := False;
|
FDevice.Valid := False;
|
||||||
|
FCachedDDCValid := False;
|
||||||
|
FCachedDUCValid := False;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure THPSDRNetwork.StartThreads;
|
procedure THPSDRNetwork.StartThreads;
|
||||||
@@ -940,6 +950,9 @@ begin
|
|||||||
B := Pkt;
|
B := Pkt;
|
||||||
PackSeqBytes(B.Seq, NextSeq(FSeqDUCSpec));
|
PackSeqBytes(B.Seq, NextSeq(FSeqDUCSpec));
|
||||||
DoSendTo(FSocket, B, SizeOf(B), FDevice.IPAddress, FPortDUCSpec);
|
DoSendTo(FSocket, B, SizeOf(B), FDevice.IPAddress, FPortDUCSpec);
|
||||||
|
// Кэш для пере-отправки в keepalive (см. ResendThread)
|
||||||
|
FCachedDUCSpec := Pkt;
|
||||||
|
FCachedDUCValid := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure THPSDRNetwork.SendHighPriority(const Pkt: THighPriorityPacket);
|
procedure THPSDRNetwork.SendHighPriority(const Pkt: THighPriorityPacket);
|
||||||
|
|||||||
+87
-16
@@ -217,6 +217,11 @@ type
|
|||||||
FSpecRefLevel: Double;
|
FSpecRefLevel: Double;
|
||||||
FSpecRange: Double;
|
FSpecRange: Double;
|
||||||
FSpecGridStep: Double;
|
FSpecGridStep: Double;
|
||||||
|
// TX-specific grid (Thetis-style: при TX рисуем спектр в своих границах).
|
||||||
|
// Применяются к FSpecView когда FTransmitting=True; иначе используются RX-значения.
|
||||||
|
FTXSpecRefLevel: Double;
|
||||||
|
FTXSpecRange: Double;
|
||||||
|
FTXSpecGridStep: Double;
|
||||||
// DSP data buffers (copy of last DSP output, for WebServer.PushSpectrum)
|
// DSP data buffers (copy of last DSP output, for WebServer.PushSpectrum)
|
||||||
FSpectrumBuf: array[0..1023] of Single;
|
FSpectrumBuf: array[0..1023] of Single;
|
||||||
FWaterfallBuf: array[0..1023] of Single;
|
FWaterfallBuf: array[0..1023] of Single;
|
||||||
@@ -559,6 +564,10 @@ type
|
|||||||
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
|
||||||
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
|
||||||
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||||
|
// Пушит активные grid-параметры (RX или TX в зависимости от FTransmitting)
|
||||||
|
// в FSpecView и сбрасывает кэш сетки. Вызывается при смене RX↔TX и при
|
||||||
|
// правке любого из соответствующих полей (FSpec*, FTXSpec*).
|
||||||
|
procedure ApplySpecViewGridFromState;
|
||||||
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||||||
// TX settings: применить FTXSettings к WDSP и переслать DUC Specific
|
// TX settings: применить FTXSettings к WDSP и переслать DUC Specific
|
||||||
@@ -999,6 +1008,10 @@ begin
|
|||||||
FSpecRefLevel := -20.0;
|
FSpecRefLevel := -20.0;
|
||||||
FSpecRange := 110.0;
|
FSpecRange := 110.0;
|
||||||
FSpecGridStep := 10.0;
|
FSpecGridStep := 10.0;
|
||||||
|
// TX grid defaults (синхронно с TSettingsManager.DefaultTX)
|
||||||
|
FTXSpecRefLevel := 0.0;
|
||||||
|
FTXSpecRange := 80.0;
|
||||||
|
FTXSpecGridStep := 10.0;
|
||||||
FSettingsForm := nil;
|
FSettingsForm := nil;
|
||||||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||||||
|
|
||||||
@@ -1108,6 +1121,8 @@ begin
|
|||||||
FSpecView.SpecRefLevel := FSpecRefLevel;
|
FSpecView.SpecRefLevel := FSpecRefLevel;
|
||||||
FSpecView.SpecRange := FSpecRange;
|
FSpecView.SpecRange := FSpecRange;
|
||||||
FSpecView.SpecGridStep := FSpecGridStep;
|
FSpecView.SpecGridStep := FSpecGridStep;
|
||||||
|
// TX overlay: ширина окна = TX_SAMPLE_RATE из движка (фикс 192k)
|
||||||
|
FSpecView.TXSpanHz := WDSPEngine.TX_SAMPLE_RATE;
|
||||||
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
|
||||||
|
|
||||||
FNetwork := THPSDRNetwork.Create;
|
FNetwork := THPSDRNetwork.Create;
|
||||||
@@ -2487,6 +2502,12 @@ begin
|
|||||||
FSpecView.SpanHz := FSpanHz;
|
FSpecView.SpanHz := FSpanHz;
|
||||||
FSpecView.Mode := FMode;
|
FSpecView.Mode := FMode;
|
||||||
FSpecView.FilterBW := FFilterBW;
|
FSpecView.FilterBW := FFilterBW;
|
||||||
|
// TX overlay следует за активной TX-частотой (учитывает split). При CTUN
|
||||||
|
// спектр TX будет отрисован на VFO, а не в центре дисплея.
|
||||||
|
FSpecView.TXFreq := ActiveTXFreqHz;
|
||||||
|
// Какой VFO привязан к TX (для split-полос фильтра): 1=B при FSplitTxB, иначе FActiveVfo.
|
||||||
|
if FSplitTxB then FSpecView.TXVfoIndex := 1
|
||||||
|
else FSpecView.TXVfoIndex := FActiveVfo;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
@@ -2855,34 +2876,38 @@ end;
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function TMainForm.BuildMicLineSelectByte: Byte;
|
function TMainForm.BuildMicLineSelectByte: Byte;
|
||||||
// HPSDR Protocol-2, DUC Specific byte 50:
|
// openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50:
|
||||||
// bit 0 — mic boost (+20 dB)
|
// bit 0 — Line in (1 = line-in вместо mic-in)
|
||||||
// bit 1 — Line in вместо mic in
|
// bit 1 — Mic Boost (+20 dB)
|
||||||
// bit 2 — Mic-jack PTT enable (tip)
|
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
|
||||||
// bit 4 — Mic bias on (электретный капсюль)
|
// bit 3 — PTT/bias polarity (не используем)
|
||||||
// Биты 3 (PTT tip-bias-ring) и 5..7 не используются.
|
// bit 4 — Orion mic bias (1 = enable +5V bias)
|
||||||
|
// bit 5 — Saturn 3.5mm/XLR (не используем)
|
||||||
begin
|
begin
|
||||||
Result := 0;
|
Result := 0;
|
||||||
if FTXSettings.MicBoost then Result := Result or $01;
|
if FTXSettings.MicLineIn then Result := Result or $01;
|
||||||
if FTXSettings.MicLineIn then Result := Result or $02;
|
if FTXSettings.MicBoost then Result := Result or $02;
|
||||||
if FTXSettings.MicPTTEnabled then Result := Result or $04;
|
if not FTXSettings.MicPTTEnabled then Result := Result or $04;
|
||||||
if FTXSettings.MicBias then Result := Result or $10;
|
if FTXSettings.MicBias then Result := Result or $10;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.SendDUCSpecificFromSettings;
|
procedure TMainForm.SendDUCSpecificFromSettings;
|
||||||
var
|
var
|
||||||
DUCPkt: TDUCSpecificPacket;
|
DUCPkt: TDUCSpecificPacket;
|
||||||
|
RateKsps: Word;
|
||||||
begin
|
begin
|
||||||
if not (FNetwork.Connected and FNetwork.Running) then Exit;
|
if not (FNetwork.Connected and FNetwork.Running) then Exit;
|
||||||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||||||
|
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
|
||||||
|
RateKsps := FDSPEngine.TXSampleRate div 1000;
|
||||||
DUCPkt.NumDACs := 1;
|
DUCPkt.NumDACs := 1;
|
||||||
DUCPkt.SidetoneLevel := 50;
|
DUCPkt.SidetoneLevel := 50;
|
||||||
DUCPkt.SidetoneFreqHi := Hi(600);
|
DUCPkt.SidetoneFreqHi := Hi(600);
|
||||||
DUCPkt.SidetoneFreqLo := Lo(600);
|
DUCPkt.SidetoneFreqLo := Lo(600);
|
||||||
DUCPkt.KeyerSpeed := 20;
|
DUCPkt.KeyerSpeed := 20;
|
||||||
DUCPkt.KeyerWeight := 50;
|
DUCPkt.KeyerWeight := 50;
|
||||||
DUCPkt.DUC0RateHi := Hi(192);
|
DUCPkt.DUC0RateHi := Hi(RateKsps);
|
||||||
DUCPkt.DUC0RateLo := Lo(192);
|
DUCPkt.DUC0RateLo := Lo(RateKsps);
|
||||||
DUCPkt.DUC0Bits := 24;
|
DUCPkt.DUC0Bits := 24;
|
||||||
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
||||||
FNetwork.SendDUCSpecific(DUCPkt);
|
FNetwork.SendDUCSpecific(DUCPkt);
|
||||||
@@ -3290,6 +3315,10 @@ begin
|
|||||||
CATApplySettings(G_Settings);
|
CATApplySettings(G_Settings);
|
||||||
// TX-настройки per-device — отдельная JSON-секция "tx"
|
// TX-настройки per-device — отдельная JSON-секция "tx"
|
||||||
FSettings.LoadTX(FDevMAC, FTXSettings);
|
FSettings.LoadTX(FDevMAC, FTXSettings);
|
||||||
|
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
|
||||||
|
FTXSpecRange := FTXSettings.TXSpecRange;
|
||||||
|
if FTXSettings.TXSpecGridStep > 0 then
|
||||||
|
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
|
||||||
FVolume := G_Settings.Volume;
|
FVolume := G_Settings.Volume;
|
||||||
FActiveVfo := G_Settings.ActiveVfo;
|
FActiveVfo := G_Settings.ActiveVfo;
|
||||||
// PA settings
|
// PA settings
|
||||||
@@ -3431,6 +3460,7 @@ begin
|
|||||||
SendDUCSpecificFromSettings;
|
SendDUCSpecificFromSettings;
|
||||||
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||||||
ApplyTXSettingsToDSP;
|
ApplyTXSettingsToDSP;
|
||||||
|
ApplyModeFilter;
|
||||||
|
|
||||||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||||||
if FWDSPReady then
|
if FWDSPReady then
|
||||||
@@ -4096,8 +4126,10 @@ begin
|
|||||||
7: begin Lo := -Half; Hi := Half; end; // SAM
|
7: begin Lo := -Half; Hi := Half; end; // SAM
|
||||||
else Lo := -Half; Hi := Half;
|
else Lo := -Half; Hi := Half;
|
||||||
end;
|
end;
|
||||||
if FWDSPReady then
|
if FWDSPReady then begin
|
||||||
FDSPEngine.SetFilter(Lo, Hi);
|
FDSPEngine.SetFilter(Lo, Hi);
|
||||||
|
FDSPEngine.SetTXFilter(Lo, Hi);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BtnModeClick(Sender: TObject);
|
procedure TMainForm.BtnModeClick(Sender: TObject);
|
||||||
@@ -4111,6 +4143,7 @@ begin
|
|||||||
FDSPEngine.SetMode(N);
|
FDSPEngine.SetMode(N);
|
||||||
UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим
|
UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
|
SyncSpecViewFreq; // FSpecView.Mode/FilterBW — иначе полоса рисуется под старый режим
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||||||
@@ -4128,6 +4161,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter);
|
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter);
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
|
SyncSpecViewFreq;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BtnVfoSwapClick(Sender: TObject);
|
procedure TMainForm.BtnVfoSwapClick(Sender: TObject);
|
||||||
@@ -4223,6 +4257,13 @@ begin
|
|||||||
BtnMOX.Active := True;
|
BtnMOX.Active := True;
|
||||||
end else
|
end else
|
||||||
StyleButton(BtnMOX, False);
|
StyleButton(BtnMOX, False);
|
||||||
|
// TX-overlay в SpectrumView: фиксируем актуальную TX-частоту/VFO-индекс и режим
|
||||||
|
FSpecView.TXFreq := ActiveTXFreqHz;
|
||||||
|
if FSplitTxB then FSpecView.TXVfoIndex := 1
|
||||||
|
else FSpecView.TXVfoIndex := FActiveVfo;
|
||||||
|
FSpecView.TXMode := FTransmitting;
|
||||||
|
// Перерисовываем сетку спектра под активный тракт (Thetis-style).
|
||||||
|
ApplySpecViewGridFromState;
|
||||||
if FWDSPReady then
|
if FWDSPReady then
|
||||||
FDSPEngine.SetTXRun(FTransmitting);
|
FDSPEngine.SetTXRun(FTransmitting);
|
||||||
if FNetwork.Connected and FNetwork.Running then
|
if FNetwork.Connected and FNetwork.Running then
|
||||||
@@ -4396,6 +4437,7 @@ begin
|
|||||||
FDSPEngine.SetMode(FMode);
|
FDSPEngine.SetMode(FMode);
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
end;
|
end;
|
||||||
|
SyncSpecViewFreq;
|
||||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||||
begin
|
begin
|
||||||
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
||||||
@@ -4516,6 +4558,7 @@ begin
|
|||||||
if FWDSPReady then FDSPEngine.SetMode(FMode);
|
if FWDSPReady then FDSPEngine.SetMode(FMode);
|
||||||
UpdateFilterButtons;
|
UpdateFilterButtons;
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
|
SyncSpecViewFreq;
|
||||||
FBandCache[FCurrentBand].Mode := FMode;
|
FBandCache[FCurrentBand].Mode := FMode;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -4547,6 +4590,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
|
SyncSpecViewFreq;
|
||||||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||||||
end;
|
end;
|
||||||
Exit;
|
Exit;
|
||||||
@@ -4569,6 +4613,7 @@ begin
|
|||||||
FFilter := best;
|
FFilter := best;
|
||||||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
|
SyncSpecViewFreq;
|
||||||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -4984,10 +5029,21 @@ procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
|||||||
// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific.
|
// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific.
|
||||||
var
|
var
|
||||||
OldMicByte, NewMicByte: Byte;
|
OldMicByte, NewMicByte: Byte;
|
||||||
|
GridChanged: Boolean;
|
||||||
begin
|
begin
|
||||||
OldMicByte := BuildMicLineSelectByte;
|
OldMicByte := BuildMicLineSelectByte;
|
||||||
|
GridChanged := (FTXSpecRefLevel <> T.TXSpecRefLevel)
|
||||||
|
or (FTXSpecRange <> T.TXSpecRange)
|
||||||
|
or (FTXSpecGridStep <> T.TXSpecGridStep);
|
||||||
FTXSettings := T;
|
FTXSettings := T;
|
||||||
NewMicByte := BuildMicLineSelectByte;
|
NewMicByte := BuildMicLineSelectByte;
|
||||||
|
// TX-grid в момент правки — обновляем рабочие поля и (если на передаче) пушим в FSpecView
|
||||||
|
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
|
||||||
|
FTXSpecRange := FTXSettings.TXSpecRange;
|
||||||
|
if FTXSettings.TXSpecGridStep > 0 then
|
||||||
|
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
|
||||||
|
if GridChanged and FTransmitting then
|
||||||
|
ApplySpecViewGridFromState;
|
||||||
ApplyTXSettingsToDSP;
|
ApplyTXSettingsToDSP;
|
||||||
if OldMicByte <> NewMicByte then
|
if OldMicByte <> NewMicByte then
|
||||||
SendDUCSpecificFromSettings;
|
SendDUCSpecificFromSettings;
|
||||||
@@ -5164,10 +5220,25 @@ begin
|
|||||||
FSpecRefLevel := RefLevel;
|
FSpecRefLevel := RefLevel;
|
||||||
FSpecRange := Range;
|
FSpecRange := Range;
|
||||||
if GridStep > 0 then FSpecGridStep := GridStep;
|
if GridStep > 0 then FSpecGridStep := GridStep;
|
||||||
FSpecView.SpecRefLevel := FSpecRefLevel;
|
// На передаче FSpecView показывает TX-сетку — RX-правка не должна её перетирать.
|
||||||
FSpecView.SpecRange := FSpecRange;
|
if not FTransmitting then ApplySpecViewGridFromState;
|
||||||
FSpecView.SpecGridStep := FSpecGridStep;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.ApplySpecViewGridFromState;
|
||||||
|
begin
|
||||||
|
if FTransmitting then
|
||||||
|
begin
|
||||||
|
FSpecView.SpecRefLevel := FTXSpecRefLevel;
|
||||||
|
FSpecView.SpecRange := FTXSpecRange;
|
||||||
|
FSpecView.SpecGridStep := FTXSpecGridStep;
|
||||||
|
end else
|
||||||
|
begin
|
||||||
|
FSpecView.SpecRefLevel := FSpecRefLevel;
|
||||||
|
FSpecView.SpecRange := FSpecRange;
|
||||||
|
FSpecView.SpecGridStep := FSpecGridStep;
|
||||||
|
end;
|
||||||
InvalidateGridCache;
|
InvalidateGridCache;
|
||||||
|
FSpectrumDirty := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ type
|
|||||||
TXWfDetector: Integer;
|
TXWfDetector: Integer;
|
||||||
TXWfAvgMode: Integer;
|
TXWfAvgMode: Integer;
|
||||||
TXWfAvgTimeMS: Double;
|
TXWfAvgTimeMS: Double;
|
||||||
|
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах)
|
||||||
|
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
|
||||||
|
TXSpecRange: Double; // dB range, e.g. 80
|
||||||
|
TXSpecGridStep: Double; // grid step in dB, e.g. 10
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TGlobalSettings = record
|
TGlobalSettings = record
|
||||||
@@ -765,6 +769,10 @@ begin
|
|||||||
T.TXWfDetector := 0;
|
T.TXWfDetector := 0;
|
||||||
T.TXWfAvgMode := 3;
|
T.TXWfAvgMode := 3;
|
||||||
T.TXWfAvgTimeMS := 120.0;
|
T.TXWfAvgTimeMS := 120.0;
|
||||||
|
// TX Grid — потолок 0 dBm (TX-уровни выше RX), 80 dB динамики, шаг 10 dB
|
||||||
|
T.TXSpecRefLevel := 0.0;
|
||||||
|
T.TXSpecRange := 80.0;
|
||||||
|
T.TXSpecGridStep := 10.0;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TSettingsManager.LoadTX(const MAC: array of Byte;
|
function TSettingsManager.LoadTX(const MAC: array of Byte;
|
||||||
@@ -824,6 +832,9 @@ begin
|
|||||||
T.TXWfDetector := JI(O,'tx_wf_detector', T.TXWfDetector);
|
T.TXWfDetector := JI(O,'tx_wf_detector', T.TXWfDetector);
|
||||||
T.TXWfAvgMode := JI(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
T.TXWfAvgMode := JI(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||||||
T.TXWfAvgTimeMS := JD(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
T.TXWfAvgTimeMS := JD(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||||||
|
T.TXSpecRefLevel := JD(O,'tx_spec_ref_level', T.TXSpecRefLevel);
|
||||||
|
T.TXSpecRange := JD(O,'tx_spec_range', T.TXSpecRange);
|
||||||
|
T.TXSpecGridStep := JD(O,'tx_spec_grid_step', T.TXSpecGridStep);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
|
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
|
||||||
@@ -875,6 +886,9 @@ begin
|
|||||||
JW(O,'tx_wf_detector', T.TXWfDetector);
|
JW(O,'tx_wf_detector', T.TXWfDetector);
|
||||||
JW(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
JW(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||||||
JW(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
JW(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||||||
|
JW(O,'tx_spec_ref_level', T.TXSpecRefLevel);
|
||||||
|
JW(O,'tx_spec_range', T.TXSpecRange);
|
||||||
|
JW(O,'tx_spec_grid_step', T.TXSpecGridStep);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
|
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ type
|
|||||||
FCmbTXWfDet: TComboBox;
|
FCmbTXWfDet: TComboBox;
|
||||||
FCmbTXWfAvg: TComboBox;
|
FCmbTXWfAvg: TComboBox;
|
||||||
FEdTXWfAvgTime: TFloatSpinEdit;
|
FEdTXWfAvgTime: TFloatSpinEdit;
|
||||||
|
// TX Grid (отдельные от RX grid в Display)
|
||||||
|
FEdTXRefLevel: TEdit;
|
||||||
|
FEdTXRange: TEdit;
|
||||||
|
FCmbTXGridStep: TComboBox;
|
||||||
|
|
||||||
// ---- CAT tab controls ----
|
// ---- CAT tab controls ----
|
||||||
FCATSerialEn: array[0..3] of TCheckBox;
|
FCATSerialEn: array[0..3] of TCheckBox;
|
||||||
@@ -1084,6 +1088,37 @@ begin
|
|||||||
FCmbTXWfAvg.ItemIndex := 3;
|
FCmbTXWfAvg.ItemIndex := 3;
|
||||||
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 5 * STEP + 5, 100);
|
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 5 * STEP + 5, 100);
|
||||||
FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
|
FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
|
||||||
|
|
||||||
|
// ---- TX Grid (RefLevel / Range / GridStep — отдельные от RX) ----
|
||||||
|
Inc(Y, 290 + 12);
|
||||||
|
Grp := MakeGroupPanel(FPageTransmit, 'TX Grid', MARGIN, Y, 700, 170);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW);
|
||||||
|
FEdTXRefLevel := TEdit.Create(Self);
|
||||||
|
FEdTXRefLevel.Parent := Grp;
|
||||||
|
FEdTXRefLevel.SetBounds(CX, R1, 96, BTN_H);
|
||||||
|
FEdTXRefLevel.Color := CLR_INPUT;
|
||||||
|
FEdTXRefLevel.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
FEdTXRefLevel.Font.Name := UI_FONT;
|
||||||
|
FEdTXRefLevel.Font.Size := 9;
|
||||||
|
FEdTXRefLevel.Text := '0';
|
||||||
|
FEdTXRefLevel.OnExit := OnTXAnyChange;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Range (dB)', PAD, R1 + STEP + 5, LW);
|
||||||
|
FEdTXRange := TEdit.Create(Self);
|
||||||
|
FEdTXRange.Parent := Grp;
|
||||||
|
FEdTXRange.SetBounds(CX, R1 + STEP, 96, BTN_H);
|
||||||
|
FEdTXRange.Color := CLR_INPUT;
|
||||||
|
FEdTXRange.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
FEdTXRange.Font.Name := UI_FONT;
|
||||||
|
FEdTXRange.Font.Size := 9;
|
||||||
|
FEdTXRange.Text := '80';
|
||||||
|
FEdTXRange.OnExit := OnTXAnyChange;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Grid step', PAD, R1 + 2 * STEP + 5, LW);
|
||||||
|
FCmbTXGridStep := MkCmb(Grp, CX, R1 + 2 * STEP, 130);
|
||||||
|
for i := 0 to High(GRID_STEP_NAMES) do FCmbTXGridStep.Items.Add(GRID_STEP_NAMES[i]);
|
||||||
|
FCmbTXGridStep.ItemIndex := 1;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.CollectTXFromUI;
|
procedure TSettingsForm.CollectTXFromUI;
|
||||||
@@ -1139,6 +1174,19 @@ begin
|
|||||||
if (FCmbTXWfAvg <> nil) and (FCmbTXWfAvg.ItemIndex >= 0) then
|
if (FCmbTXWfAvg <> nil) and (FCmbTXWfAvg.ItemIndex >= 0) then
|
||||||
FTX.TXWfAvgMode := WF_AVGMODE_VALUES[FCmbTXWfAvg.ItemIndex];
|
FTX.TXWfAvgMode := WF_AVGMODE_VALUES[FCmbTXWfAvg.ItemIndex];
|
||||||
if FEdTXWfAvgTime <> nil then FTX.TXWfAvgTimeMS := FEdTXWfAvgTime.Value;
|
if FEdTXWfAvgTime <> nil then FTX.TXWfAvgTimeMS := FEdTXWfAvgTime.Value;
|
||||||
|
if FEdTXRefLevel <> nil then
|
||||||
|
begin
|
||||||
|
Val(FEdTXRefLevel.Text, FTX.TXSpecRefLevel, i);
|
||||||
|
if i <> 0 then FTX.TXSpecRefLevel := 0.0;
|
||||||
|
end;
|
||||||
|
if FEdTXRange <> nil then
|
||||||
|
begin
|
||||||
|
Val(FEdTXRange.Text, FTX.TXSpecRange, i);
|
||||||
|
if i <> 0 then FTX.TXSpecRange := 80.0;
|
||||||
|
end;
|
||||||
|
if (FCmbTXGridStep <> nil) and (FCmbTXGridStep.ItemIndex >= 0)
|
||||||
|
and (FCmbTXGridStep.ItemIndex <= High(GRID_STEPS)) then
|
||||||
|
FTX.TXSpecGridStep := GRID_STEPS[FCmbTXGridStep.ItemIndex];
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.FireTXChange;
|
procedure TSettingsForm.FireTXChange;
|
||||||
@@ -1241,6 +1289,18 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
if FEdTXWfAvgTime <> nil then FEdTXWfAvgTime.Value := T.TXWfAvgTimeMS;
|
if FEdTXWfAvgTime <> nil then FEdTXWfAvgTime.Value := T.TXWfAvgTimeMS;
|
||||||
|
if FEdTXRefLevel <> nil then FEdTXRefLevel.Text := FormatFloat('0.#', T.TXSpecRefLevel);
|
||||||
|
if FEdTXRange <> nil then FEdTXRange.Text := FormatFloat('0.#', T.TXSpecRange);
|
||||||
|
if FCmbTXGridStep <> nil then
|
||||||
|
begin
|
||||||
|
FCmbTXGridStep.ItemIndex := 1; // дефолт 10 dB
|
||||||
|
for i := 0 to High(GRID_STEPS) do
|
||||||
|
if Abs(GRID_STEPS[i] - T.TXSpecGridStep) < 0.5 then
|
||||||
|
begin
|
||||||
|
FCmbTXGridStep.ItemIndex := i;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
finally
|
finally
|
||||||
FLoading := False;
|
FLoading := False;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+188
-28
@@ -80,6 +80,16 @@ type
|
|||||||
FSpecRange: Double;
|
FSpecRange: Double;
|
||||||
FSpecGridStep: Double;
|
FSpecGridStep: Double;
|
||||||
|
|
||||||
|
// ── TX overlay (Thetis-style: при TX данные в FSpectrumBuf приходят
|
||||||
|
// от TX-анализатора @ FTXSpanHz Гц вокруг FTXFreq, и должны быть
|
||||||
|
// отрисованы в RX-координатах (FCenterFreq/FSpanHz), чтобы линейка
|
||||||
|
// частот совпадала с RX и сигнал стоял на правильной частоте даже
|
||||||
|
// при включённом CTUN. Если RX rate ≠ TX rate, видна только пересечение.
|
||||||
|
FTXMode: Boolean;
|
||||||
|
FTXFreq: Double; // центр TX-сигнала (Hz)
|
||||||
|
FTXSpanHz: Double; // ширина TX-окна (192000 для Hermes/Saturn)
|
||||||
|
FTXVfoIndex: Integer; // 0=A, 1=B — какой VFO привязан к TX (для split)
|
||||||
|
|
||||||
// ── Marker ────────────────────────────────────────────────────────────────
|
// ── Marker ────────────────────────────────────────────────────────────────
|
||||||
FMarkerActive: Boolean;
|
FMarkerActive: Boolean;
|
||||||
FMarkerX: Integer;
|
FMarkerX: Integer;
|
||||||
@@ -114,6 +124,14 @@ type
|
|||||||
function ScaleX(X, Total, Width: Integer): Integer;
|
function ScaleX(X, Total, Width: Integer): Integer;
|
||||||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||||||
|
// Альфа-blending вертикальной полосы поверх FSpectrumBitmap (pf32bit, BGRA).
|
||||||
|
// Используется для прозрачной заливки полосы фильтра разными цветами
|
||||||
|
// (RX-зелёный / TX-красный) поверх градиента сетки.
|
||||||
|
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||||
|
// Считает X1/X2 для полосы фильтра вокруг указанной частоты VFO, c учётом
|
||||||
|
// текущего режима (LSB/USB/DSB и пр.) и FFilterBW.
|
||||||
|
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||||
|
out X1, X2, VfoX: Integer);
|
||||||
|
|
||||||
public
|
public
|
||||||
constructor Create;
|
constructor Create;
|
||||||
@@ -144,6 +162,12 @@ type
|
|||||||
property SpecRange: Double read FSpecRange write FSpecRange;
|
property SpecRange: Double read FSpecRange write FSpecRange;
|
||||||
property SpecGridStep: Double read FSpecGridStep write FSpecGridStep;
|
property SpecGridStep: Double read FSpecGridStep write FSpecGridStep;
|
||||||
|
|
||||||
|
// ── TX overlay ────────────────────────────────────────────────────────────
|
||||||
|
property TXMode: Boolean read FTXMode write FTXMode;
|
||||||
|
property TXFreq: Double read FTXFreq write FTXFreq;
|
||||||
|
property TXSpanHz: Double read FTXSpanHz write FTXSpanHz;
|
||||||
|
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
|
||||||
|
|
||||||
// ── Маркер ────────────────────────────────────────────────────────────────
|
// ── Маркер ────────────────────────────────────────────────────────────────
|
||||||
property MarkerActive: Boolean read FMarkerActive write FMarkerActive;
|
property MarkerActive: Boolean read FMarkerActive write FMarkerActive;
|
||||||
property MarkerX: Integer read FMarkerX write FMarkerX;
|
property MarkerX: Integer read FMarkerX write FMarkerX;
|
||||||
@@ -358,6 +382,8 @@ constructor TSpectrumView.Create;
|
|||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
FSpectrumBitmap := TBitmap.Create;
|
FSpectrumBitmap := TBitmap.Create;
|
||||||
|
// pf32bit нужен для попиксельного alpha-blend в BlendBand (TX/RX-полосы фильтра).
|
||||||
|
FSpectrumBitmap.PixelFormat := pf32bit;
|
||||||
FWaterfallBitmap := TBitmap.Create;
|
FWaterfallBitmap := TBitmap.Create;
|
||||||
FWfBitmap := TBitmap.Create;
|
FWfBitmap := TBitmap.Create;
|
||||||
FGridBitmap := TBitmap.Create;
|
FGridBitmap := TBitmap.Create;
|
||||||
@@ -383,6 +409,11 @@ begin
|
|||||||
FSpecRange := 110.0;
|
FSpecRange := 110.0;
|
||||||
FSpecGridStep := 10.0;
|
FSpecGridStep := 10.0;
|
||||||
FSpanHz := 192000;
|
FSpanHz := 192000;
|
||||||
|
// TX overlay defaults — сетка совпадает с RX по умолчанию
|
||||||
|
FTXMode := False;
|
||||||
|
FTXFreq := 0.0;
|
||||||
|
FTXSpanHz := 192000.0;
|
||||||
|
FTXVfoIndex := 0;
|
||||||
FSpectrumBufCount := 1024;
|
FSpectrumBufCount := 1024;
|
||||||
FWaterfallBufCount := 1024;
|
FWaterfallBufCount := 1024;
|
||||||
FWfFrameInterval := 2;
|
FWfFrameInterval := 2;
|
||||||
@@ -442,6 +473,61 @@ begin
|
|||||||
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
|
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||||
|
// Накладывает полупрозрачную вертикальную полосу [X1..X2) высотой H на
|
||||||
|
// FSpectrumBitmap. Канал A — сила оверлея 0..255 (255 = полностью непрозрачно).
|
||||||
|
// Формула: dst = src*(1-a) + color*a, BGRA-порядок (pf32bit Lazarus default).
|
||||||
|
// BeginUpdate(False) — нужен для прямого доступа к raw-пикселям через ScanLine.
|
||||||
|
var
|
||||||
|
Y, X, InvA: Integer;
|
||||||
|
Row: PByte;
|
||||||
|
W: Integer;
|
||||||
|
begin
|
||||||
|
if (FSpectrumBitmap = nil) or (Alpha = 0) then Exit;
|
||||||
|
W := FSpectrumBitmap.Width;
|
||||||
|
if X1 < 0 then X1 := 0;
|
||||||
|
if X2 > W then X2 := W;
|
||||||
|
if X2 <= X1 then Exit;
|
||||||
|
InvA := 255 - Alpha;
|
||||||
|
FSpectrumBitmap.BeginUpdate(False);
|
||||||
|
try
|
||||||
|
for Y := 0 to H - 1 do
|
||||||
|
begin
|
||||||
|
Row := PByte(FSpectrumBitmap.ScanLine[Y]);
|
||||||
|
if Row = nil then Continue;
|
||||||
|
Inc(Row, X1 * 4);
|
||||||
|
for X := X1 to X2 - 1 do
|
||||||
|
begin
|
||||||
|
Row[0] := Byte((Alpha * B + InvA * Row[0]) div 255);
|
||||||
|
Row[1] := Byte((Alpha * G + InvA * Row[1]) div 255);
|
||||||
|
Row[2] := Byte((Alpha * R + InvA * Row[2]) div 255);
|
||||||
|
// Row[3] оставляем — на BitBlt в TPaintBox не влияет.
|
||||||
|
Inc(Row, 4);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
FSpectrumBitmap.EndUpdate(False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||||
|
out X1, X2, VfoX: Integer);
|
||||||
|
// Возвращает X-координаты левого/правого края полосы фильтра вокруг VfoFreq
|
||||||
|
// и саму позицию VFO. Учитывает FMode (LSB/USB/DSB) и FFilterBW.
|
||||||
|
var
|
||||||
|
Lo_Hz, Hi_Hz, Half: Double;
|
||||||
|
begin
|
||||||
|
Half := FFilterBW / 2;
|
||||||
|
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||||
|
case FMode of
|
||||||
|
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
||||||
|
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
||||||
|
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||||||
|
end;
|
||||||
|
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||||
|
X2 := Max(0, Min(W - 1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||||
var
|
var
|
||||||
Row, Col: Integer; YMin, Alpha: Integer;
|
Row, Col: Integer; YMin, Alpha: Integer;
|
||||||
@@ -630,7 +716,7 @@ var
|
|||||||
i, Yp, W, H, GX: Integer;
|
i, Yp, W, H, GX: Integer;
|
||||||
DBmin, DBmax, dB: Double;
|
DBmin, DBmax, dB: Double;
|
||||||
VfoX, X1, X2: Integer;
|
VfoX, X1, X2: Integer;
|
||||||
Lo_Hz, Hi_Hz, Half: Double;
|
TXVfoX, TXX1, TXX2: Integer;
|
||||||
AGCy, AGCHangY: Integer;
|
AGCy, AGCHangY: Integer;
|
||||||
SrcF, Frac, dBv: Double;
|
SrcF, Frac, dBv: Double;
|
||||||
S0, S1: Integer;
|
S0, S1: Integer;
|
||||||
@@ -679,21 +765,34 @@ begin
|
|||||||
LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020);
|
LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020);
|
||||||
|
|
||||||
// ── 2. Полоса фильтра ─────────────────────────────────────────────────────
|
// ── 2. Полоса фильтра ─────────────────────────────────────────────────────
|
||||||
Half := FFilterBW / 2;
|
// Логика:
|
||||||
if FActiveVfo = 0 then
|
// Не TX → одна полоса на FActiveVfo, штатным цветом FTheme.SpecFilter (solid).
|
||||||
VfoX := Round((FVfoA - FCenterFreq + FSpanHz/2) / FSpanHz * W)
|
// TX, не split → одна полоса на FActiveVfo, alpha-blend красный.
|
||||||
else
|
// TX, split → две полосы: RX (FActiveVfo) — alpha-blend зелёный,
|
||||||
VfoX := Round((FVfoB - FCenterFreq + FSpanHz/2) / FSpanHz * W);
|
// TX (FTXVfoIndex) — alpha-blend красный.
|
||||||
case FMode of
|
// Edges + VFO-cursor рисуются позже на VfoX/X1/X2 — это RX-VFO-полоса
|
||||||
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
// (для split-режима TX-полоса рисуется без edges чтобы визуально отличалась).
|
||||||
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
||||||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||||
|
if FTXMode then
|
||||||
|
begin
|
||||||
|
if FTXVfoIndex <> FActiveVfo then
|
||||||
|
begin
|
||||||
|
// Split TX: RX-полоса зелёная, TX-полоса (на другом VFO) красная.
|
||||||
|
if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100);
|
||||||
|
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||||
|
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||||
|
if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100);
|
||||||
|
end else
|
||||||
|
// Не split: единственная полоса — TX на активном VFO, красная.
|
||||||
|
if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100);
|
||||||
|
end else
|
||||||
|
begin
|
||||||
|
// Обычный приём — заливка штатным цветом без alpha.
|
||||||
|
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
|
||||||
|
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
|
||||||
|
C.Pen.Style := psSolid;
|
||||||
end;
|
end;
|
||||||
X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
|
||||||
X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
|
||||||
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
|
|
||||||
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
|
|
||||||
C.Pen.Style := psSolid;
|
|
||||||
|
|
||||||
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
||||||
if FWDSPReady then
|
if FWDSPReady then
|
||||||
@@ -729,15 +828,40 @@ begin
|
|||||||
if FSpPtsLen <> W + 2 then
|
if FSpPtsLen <> W + 2 then
|
||||||
begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end;
|
begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end;
|
||||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
||||||
for i := 0 to W - 1 do
|
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
|
||||||
begin
|
begin
|
||||||
SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
|
// TX-режим: данные приходят @ FTXSpanHz вокруг FTXFreq (TX-анализатор —
|
||||||
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
// baseband), а рисуем в координатах RX (FCenterFreq/FSpanHz). Это даёт
|
||||||
S1 := Min(S0 + 1, SrcCount - 1);
|
// правильное положение TX-сигнала при CTUN ON и выравнивает сетку, когда
|
||||||
Frac := SrcF - S0;
|
// RX rate ≠ TX rate (192k).
|
||||||
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
for i := 0 to W - 1 do
|
||||||
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
begin
|
||||||
end;
|
// freq, соответствующая пикселю i в RX-координатах
|
||||||
|
SrcF := FCenterFreq - FSpanHz * 0.5 + i * FSpanHz / Max(1, W - 1);
|
||||||
|
// смещение от центра TX-окна
|
||||||
|
Frac := SrcF - FTXFreq;
|
||||||
|
if (Frac < -FTXSpanHz * 0.5) or (Frac > FTXSpanHz * 0.5) then
|
||||||
|
dBv := -200.0 // вне диапазона TX-анализатора
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
SrcF := (Frac + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
|
||||||
|
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
||||||
|
S1 := Min(S0 + 1, SrcCount - 1);
|
||||||
|
Frac := SrcF - S0;
|
||||||
|
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
||||||
|
end;
|
||||||
|
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
||||||
|
end;
|
||||||
|
end else
|
||||||
|
for i := 0 to W - 1 do
|
||||||
|
begin
|
||||||
|
SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
|
||||||
|
S0 := Min(Trunc(SrcF), SrcCount - 1);
|
||||||
|
S1 := Min(S0 + 1, SrcCount - 1);
|
||||||
|
Frac := SrcF - S0;
|
||||||
|
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
||||||
|
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
||||||
|
end;
|
||||||
FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H);
|
FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H);
|
||||||
|
|
||||||
// ── 5. Градиент ───────────────────────────────────────────────────────────
|
// ── 5. Градиент ───────────────────────────────────────────────────────────
|
||||||
@@ -756,6 +880,17 @@ begin
|
|||||||
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
|
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
|
||||||
C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1;
|
C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1;
|
||||||
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
|
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
|
||||||
|
// Split TX: рисуем края + cursor для TX-VFO красным, чтобы было видно где идёт передача
|
||||||
|
if FTXMode and (FTXVfoIndex <> FActiveVfo) then
|
||||||
|
begin
|
||||||
|
C.Pen.Color := TColor($002030E0); C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||||||
|
C.MoveTo(TXX1, 0); C.LineTo(TXX1, H);
|
||||||
|
C.MoveTo(TXX2, 0); C.LineTo(TXX2, H);
|
||||||
|
C.Pen.Width := 2;
|
||||||
|
C.MoveTo(TXVfoX, 0); C.LineTo(TXVfoX, H - 12);
|
||||||
|
C.Brush.Color := TColor($002030E0); C.Brush.Style := bsSolid; C.Pen.Width := 1;
|
||||||
|
C.Polygon([Point(TXVfoX-5,0), Point(TXVfoX+5,0), Point(TXVfoX,8)]);
|
||||||
|
end;
|
||||||
|
|
||||||
if FMarkerActive then DrawMarkerLine(C, W, H);
|
if FMarkerActive then DrawMarkerLine(C, W, H);
|
||||||
end;
|
end;
|
||||||
@@ -851,11 +986,36 @@ begin
|
|||||||
WatSrcF := 0.0; Px := @FWfPixels[0];
|
WatSrcF := 0.0; Px := @FWfPixels[0];
|
||||||
for X := 0 to W - 1 do
|
for X := 0 to W - 1 do
|
||||||
begin
|
begin
|
||||||
WatS0 := Trunc(WatSrcF);
|
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
|
||||||
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
begin
|
||||||
WatS1 := WatS0 + 1;
|
// частота для пикселя X в RX-координатах → индекс в TX-буфере
|
||||||
frac := WatSrcF - WatS0;
|
dB := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq;
|
||||||
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
if (dB < -FTXSpanHz * 0.5) or (dB > FTXSpanHz * 0.5) then
|
||||||
|
begin
|
||||||
|
// Вне TX-окна: копируем пиксель из предыдущей строки (она лежит на
|
||||||
|
// расстоянии W в FWfPixels — Move уже сдвинул её ниже). Так старая
|
||||||
|
// RX-картинка продолжается без чёрной "дыры", пока TX рисует своё
|
||||||
|
// окно в центре.
|
||||||
|
if H > 1 then Px^ := FWfPixels[X + W]
|
||||||
|
else Px^ := 0;
|
||||||
|
Inc(Px);
|
||||||
|
WatSrcF := WatSrcF + Step;
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
|
||||||
|
WatS0 := Trunc(frac);
|
||||||
|
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
||||||
|
WatS1 := WatS0 + 1;
|
||||||
|
frac := frac - WatS0;
|
||||||
|
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
||||||
|
end else
|
||||||
|
begin
|
||||||
|
WatS0 := Trunc(WatSrcF);
|
||||||
|
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
|
||||||
|
WatS1 := WatS0 + 1;
|
||||||
|
frac := WatSrcF - WatS0;
|
||||||
|
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
|
||||||
|
end;
|
||||||
V := Trunc((dB - WfLow) * InvRange);
|
V := Trunc((dB - WfLow) * InvRange);
|
||||||
if V < 0 then V := 0; if V > 255 then V := 255;
|
if V < 0 then V := 0; if V > 255 then V := 255;
|
||||||
if FLightTheme then
|
if FLightTheme then
|
||||||
|
|||||||
+30
-16
@@ -108,6 +108,12 @@ const
|
|||||||
// TX mic ring buffer — должен быть степенью двойки
|
// TX mic ring buffer — должен быть степенью двойки
|
||||||
TX_MIC_RING = 8192;
|
TX_MIC_RING = 8192;
|
||||||
|
|
||||||
|
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
|
||||||
|
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0),
|
||||||
|
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
|
||||||
|
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
|
||||||
|
TX_SAMPLE_RATE = 192000;
|
||||||
|
|
||||||
type
|
type
|
||||||
// Пакет в очереди между сетевым и DSP потоком
|
// Пакет в очереди между сетевым и DSP потоком
|
||||||
TIQQueueItem = record
|
TIQQueueItem = record
|
||||||
@@ -145,6 +151,8 @@ type
|
|||||||
FAudioRate: Integer;
|
FAudioRate: Integer;
|
||||||
FBufSize: Integer; // входной буфер @ FSampleRate
|
FBufSize: Integer; // входной буфер @ FSampleRate
|
||||||
FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate)
|
FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate)
|
||||||
|
FTXSampleRate: Integer; // фиксированный rate TX/DUC (TX_SAMPLE_RATE), не меняется при ChangeSampleRate
|
||||||
|
FTXOutBufSize: Integer; // выходной буфер TXA в IQ парах @ FTXSampleRate (= FAudioBufSize * FTXSampleRate / FAudioRate)
|
||||||
|
|
||||||
// Interleaved double I/Q буферы для fexchange0
|
// Interleaved double I/Q буферы для fexchange0
|
||||||
FRXIn: array of Double;
|
FRXIn: array of Double;
|
||||||
@@ -386,6 +394,7 @@ type
|
|||||||
|
|
||||||
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 AudioBufSize: Integer read FAudioBufSize;
|
property AudioBufSize: Integer read FAudioBufSize;
|
||||||
property AGCTop: Double read FAGCTop;
|
property AGCTop: Double read FAGCTop;
|
||||||
property AGCSlope: Integer read FAGCSlope;
|
property AGCSlope: Integer read FAGCSlope;
|
||||||
@@ -644,6 +653,9 @@ begin
|
|||||||
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
|
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
|
||||||
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
|
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
|
||||||
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
|
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
|
||||||
|
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
|
||||||
|
FTXSampleRate := TX_SAMPLE_RATE;
|
||||||
|
FTXOutBufSize := BufSize * FTXSampleRate div AudioRate; // = 4096 @ 48k audio, 192k tx
|
||||||
FInitialized := False;
|
FInitialized := False;
|
||||||
FAnalyzerOpen := False;
|
FAnalyzerOpen := False;
|
||||||
FMode := MODE_USB;
|
FMode := MODE_USB;
|
||||||
@@ -672,10 +684,10 @@ begin
|
|||||||
FWfAvgMode := 3; // Log Recursive
|
FWfAvgMode := 3; // Log Recursive
|
||||||
FWfAvgTimeMS := 120.0;
|
FWfAvgTimeMS := 120.0;
|
||||||
|
|
||||||
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
||||||
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
||||||
SetLength(FTXIn, FAudioBufSize * 2);
|
SetLength(FTXIn, FAudioBufSize * 2); // in_size пар @ FAudioRate
|
||||||
SetLength(FTXOut, FBufSize * 2);
|
SetLength(FTXOut, FTXOutBufSize * 2); // out_size пар @ FTXSampleRate (фикс)
|
||||||
SetLength(FRXAccI, FBufSize);
|
SetLength(FRXAccI, FBufSize);
|
||||||
SetLength(FRXAccQ, FBufSize);
|
SetLength(FRXAccQ, FBufSize);
|
||||||
SetLength(FSpecBuf, FBufSize * 2);
|
SetLength(FSpecBuf, FBufSize * 2);
|
||||||
@@ -847,7 +859,7 @@ const KEEP_TIME_SEC = 0.10;
|
|||||||
begin
|
begin
|
||||||
AnalyzerFFT := NormalizeFFTSize(FTXFFTSize);
|
AnalyzerFFT := NormalizeFFTSize(FTXFFTSize);
|
||||||
FTXDispPos := 0;
|
FTXDispPos := 0;
|
||||||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
|
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FTXSampleRate,
|
||||||
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||||||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffectiveFPS);
|
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffectiveFPS);
|
||||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||||||
@@ -868,7 +880,7 @@ begin
|
|||||||
SetDisplayNumAverage (TX_DISP_ID, 1, WfAvgCount);
|
SetDisplayNumAverage (TX_DISP_ID, 1, WfAvgCount);
|
||||||
SetDisplayAvBackmult (TX_DISP_ID, 1, WfBackmult);
|
SetDisplayAvBackmult (TX_DISP_ID, 1, WfBackmult);
|
||||||
SetDisplayNormOneHz (TX_DISP_ID, 1, 0);
|
SetDisplayNormOneHz (TX_DISP_ID, 1, 0);
|
||||||
SetDisplaySampleRate (TX_DISP_ID, FSampleRate);
|
SetDisplaySampleRate (TX_DISP_ID, FTXSampleRate);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||||
@@ -951,16 +963,16 @@ begin
|
|||||||
SetRXAPanelRun(RXA_CHAN, 1);
|
SetRXAPanelRun(RXA_CHAN, 1);
|
||||||
SetChannelState(RXA_CHAN, 1, 0);
|
SetChannelState(RXA_CHAN, 1, 0);
|
||||||
|
|
||||||
// TXA: mic @ FAudioRate → IQ @ FSampleRate (DUC rate)
|
// TXA: mic @ FAudioRate → IQ @ FTXSampleRate (фиксированный DUC rate, не зависит от RX).
|
||||||
// out_size = FAudioBufSize * FSampleRate/FAudioRate = FBufSize (4096 @ 192kHz)
|
// out_size = FAudioBufSize * FTXSampleRate/FAudioRate = FTXOutBufSize (4096 @ 192k tx).
|
||||||
// FTXOut уже имеет размер FBufSize*2 — подходит идеально
|
// FTXOut уже имеет размер FTXOutBufSize*2.
|
||||||
OpenChannel(
|
OpenChannel(
|
||||||
TXA_CHAN,
|
TXA_CHAN,
|
||||||
FAudioBufSize,
|
FAudioBufSize,
|
||||||
FAudioBufSize,
|
FAudioBufSize,
|
||||||
FAudioRate,
|
FAudioRate,
|
||||||
FAudioRate,
|
FAudioRate,
|
||||||
FSampleRate,
|
FTXSampleRate,
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
0.010, 0.010, 0.010, 0.010,
|
0.010, 0.010, 0.010, 0.010,
|
||||||
@@ -969,7 +981,8 @@ begin
|
|||||||
|
|
||||||
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
|
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
|
||||||
SetTXAPanelRun(TXA_CHAN, 1);
|
SetTXAPanelRun(TXA_CHAN, 1);
|
||||||
SetTXAPanelSelect(TXA_CHAN, 1);
|
// PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I.
|
||||||
|
SetTXAPanelSelect(TXA_CHAN, 2);
|
||||||
|
|
||||||
FInitialized := True;
|
FInitialized := True;
|
||||||
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
|
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
|
||||||
@@ -1186,6 +1199,7 @@ begin
|
|||||||
FMode := Mode;
|
FMode := Mode;
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
|
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
|
||||||
|
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
|
||||||
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
|
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1490,7 +1504,7 @@ end;
|
|||||||
|
|
||||||
procedure TWDSPEngine.ProcessTXBlock;
|
procedure TWDSPEngine.ProcessTXBlock;
|
||||||
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
||||||
// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback.
|
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
|
||||||
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
|
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
|
||||||
// если mic ring buffer содержит достаточно данных — берём их,
|
// если mic ring buffer содержит достаточно данных — берём их,
|
||||||
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
|
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
|
||||||
@@ -1526,15 +1540,15 @@ begin
|
|||||||
// TX сигнал → отдельный TX-analyzer (TX_DISP_ID).
|
// TX сигнал → отдельный TX-analyzer (TX_DISP_ID).
|
||||||
// Подаём чанками по DISPLAY_BLOCK_SIZE — analyzer ожидает именно такой
|
// Подаём чанками по DISPLAY_BLOCK_SIZE — analyzer ожидает именно такой
|
||||||
// размер блока (см. SetAnalyzer). Если подавать FTXOut целиком
|
// размер блока (см. SetAnalyzer). Если подавать FTXOut целиком
|
||||||
// (FBufSize пар), analyzer прочитает только первые DISPLAY_BLOCK_SIZE
|
// (FTXOutBufSize пар), analyzer прочитает только первые DISPLAY_BLOCK_SIZE
|
||||||
// и проигнорирует остальное → спектр будет пустой.
|
// и проигнорирует остальное → спектр будет пустой.
|
||||||
if FAnalyzerOpen then
|
if FAnalyzerOpen then
|
||||||
for i := 0 to FBufSize - 1 do
|
for i := 0 to FTXOutBufSize - 1 do
|
||||||
FeedTXDisplaySample(FTXOut[i * 2], FTXOut[i * 2 + 1]);
|
FeedTXDisplaySample(FTXOut[i * 2], FTXOut[i * 2 + 1]);
|
||||||
|
|
||||||
// FTXOut содержит FBufSize IQ пар @ FSampleRate (interleaved double)
|
// FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double)
|
||||||
if Assigned(FOnTXIQ) then
|
if Assigned(FOnTXIQ) then
|
||||||
FOnTXIQ(FTXOut, FBufSize);
|
FOnTXIQ(FTXOut, FTXOutBufSize);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
||||||
|
|||||||
Reference in New Issue
Block a user