Files
ewsdr/MainForm.pas
T
2026-03-09 20:55:19 +03:00

4344 lines
149 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit MainForm;
{
OpenHPSDR Transceiver - Main Form
Lazarus / FPC 3.2+ (нет inline var, нет анонимных процедур)
ИСПРАВЛЕНИЯ:
- (Web NR/NB/ANF fix) BtnNRClick, BtnNBClick, BtnANFClick переписаны
без использования «Sender as TFlatButton» — теперь обращаются напрямую
к BtnNR / BtnNB / BtnANF. Это устраняет EAccessViolation / EInvalidCast
при вызове из SyncWebNR/SyncWebNB/SyncWebANF через TThread.Synchronize,
где Sender передавался как nil. Исключение в UI-потоке приводило к
разрыву WebSocket-соединения и «отключению» веб-клиента.
- SyncWebNR / SyncWebNB / SyncWebANF: передают реальную кнопку (BtnNR /
BtnNB / BtnANF) вместо nil — защита на случай изменения обработчиков.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, WinFirewall, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput,
IntfGraphics, FPImage,
Settings,
WebServer;
const
CLR_BG = TColor($00101010);
CLR_PANEL = TColor($00181818);
CLR_BORDER = TColor($00303030);
CLR_FREQ = TColor($0000FF00);
CLR_FREQ_DIM = TColor($00008000);
CLR_AMBER = TColor($0000AAFF);
CLR_ACTIVE = TColor($00003300);
CLR_INACTIVE = TColor($00202020);
CLR_TEXT = TColor($00CCCCCC);
CLR_TEXTDIM = TColor($00666666);
CLR_METER_ON = TColor($0000CC44);
CLR_METER_OVR= TColor($000000CC);
CLR_SPECTRUM = TColor($0044FF44);
BAND_COUNT = 11;
MODE_COUNT = 8;
FILT_COUNT = 10;
// Группы фильтров по типу модуляции
// SSB: LSB/USB
FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string =
('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k');
FILT_SSB_BW: array[0..FILT_COUNT-1] of Integer =
(5000, 4400, 3800, 3300, 2900, 2700, 2400, 2100, 1800, 1000);
FILT_SSB_DEF = 5; // 2.7k
// CW: CWL/CWU
FILT_CW_NAMES: array[0..FILT_COUNT-1] of string =
('1.0k','800','750','600','500','400','250','100','50','25');
FILT_CW_BW: array[0..FILT_COUNT-1] of Integer =
(1000, 800, 750, 600, 500, 400, 250, 100, 50, 25);
FILT_CW_DEF = 4; // 500
// AM/SAM/DSB
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
('16k','12k','10k','8k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
FILT_AM_BW: array[0..FILT_COUNT-1] of Integer =
(16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400);
FILT_AM_DEF = 3; // 8k
// FM
FILT_FM_NAMES: array[0..FILT_COUNT-1] of string =
('20k','15k','12k','10k','8k','6k','5k','4k','3k','2k');
FILT_FM_BW: array[0..FILT_COUNT-1] of Integer =
(20000, 15000, 12000, 10000, 8000, 6000, 5000, 4000, 3000, 2000);
FILT_FM_DEF = 0; // 20k
BAND_NAMES: array[0..BAND_COUNT-1] of string = (
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
BAND_FREQ: array[0..BAND_COUNT-1] of Double = (
1900000, 3750000, 5357000, 7100000, 10125000,
14200000, 18120000, 21200000, 24940000, 28500000, 50150000);
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
WFALL_PALETTE: array[0..255] of LongWord = (
$00000000, $00020000, $00050000, $00080000, $000A0000, $000D0000, $00100000, $00120000,
$00150000, $00180000, $001A0000, $001D0000, $00200000, $00220000, $00250000, $00280000,
$002A0000, $002D0000, $00300000, $00320000, $00350000, $00380000, $003A0000, $003D0000,
$00400000, $00420000, $00450000, $00480000, $004A0000, $004D0000, $00500000, $00530000,
$00560000, $005A0000, $005D0000, $00600000, $00640000, $00670000, $006A0000, $006E0000,
$00710000, $00740000, $00780000, $007B0000, $007E0000, $00820000, $00850000, $00880000,
$008C0000, $008F0000, $00920000, $00960000, $00990000, $009C0000, $00A00000, $00A30000,
$00A60000, $00AA0000, $00AD0000, $00B00000, $00B40000, $00B60200, $00B90500, $00BB0700,
$00BE0A00, $00C00C00, $00C30F00, $00C51100, $00C81400, $00CA1600, $00CD1900, $00CF1B00,
$00D21E00, $00D42000, $00D72300, $00D92500, $00DC2800, $00DE2A00, $00E12D00, $00E32F00,
$00E63200, $00E73700, $00E83C00, $00E94100, $00EB4600, $00EC4B00, $00ED5000, $00EE5500,
$00F05A00, $00F15F00, $00F26400, $00F36900, $00F56E00, $00F67300, $00F77800, $00F87D00,
$00FA8200, $00FB8700, $00FC8C00, $00FD9100, $00FF9600, $00FD9A00, $00FB9E00, $00F9A200,
$00F8A600, $00F6AA00, $00F4AE00, $00F2B200, $00F1B600, $00EFBA00, $00EDBE00, $00EBC200,
$00EAC600, $00E8CA00, $00E6CE00, $00E4D200, $00E3D600, $00E1DA00, $00DFDE00, $00DDE200,
$00DCE600, $00D6E700, $00D0E800, $00CAE900, $00C4EB00, $00BEEC00, $00B8ED00, $00B2EE00,
$00ACF000, $00A6F100, $00A0F200, $009AF300, $0094F500, $008EF600, $0088F700, $0082F800,
$007CFA00, $0076FB00, $0070FC00, $006AFD00, $0064FF00, $005FFF04, $005AFF08, $0055FF0C,
$0050FF10, $004BFF14, $0046FF18, $0041FF1C, $003CFF20, $0037FF24, $0032FF28, $002CFF2C,
$0028FF30, $0023FF34, $001EFF38, $0019FF3C, $0014FF40, $000FFF44, $000AFF48, $0005FF4C,
$0000FF50, $0000FF56, $0000FF5C, $0000FF62, $0000FF68, $0000FF6E, $0000FF74, $0000FF7A,
$0000FF80, $0000FF86, $0000FF8C, $0000FF92, $0000FF98, $0000FF9E, $0000FFA4, $0000FFAA,
$0000FFB0, $0000FFB6, $0000FFBC, $0000FFC2, $0000FFC8, $0000FDCA, $0000FBCD, $0000F9D0,
$0000F8D3, $0000F6D5, $0000F4D8, $0000F2DB, $0000F1DE, $0000EFE0, $0000EDE3, $0000EBE6,
$0000EAE9, $0000E8EB, $0000E6EE, $0000E4F1, $0000E3F4, $0000E1F6, $0000DFF9, $0000DDFC,
$0000DCFF, $0000D7FF, $0000D2FF, $0000CDFF, $0000C8FF, $0000C3FF, $0000BEFF, $0000B9FF,
$0000B4FF, $0000AFFF, $0000AAFF, $0000A5FF, $0000A0FF, $00009BFF, $000096FF, $000091FF,
$00008CFF, $000087FF, $000082FF, $00007DFF, $000078FF, $000073FF, $00006FFF, $00006AFF,
$000066FF, $000061FF, $00005DFF, $000058FF, $000054FF, $00004FFF, $00004BFF, $000046FF,
$000042FF, $00003DFF, $000039FF, $000034FF, $000030FF, $00002BFF, $000027FF, $000022FF,
$00001EFF, $00112DFF, $00223CFF, $00334BFF, $00445AFF, $005569FF, $006678FF, $007787FF,
$008896FF, $0099A5FF, $00AAB4FF, $00BBC3FF, $00CCD2FF, $00DDE1FF, $00EEF0FF, $00FFFFFF
);
type
TDeviceItem = record
Dev: THPSDRDevice;
Display: string;
end;
// Синхронизирующий объект для OnDeviceFound
TDeviceFoundSync = class
private
FForm: TObject; // TMainForm, через forward ref
FDev: THPSDRDevice;
FEntry: string;
public
constructor Create(AForm: TObject; const D: THPSDRDevice; const E: string);
procedure Execute;
end;
// Синхронизирующий объект для HP Status
TStatusUISync = class
private
FForm: TObject;
FFwdW: Double;
FSWRV: Double;
FSupplyV: Double;
FPLLLock: Boolean;
public
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL: Boolean);
procedure Execute;
end;
// Синхронизирующий объект для DDC IQ
TDDCSeqSync = class
private
FForm: TObject;
FDDCIdx: Integer;
FSeq: LongWord;
public
constructor Create(AForm: TObject; Idx: Integer; Seq: LongWord);
procedure Execute;
end;
{ TMainForm }
TMainForm = class(TForm)
private
// ---- Network ----
FNetwork: THPSDRNetwork;
FDevices: array of TDeviceItem;
FDeviceCount: Integer;
FDeviceDialog: TDeviceDialog;
FPendingIP: string; // IP устройства для подключения
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
FPanelHidden: Boolean; // True = левая панель скрыта
FPendingBoardType: Integer; // BoardType устройства для подключения
// ---- DSP + Audio ----
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FWDSPReady: Boolean; // True когда WDSP открыт и работает
// ---- State ----
FVfoA: Double;
FVfoB: Double;
FActiveVfo: Integer;
FMode: Integer;
FFilter: Integer;
FAGCMode: Integer; // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF
FAGCTop: Integer; // AGC level dBm, 20..120
FCTun: Boolean; // Center Tune: True=спектр стоит, маркер двигается
FFilterBW: Integer; // текущая полоса фильтра в Гц
// мышь на спектре/водопаде
FSpecDrag: Boolean;
FSpecDragX0: Integer; // X при нажатии
FSpecDragFreq: Double; // FCenterFreq при нажатии
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
// Маркер правой кнопки: вертикальная линия на спектре и водопаде
FMarkerActive: Boolean; // True = линия видима
FMarkerX: Integer; // X в пикселях (относительно ширины панели)
FDriveLevel: Byte;
FRunning: Boolean;
FTransmitting: Boolean;
FMuted: Boolean;
FVolume: Integer;
FLastSMeter: Double;
FSMeterPeak: Double; // верхняя граница светлой зоны
FSMeterMin: Double; // нижняя граница светлой зоны
FSMeterAvg: Double; // сглаженное среднее (EMA)
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
FRXStartTime: QWord; // GetTickCount64 момента нажатия START
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
FLastDDCSeq: LongWord; // последний seq (пишется из сетевого потока)
FLastDDCIndex: Integer; // последний DDC index
FLastFwdW: Double;
FLastSWR: Double;
// ---- Spectrum ----
FSpectrumBuf: array[0..1023] of Single;
FWfAvgBuf: array[0..1023] of Single;
FSpectrumBitmap: TBitmap;
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FWfHigh: Double;
FWfLow: Double;
FWaterfallBitmap:TBitmap;
FWaterfallTemp: TBitmap;
FSpectrumWidth: Integer;
FSpectrumHeight: Integer;
FWaterfallHeight:Integer;
// Водопад: прямой массив пикселей BGRA + RawImage bitmap без IntfImage roundtrip
FWfPixels: array of LongWord; // W×H пикселей, строка 0 = верх (новейшая)
FWfBitmap: TBitmap;
FWfBitmapW: Integer;
FWfBitmapH: Integer;
// Спектр: кэш фона (сетка+фон) — перерисовывается только при ресайзе
FGridBitmap: TBitmap;
FGridBitmapW: Integer;
FGridBitmapH: Integer;
FSpPts: array of TPoint;
FSpPtsLen: Integer;
// Градиент спектра: кэшированный IntfImage — без alloc каждый кадр
FSpecGradImg: TLazIntfImage;
FSpecGradBmp: TBitmap; // кэш bitmap для градиента — без alloc каждый кадр
// Водопад: кэшированный IntfImage — без alloc/free каждый кадр
FWfIntfImg: TLazIntfImage;
// Кэш линейки частот — перерисовывается только при смене FCenterFreq/FVfoA
FRulerBitmap: TBitmap;
FRulerLastFreq: Double;
FRulerLastVfo: Double;
// --- Settings ---
FSettings: TSettingsManager;
FWebServer: TWebServer; // веб-интерфейс (порт 8080)
// Временные поля для передачи параметров в Synchronize-методы
FWebSyncFreq: Double;
FWebSyncInt: Integer;
FWebSyncBool: Boolean;
FWebSyncM: TThreadMethod;
FCurrentBand: Integer; // текущий активный диапазон 0..10
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
FDevConnected: Boolean; // True после первого подключения
FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO
FSpanHz: Double;
FSampleRate: Integer; // текущий DDC sample rate (Hz)
// ---- Timers ----
FMeterTimer: TTimer;
FSpectrumTimer: TTimer;
FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации
// ---- Toolbar ----
PanelToolbar: TPanel;
BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
// ---- Left panel ----
PanelLeft: TPanel;
PanelVfoA: TPanel;
LblVfoALabel: TLabel;
FreqDispA: TFreqDisplay;
PanelVfoB: TPanel;
LblVfoBLabel: TLabel;
FreqDispB: TFreqDisplay;
PanelVfoButtons: TPanel;
BtnVfoSwap: TFlatButton;
BtnVfoACopyB: TFlatButton;
BtnVfoBCopyA: TFlatButton;
PanelBands: TPanel;
BtnBand: array[0..BAND_COUNT-1] of TFlatButton;
PanelMode: TPanel;
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
PanelFilter: TPanel;
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
BtnCTun: TFlatButton;
PanelRX: TPanel;
LblAGC: TLabel;
BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF
LblAGCTop: TLabel; // показывает значение уровня
TrkAGC: TTrackBar; // ползунок уровня AGC
LblVol: TLabel;
TrkVolume: TTrackBar;
BtnNR: TFlatButton;
BtnNB: TFlatButton;
BtnSNB: TFlatButton;
BtnANF: TFlatButton;
BtnMute: TFlatButton;
PanelSMeterRight: TPanel; // контейнер S-метра справа
PbSMeterRight: TPaintBox;// новый большой S-метр справа
FSmBitmap: TBitmap; // off-screen буфер для alpha-blend
PanelTX: TPanel;
LblDrv: TLabel;
TrkDrive: TTrackBar;
BtnMOX: TFlatButton;
PbFwdPower: TPaintBox;
PbSWR: TPaintBox;
LblFwdPwr: TLabel;
LblSWRVal: TLabel;
// ---- Right panel ----
PanelRight: TPanel;
PanelSpanButtons: TPanel;
LblSpan: TLabel;
BtnSpan48k: TFlatButton;
BtnSpan96k: TFlatButton;
BtnSpan192k: TFlatButton;
BtnSpan384k: TFlatButton;
BtnSpan768k: TFlatButton;
BtnSpan1536k: TFlatButton;
BtnWfAGC: TFlatButton;
BtnWfNF: TFlatButton;
BtnHidePanel: TFlatButton; // скрыть/показать левую панель
PbSpectrum: TPaintBox;
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
PbWaterfall: TPaintBox;
// ---- Status bar ----
StatusBar1: TStatusBar;
// ---- Helpers ----
procedure BuildUI;
procedure ApplyDarkTheme;
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
procedure PositionSpanOverlayButtons;
procedure UpdateVfoDisplay;
function FormatFreq(Hz: Double): string;
function ScaleX(X, Total, Width: Integer): Integer;
procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect; Value, Peak, MinVal: Double;
out ZX1, ZX2, ZY1, ZY2: Integer);
procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect; X1, X2, Y1, Y2: Integer);
procedure PbSMeterRightPaint(Sender: TObject);
procedure DrawBarMeter(ACanvas: TCanvas; R: TRect;
Value, MaxVal: Double; BarColor: TColor);
procedure DrawSpectrum;
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
procedure DrawWaterfall;
procedure ResetWfAvgBuf;
procedure ResizeSMeter;
procedure FillDemoSpectrum;
procedure ResetSpectrumBuf;
procedure ResizeSpectrumPanels;
procedure RecreateDSPEngine(ASampleRate: Integer);
// Network callbacks
procedure OnDeviceFound(const Dev: THPSDRDevice);
procedure OnHPStatusCB(const Status: THighPriorityStatus);
procedure OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
procedure OnWDSPOpenDone(Success: Boolean);
procedure DoConnectDevice(const Dev: THPSDRDevice);
procedure OnMicPacketCB(const Data: TMicDataPacket);
// DSP/Audio callbacks (вызываются из рабочих потоков)
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
// Public UI update (called from sync objects)
procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
procedure DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
// Event handlers
procedure BtnDiscoverClick(Sender: TObject);
procedure BtnDiscoverFromDialog(Sender: TObject);
procedure BtnStartStopClick(Sender: TObject);
procedure FreqDispAChanged(Sender: TObject; NewFreq: Int64);
procedure ApplyVfoA(NewFreq: Int64);
// Настройки
procedure SaveCurrentBand;
procedure RestoreBand(BandIdx: Integer);
procedure SaveAllAndExit;
procedure RestoreWindowBounds;
function MakeGlobalSettings: TGlobalSettings;
function MakeBandSettings: TBandSettings;
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
procedure BtnBandClick(Sender: TObject);
procedure ApplyModeFilter;
procedure BtnModeClick(Sender: TObject);
procedure BtnFilterClick(Sender: TObject);
procedure BtnCTunClick(Sender: TObject);
procedure UpdateFilterButtons;
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
procedure PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure DrawRuler;
procedure PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
procedure PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
procedure BtnVfoSwapClick(Sender: TObject);
procedure BtnVfoACopyBClick(Sender: TObject);
procedure BtnVfoBCopyAClick(Sender: TObject);
procedure BtnMOXClick(Sender: TObject);
procedure BtnMuteClick(Sender: TObject);
procedure UpdateNRButton;
procedure UpdateNBButton;
procedure UpdateSNBButton;
procedure UpdateANFButton;
procedure ApplyNoiseFilterButtonsToDSP;
procedure BtnNRClick(Sender: TObject);
procedure BtnNBClick(Sender: TObject);
procedure BtnSNBClick(Sender: TObject);
procedure BtnANFClick(Sender: TObject);
procedure BtnSpanClick(Sender: TObject);
// Веб-интерфейс: callbacks от TWebServer
procedure WebOnFreq(Hz: Double);
procedure WebOnMode(Mode: Integer);
procedure WebOnFilter(BW: Integer);
procedure WebOnAGC(Mode: Integer);
procedure WebOnAGCTop(DB: Integer);
procedure WebOnBand(Idx: Integer);
procedure WebOnSpan(Hz: Integer);
procedure WebOnVolume(V: Integer);
procedure WebOnWfAGC(On_: Boolean);
procedure WebOnWfNF(On_: Boolean);
procedure WebOnRun(On_: Boolean);
procedure WebOnMute(On_: Boolean);
procedure WebOnCtun(On_: Boolean);
procedure WebOnNR(Mode: Integer);
procedure WebOnNB(Mode: Integer);
procedure WebOnSNB(On_: Boolean);
procedure WebOnANF(On_: Boolean);
// Synchronize-обёртки (выполняются в UI-потоке)
procedure SyncWebFreq;
procedure SyncWebMode;
procedure SyncWebFilter;
procedure SyncWebAGC;
procedure SyncWebAGCTop;
procedure SyncWebBand;
procedure SyncWebSpan;
procedure SyncWebVolume;
procedure SyncWebWfAGC;
procedure SyncWebWfNF;
procedure SyncWebRun;
procedure SyncWebMute;
procedure SyncWebCtun;
procedure SyncWebNR;
procedure SyncWebNB;
procedure SyncWebSNB;
procedure SyncWebANF;
procedure BtnWfAGCClick(Sender: TObject);
procedure BtnWfNFClick(Sender: TObject);
procedure BtnHidePanelClick(Sender: TObject);
procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject);
procedure TrkVolumeChange(Sender: TObject);
procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject);
procedure PbFwdPowerPaint(Sender: TObject);
procedure PbSWRPaint(Sender: TObject);
procedure PbSpectrumPaint(Sender: TObject);
procedure PbRulerPaint(Sender: TObject);
procedure PbWaterfallPaint(Sender: TObject);
procedure MeterTimerTick(Sender: TObject);
procedure SpectrumTimerTick(Sender: TObject);
procedure RightPanelResize(Sender: TObject);
procedure AfterShowTick(Sender: TObject);
published
// Обработчики событий формы — должны быть в published для RTTI/LFM
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint;
var Handled: Boolean);
end;
var
MainForm: TMainForm;
implementation
{$R *.lfm}
// ===========================================================================
// Общая функция декодирования типа платы — используется везде
// ===========================================================================
function BoardTypeName(BoardType: Integer): string;
begin
case BoardType of
1: Result := 'HERMES (ANAN-10/100)';
2: Result := 'HERMES-E (ANAN-10E/100B)';
3: Result := 'ANGELIA (ANAN-100D)';
4: Result := 'ORION (ANAN-200D)';
5: Result := 'ORION MkII (ANAN-7000/8000)';
6: Result := 'HERMES-LITE 2';
10: Result := 'SATURN (G2)';
else Result := Format('Unknown Board #%d', [BoardType]);
end;
end;
// ===========================================================================
// Sync helpers
// ===========================================================================
constructor TDeviceFoundSync.Create(AForm: TObject;
const D: THPSDRDevice; const E: string);
begin
inherited Create;
FForm := AForm;
FDev := D;
FEntry := E;
end;
procedure TDeviceFoundSync.Execute;
begin
TMainForm(FForm).DoAddDevice(FDev, FEntry);
end;
constructor TStatusUISync.Create(AForm: TObject;
FW, SW, SV: Double; PLL: Boolean);
begin
inherited Create;
FForm := AForm;
FFwdW := FW;
FSWRV := SW;
FSupplyV := SV;
FPLLLock := PLL;
end;
procedure TStatusUISync.Execute;
begin
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock);
end;
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
begin
inherited Create;
FForm := AForm;
FDDCIdx := Idx;
FSeq := Seq;
end;
procedure TDDCSeqSync.Execute;
begin
TMainForm(FForm).DoUpdateDDCSeqOrNoDevice(FDDCIdx, FSeq);
end;
// ===========================================================================
// FormCreate / FormDestroy
// ===========================================================================
// ===========================================================================
// Settings helpers
// ===========================================================================
procedure TMainForm.RestoreWindowBounds;
var
L, T, Wd, Ht: Integer;
begin
FSettings.LoadWindowBounds(L, T, Wd, Ht);
if (Wd > 400) and (Ht > 300) then
begin
Left := L;
Top := T;
Width := Wd;
Height := Ht;
end;
end;
function TMainForm.MakeBandSettings: TBandSettings;
begin
Result.VfoA := FVfoA;
Result.VfoB := FVfoB;
Result.Mode := FMode;
Result.FilterIdx := FFilter;
Result.FilterBW := FFilterBW;
Result.AGCMode := FAGCMode;
Result.AGCTop := FAGCTop;
Result.CTun := FCTun;
Result.SpanHz := FSpanHz;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
function TMainForm.MakeGlobalSettings: TGlobalSettings;
begin
Result.Volume := FVolume;
Result.DriveLevel := FDriveLevel;
Result.ActiveVfo := FActiveVfo;
Result.NRMode := BtnNR.Tag;
Result.NBMode := BtnNB.Tag;
Result.SNBEnabled := BtnSNB.Tag <> 0;
Result.ANFEnabled := BtnANF.Tag <> 0;
Result.AGCSlope := 0;
Result.AGCHangThreshold := 100;
Result.WfAGCEnabled := FWfAGCEnabled;
Result.WfNFEnabled := FWfNFEnabled;
Result.LastBand := FCurrentBand;
Result.SampleRate := FSampleRate;
end;
procedure TMainForm.SaveCurrentBand;
begin
if not FDevConnected then Exit;
FBandCache[FCurrentBand] := MakeBandSettings;
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
end;
procedure TMainForm.RestoreBand(BandIdx: Integer);
var
B: TBandSettings;
i: Integer;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
B := FBandCache[BandIdx];
// --- Кнопки диапазонов ---
for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], i = BandIdx);
// Сбрасываем waterfall AGC/NF при смене диапазона
FWfHigh := -50.0;
FWfLow := -120.0;
// Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон
ResetWfAvgBuf;
// --- Режим: сначала устанавливаем FMode, потом кнопки и WDSP ---
FMode := B.Mode;
for i := 0 to MODE_COUNT - 1 do
StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then
FDSPEngine.SetMode(FMode);
// --- Фильтр ---
FFilter := B.FilterIdx;
FFilterBW := B.FilterBW;
UpdateFilterButtons; // обновляет кнопки фильтра
ApplyModeFilter; // применяет Lo/Hi в WDSP с учётом нового FMode
// --- AGC ---
FAGCMode := B.AGCMode;
FAGCTop := B.AGCTop;
for i := 0 to 4 do
StyleButton(BtnAGCMode[i], i = FAGCMode);
TrkAGC.Position := FAGCTop;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
// --- CTUN ---
FCTun := B.CTun;
StyleButton(BtnCTun, FCTun);
if FWDSPReady and not FCTun then
FDSPEngine.SetShift(0.0);
// --- Sample Rate / Span ---
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
// Обновляем только FSpanHz для отображения (берём текущий FSampleRate)
FSpanHz := FSampleRate;
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
// --- VFO A: центрируем на новой частоте, сбрасываем shift ---
FCenterFreq := B.VfoA; // DDC = центр диапазона
FVfoB := B.VfoB;
FreqDispB.Frequency := Round(FVfoB);
// ApplyVfoA обновит сеть, FreqDisp, shift, перерисует
ApplyVfoA(Round(B.VfoA));
end;
procedure TMainForm.SaveAllAndExit;
begin
if FDevConnected then
begin
SaveCurrentBand;
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
FSettings.Save;
end;
end;
procedure TMainForm.FormCreate(Sender: TObject);
var
bi_, StartupRate: Integer;
StartupVfoA, StartupVfoB: Double;
begin
FVfoA := 14200000;
FVfoB := 7100000;
FActiveVfo := 0;
FMode := 1;
FFilter := 5;
FAGCMode := 1; // MEDIUM
FAGCTop := 90; // 90 dBm
// default: зависит от режима, будет сброшен в UpdateFilterButtons
FCTun := False;
FFilterBW := 2700;
FSpecDrag := False;
FDriveLevel := 100;
FRunning := False;
FTransmitting := False;
FMuted := False;
FVolume := 70;
FCenterFreq := FVfoA;
FSpanHz := 192000;
FSampleRate := 192000;
FCurrentBand := 5; // 20m по умолчанию
// Waterfall normalization
FWfHigh := -50.0; // начальная верхняя граница dBm
// WF avg buffer — инициализируем значением шумового пола
// FWfAvgBuf инициализируется в ResetSpectrumBuf
FWfLow := -120.0; // начальная нижняя граница dBm
FWfAGCEnabled := False;
FWfNFEnabled := False;
FDevConnected := False;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями
for bi_ := 0 to CFG_BAND_COUNT - 1 do
TSettingsManager.DefaultBand(bi_, FBandCache[bi_]);
FCurrentBand := 5;
// Загружаем JSON настройки
FSettings := TSettingsManager.Create;
FSettings.Load;
if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then
begin
FVfoA := StartupVfoA;
FVfoB := StartupVfoB;
FCenterFreq := FVfoA;
if StartupRate > 0 then
begin
FSampleRate := StartupRate;
FSpanHz := StartupRate;
end;
end;
// Восстанавливаем размер/позицию окна при старте (до подключения устройства)
RestoreWindowBounds;
// S-метр позиционируем после рестора размера окна
// (будет пересчитан в первом тике SpectrumTimerTick)
FLastSMeter := -130;
// Создаём веб-сервер (логин: admin, пароль: hpsdr — менять здесь)
FWebServer := TWebServer.Create('admin', 'hpsdr');
FWebServer.OnFreq := WebOnFreq;
FWebServer.OnMode := WebOnMode;
FWebServer.OnFilter := WebOnFilter;
FWebServer.OnAGC := WebOnAGC;
FWebServer.OnAGCTop := WebOnAGCTop;
FWebServer.OnBand := WebOnBand;
FWebServer.OnSpan := WebOnSpan;
FWebServer.OnVolume := WebOnVolume;
FWebServer.OnWfAGC := WebOnWfAGC;
FWebServer.OnWfNF := WebOnWfNF;
FWebServer.OnRun := WebOnRun;
FWebServer.OnMute := WebOnMute;
FWebServer.OnCtun := WebOnCtun;
FWebServer.OnNR := WebOnNR;
FWebServer.OnNB := WebOnNB;
FWebServer.OnSNB := WebOnSNB;
FWebServer.OnANF := WebOnANF;
FWebServer.Start;
FSMeterPeak := -130;
FSMeterMin := -130;
FSMeterAvg := -130;
FRXPacketCount := 0;
FActiveDDC := 0;
FLastDDCSeq := 0;
FLastDDCIndex := 0;
FLastFwdW := 0;
FLastSWR := 1;
FDeviceCount := 0;
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
FVfoOverlay := TVfoOverlay.Create(Self);
FVfoOverlay.OnSelect := OnModeFilterSelect;
FVfoOverlay.Visible := False;
FPanelHidden := False;
FSpectrumBitmap := TBitmap.Create;
FWaterfallBitmap := TBitmap.Create;
FWaterfallTemp := TBitmap.Create;
FSpecGradImg := nil;
FSpecGradBmp := TBitmap.Create;
FWfIntfImg := nil;
FWfBitmap := TBitmap.Create;
FWfBitmapW := 0;
FWfBitmapH := 0;
FGridBitmap := TBitmap.Create;
FGridBitmapW := 0;
FGridBitmapH := 0;
FSpPtsLen := 0;
FRulerBitmap := TBitmap.Create;
FRulerLastFreq := -1.0;
FRulerLastVfo := -1.0;
ResetSpectrumBuf;
FNetwork := THPSDRNetwork.Create;
FNetwork.OnDeviceFound := OnDeviceFound;
FNetwork.OnHPStatus := OnHPStatusCB;
FNetwork.OnDDCIQ := OnDDCIQCB;
FNetwork.OnMicPacket := OnMicPacketCB;
BuildUI;
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
ResizeSMeter;
ApplyDarkTheme;
UpdateVfoDisplay;
UpdateFilterButtons;
// DSP Engine — создаём объект, Open вызовется при нажатии START
// (FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек)
FWDSPReady := False;
// BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz.
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
// Audio output — создаём объект сейчас, открываем после показа формы
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
FAudioOut := TAudioOutput.Create(48000);
FMeterTimer := TTimer.Create(Self);
FMeterTimer.Interval := 100;
FMeterTimer.OnTimer := MeterTimerTick;
FMeterTimer.Enabled := True;
FSpectrumTimer := TTimer.Create(Self);
FSpectrumTimer.Interval := 50;
FSpectrumTimer.OnTimer := SpectrumTimerTick;
FSpectrumTimer.Enabled := True;
// Однократный таймер — инициализация аудио после показа формы
FAfterShowTimer := TTimer.Create(Self);
FAfterShowTimer.Interval := 200;
FAfterShowTimer.OnTimer := AfterShowTick;
FAfterShowTimer.Enabled := True;
OnMouseWheel := FormMouseWheel;
// Проверяем и при необходимости добавляем исключение в Windows Firewall
// (UDP входящий трафик для HPSDR Protocol 2)
{$IFDEF WINDOWS}
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
{$ENDIF}
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
FMeterTimer.Enabled := False;
FSpectrumTimer.Enabled := False;
if FNetwork.Running then
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
FNetwork.Disconnect;
FNetwork.Free;
FAudioOut.Close;
FAudioOut.Free;
FDSPEngine.Close;
FDSPEngine.Free;
FSpectrumBitmap.Free;
FWaterfallBitmap.Free;
FWaterfallTemp.Free;
FRulerBitmap.Free;
FWfBitmap.Free;
FGridBitmap.Free;
FreeAndNil(FSpecGradImg);
FSpecGradBmp.Free;
FreeAndNil(FWfIntfImg);
// Сохраняем настройки при закрытии
if FDevConnected then
begin
SaveCurrentBand;
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
end;
// Размер окна сохраняем всегда (не зависит от подключения)
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
FSettings.SaveWindowBounds(Left, Top, Width, Height);
FSettings.Save;
FSettings.Free;
FWebServer.Stop;
FWebServer.Free;
end;
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
// Останавливаем трансивер как при нажатии STOP
if FNetwork.Connected then
begin
if FRunning then
begin
FNetwork.UpdateState(FCenterFreq, FCenterFreq, 0, False, True, True);
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
FRunning := False;
FTransmitting := False;
if FWDSPReady then FDSPEngine.SetTXRun(False);
end;
FNetwork.Disconnect;
end;
CloseAction := caFree;
end;
// ===========================================================================
// Build UI (без анонимных процедур и inline var)
// ===========================================================================
procedure TMainForm.BuildUI;
const
BTN_H = 26;
BTN_SM = 24;
LEFT_W = 232;
var
i, X, Y, W: Integer;
B: TFlatButton;
function MakeBtn(Parent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton;
begin
Result := TFlatButton.Create(Self);
Result.Parent := Parent;
Result.Caption := Cap;
Result.Left := ALeft;
Result.Top := ATop;
Result.Width := AW;
Result.Height := AH;
Result.OnClick := Handler;
StyleButton(Result, False);
end;
procedure MakeLbl(Parent: TWinControl; const Cap: string; ALeft, ATop: Integer);
var
L: TLabel;
begin
L := TLabel.Create(Self);
L.Parent := Parent;
L.Caption := Cap;
L.Left := ALeft;
L.Top := ATop;
L.Font.Color := CLR_TEXTDIM;
L.Font.Name := 'Courier New';
L.Font.Size := 7;
end;
begin
// ---- Toolbar ----
PanelToolbar := TPanel.Create(Self);
PanelToolbar.Parent := Self;
PanelToolbar.Align := alTop;
PanelToolbar.Height := 36;
PanelToolbar.BevelOuter := bvNone;
X := 4;
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
// ---- Status bar ----
StatusBar1 := TStatusBar.Create(Self);
StatusBar1.Parent := Self;
StatusBar1.Align := alBottom;
StatusBar1.SimplePanel := False;
with StatusBar1.Panels do
begin
Add; Items[0].Width := 150; Items[0].Text := 'IP: --';
Add; Items[1].Width := 120; Items[1].Text := 'Board: --';
Add; Items[2].Width := 320; Items[2].Text := 'Supply: -- | FWD: -- | SWR: --';
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
end;
// ---- Left panel ----
PanelLeft := TPanel.Create(Self);
PanelLeft.Parent := Self;
PanelLeft.Align := alLeft;
PanelLeft.Width := LEFT_W;
PanelLeft.BevelOuter := bvNone;
Y := 2;
// VFO A
PanelVfoA := TPanel.Create(Self);
PanelVfoA.Parent := PanelLeft;
PanelVfoA.SetBounds(0, Y, LEFT_W, 56);
PanelVfoA.BevelOuter := bvNone;
LblVfoALabel := TLabel.Create(Self);
LblVfoALabel.Parent := PanelVfoA;
LblVfoALabel.Caption := 'VFO-A';
LblVfoALabel.Left := 4; LblVfoALabel.Top := 2;
LblVfoALabel.Font.Name := 'Courier New'; LblVfoALabel.Font.Size := 7;
LblVfoALabel.Font.Color := CLR_TEXTDIM;
FreqDispA := TFreqDisplay.Create(Self);
FreqDispA.Parent := PanelVfoA;
FreqDispA.SetBounds(0, 14, LEFT_W, 36);
FreqDispA.FontSize := 20;
FreqDispA.FontName := 'Courier New';
FreqDispA.Frequency := Round(FVfoA);
FreqDispA.ColorNormal := CLR_FREQ;
FreqDispA.ColorHover := TColor($0040DDFF);
FreqDispA.ColorDim := CLR_TEXTDIM;
FreqDispA.OnChange := FreqDispAChanged;
Inc(Y, 58);
// VFO B
PanelVfoB := TPanel.Create(Self);
PanelVfoB.Parent := PanelLeft;
PanelVfoB.SetBounds(0, Y, LEFT_W, 46);
PanelVfoB.BevelOuter := bvNone;
LblVfoBLabel := TLabel.Create(Self);
LblVfoBLabel.Parent := PanelVfoB;
LblVfoBLabel.Caption := 'VFO-B';
LblVfoBLabel.Left := 4; LblVfoBLabel.Top := 2;
LblVfoBLabel.Font.Name := 'Courier New'; LblVfoBLabel.Font.Size := 7;
LblVfoBLabel.Font.Color := CLR_TEXTDIM;
FreqDispB := TFreqDisplay.Create(Self);
FreqDispB.Parent := PanelVfoB;
FreqDispB.SetBounds(0, 14, LEFT_W, 28);
FreqDispB.FontSize := 14;
FreqDispB.FontName := 'Courier New';
FreqDispB.Frequency := Round(FVfoB);
FreqDispB.ColorNormal := CLR_FREQ_DIM;
FreqDispB.ColorHover := TColor($0040DDFF);
FreqDispB.ColorDim := CLR_TEXTDIM;
FreqDispB.OnChange := FreqDispBChanged;
Inc(Y, 50);
// VFO buttons
PanelVfoButtons := TPanel.Create(Self);
PanelVfoButtons.Parent := PanelLeft;
PanelVfoButtons.SetBounds(0, Y, LEFT_W, BTN_H + 4);
PanelVfoButtons.BevelOuter := bvNone;
BtnVfoSwap := MakeBtn(PanelVfoButtons, 'A<>B', 2, 2, 72, BTN_H, BtnVfoSwapClick);
BtnVfoACopyB := MakeBtn(PanelVfoButtons, 'A>B', 78, 2, 72, BTN_H, BtnVfoACopyBClick);
BtnVfoBCopyA := MakeBtn(PanelVfoButtons, 'B>A', 154, 2, 72, BTN_H, BtnVfoBCopyAClick);
Inc(Y, BTN_H + 8);
// Band selector
PanelBands := TPanel.Create(Self);
PanelBands.Parent := PanelLeft;
PanelBands.SetBounds(0, Y, LEFT_W, 74);
PanelBands.BevelOuter := bvNone;
MakeLbl(PanelBands, 'BAND', 4, 2);
W := (LEFT_W - 6) div 6;
for i := 0 to BAND_COUNT - 1 do
begin
B := MakeBtn(PanelBands, BAND_NAMES[i],
2 + (i mod 6) * W,
16 + (i div 6) * 27,
W - 2, BTN_SM, BtnBandClick);
B.Tag := i;
BtnBand[i] := B;
end;
Inc(Y, 76);
// Mode selector
PanelMode := TPanel.Create(Self);
PanelMode.Parent := PanelLeft;
PanelMode.SetBounds(0, Y, LEFT_W, 74);
PanelMode.BevelOuter := bvNone;
MakeLbl(PanelMode, 'MODE', 4, 2);
W := (LEFT_W - 6) div 4;
for i := 0 to MODE_COUNT - 1 do
begin
B := MakeBtn(PanelMode, MODE_NAMES[i],
2 + (i mod 4) * W,
16 + (i div 4) * 27,
W - 2, BTN_SM, BtnModeClick);
B.Tag := i;
BtnMode[i] := B;
StyleButton(B, i = FMode);
end;
Inc(Y, 76);
// Filter selector (10 кнопок в 2 ряда по 5)
PanelFilter := TPanel.Create(Self);
PanelFilter.Parent := PanelLeft;
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
PanelFilter.BevelOuter := bvNone;
MakeLbl(PanelFilter, 'FILTER', 4, 2);
W := (LEFT_W - 6) div 5;
for i := 0 to FILT_COUNT - 1 do
begin
B := MakeBtn(PanelFilter, '---',
2 + (i mod 5) * W,
16 + (i div 5) * 27,
W - 2, BTN_SM, BtnFilterClick);
B.Tag := i;
BtnFilter[i] := B;
end;
Inc(Y, 76);
// CTUN button
BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick);
BtnCTun.Tag := 0;
StyleButton(BtnCTun, FCTun);
Inc(Y, BTN_H + 4);
// RX controls
PanelRX := TPanel.Create(Self);
PanelRX.Parent := PanelLeft;
PanelRX.SetBounds(0, Y, LEFT_W, 120);
PanelRX.BevelOuter := bvNone;
// AGC mode — 5 кнопок в ряд
MakeLbl(PanelRX, 'AGC', 4, 4);
W := (LEFT_W - 10) div 5;
BtnAGCMode[0] := MakeBtn(PanelRX,'FAST', 2, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[0].Tag:=0;
BtnAGCMode[1] := MakeBtn(PanelRX,'MED', W+4, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[1].Tag:=1;
BtnAGCMode[2] := MakeBtn(PanelRX,'SLOW', 2*W+6, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[2].Tag:=2;
BtnAGCMode[3] := MakeBtn(PanelRX,'LONG', 3*W+8, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[3].Tag:=3;
BtnAGCMode[4] := MakeBtn(PanelRX,'OFF', 4*W+10, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[4].Tag:=4;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
// AGC level slider
MakeLbl(PanelRX, 'THRESH', 4, 50);
TrkAGC := TTrackBar.Create(Self);
TrkAGC.Parent := PanelRX; TrkAGC.Left := 56; TrkAGC.Top := 46;
TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 20;
TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → 20..-120 dBm
TrkAGC.Position := FAGCTop;
TrkAGC.TickStyle := tsNone; TrkAGC.Reversed := True;
TrkAGC.OnChange := TrkAGCChange;
LblAGCTop := TLabel.Create(Self);
LblAGCTop.Parent := PanelRX;
LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 50;
LblAGCTop.Caption := Format('-%ddB', [FAGCTop]);
LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New';
LblAGCTop.Font.Size := 7;
// VOL
MakeLbl(PanelRX, 'VOL', 4, 74);
TrkVolume := TTrackBar.Create(Self);
TrkVolume.Parent := PanelRX; TrkVolume.Left := 34; TrkVolume.Top := 70;
TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 20;
TrkVolume.Min := 0; TrkVolume.Max := 100;
TrkVolume.Position := FVolume; TrkVolume.TickStyle := tsNone;
TrkVolume.OnChange := TrkVolumeChange;
// NR NB SNB ANF MUTE
W := (LEFT_W - 10) div 5;
BtnNR := MakeBtn(PanelRX, 'NR', 2, 96, W, BTN_SM, BtnNRClick);
BtnNB := MakeBtn(PanelRX, 'NB', W+4, 96, W, BTN_SM, BtnNBClick);
BtnSNB := MakeBtn(PanelRX, 'SNB', 2*W+6, 96, W, BTN_SM, BtnSNBClick);
BtnANF := MakeBtn(PanelRX, 'ANF', 3*W+8, 96, W, BTN_SM, BtnANFClick);
BtnMute:= MakeBtn(PanelRX, 'MUTE', 4*W+10, 96, W, BTN_SM, BtnMuteClick);
Inc(Y, 124);
// S-Meter
Inc(Y, 52);
// TX controls
PanelTX := TPanel.Create(Self);
PanelTX.Parent := PanelLeft;
PanelTX.SetBounds(0, Y, LEFT_W, 104);
PanelTX.BevelOuter := bvNone;
MakeLbl(PanelTX, 'TX', 4, 2);
LblDrv := TLabel.Create(Self);
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 17;
LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM;
LblDrv.Font.Name := 'Courier New'; LblDrv.Font.Size := 7;
TrkDrive := TTrackBar.Create(Self);
TrkDrive.Parent := PanelTX; TrkDrive.Left := 34; TrkDrive.Top := 14;
TrkDrive.Width := 150; TrkDrive.Height := 22;
TrkDrive.Min := 0; TrkDrive.Max := 100; TrkDrive.Position := 50;
TrkDrive.TickStyle := tsNone; TrkDrive.OnChange := TrkDriveChange;
BtnMOX := MakeBtn(PanelTX, 'MOX', 2, 38, 70, 32, BtnMOXClick);
BtnMOX.Font.Size := 12; BtnMOX.Font.Bold := True;
LblFwdPwr := TLabel.Create(Self);
LblFwdPwr.Parent := PanelTX; LblFwdPwr.Left := 80; LblFwdPwr.Top := 42;
LblFwdPwr.Caption := '0W'; LblFwdPwr.Font.Color := CLR_METER_ON;
LblFwdPwr.Font.Name := 'Courier New'; LblFwdPwr.Font.Size := 8;
LblSWRVal := TLabel.Create(Self);
LblSWRVal.Parent := PanelTX; LblSWRVal.Left := 140; LblSWRVal.Top := 42;
LblSWRVal.Caption := 'SWR:1.0'; LblSWRVal.Font.Color := CLR_TEXTDIM;
LblSWRVal.Font.Name := 'Courier New'; LblSWRVal.Font.Size := 8;
MakeLbl(PanelTX, 'PWR', 4, 76);
PbFwdPower := TPaintBox.Create(Self);
PbFwdPower.Parent := PanelTX;
PbFwdPower.SetBounds(34, 74, LEFT_W - 38, 12);
PbFwdPower.OnPaint := PbFwdPowerPaint;
MakeLbl(PanelTX, 'SWR', 4, 92);
PbSWR := TPaintBox.Create(Self);
PbSWR.Parent := PanelTX;
PbSWR.SetBounds(34, 90, LEFT_W - 38, 12);
PbSWR.OnPaint := PbSWRPaint;
// ---- Right panel ----
PanelRight := TPanel.Create(Self);
PanelRight.Parent := Self;
PanelRight.Align := alClient;
PanelRight.BevelOuter := bvNone;
PanelRight.OnResize := RightPanelResize;
PanelSpanButtons := TPanel.Create(Self);
PanelSpanButtons.Parent := PanelRight;
PanelSpanButtons.Align := alTop;
PanelSpanButtons.Height := 30;
PanelSpanButtons.BevelOuter := bvNone;
LblSpan := TLabel.Create(Self);
LblSpan.Parent := PanelSpanButtons; LblSpan.Left := 4; LblSpan.Top := 8;
LblSpan.Caption := 'SPAN:'; LblSpan.Font.Color := CLR_TEXTDIM;
LblSpan.Font.Name := 'Courier New'; LblSpan.Font.Size := 7;
BtnSpan48k := MakeBtn(PanelSpanButtons, '48k', 50, 3, 46, BTN_SM, BtnSpanClick);
BtnSpan96k := MakeBtn(PanelSpanButtons, '96k', 99, 3, 46, BTN_SM, BtnSpanClick);
BtnSpan192k := MakeBtn(PanelSpanButtons, '192k', 148, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan384k := MakeBtn(PanelSpanButtons, '384k', 203, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan768k := MakeBtn(PanelSpanButtons, '768k', 258, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan1536k := MakeBtn(PanelSpanButtons, '1536k', 313, 3, 58, BTN_SM, BtnSpanClick);
BtnSpan48k.Tag := 48000;
BtnSpan96k.Tag := 96000;
BtnSpan192k.Tag := 192000;
BtnSpan384k.Tag := 384000;
BtnSpan768k.Tag := 768000;
BtnSpan1536k.Tag := 1536000;
StyleSpanButton(BtnSpan48k, False);
StyleSpanButton(BtnSpan96k, False);
StyleSpanButton(BtnSpan192k, True);
StyleSpanButton(BtnSpan384k, False);
StyleSpanButton(BtnSpan768k, False);
StyleSpanButton(BtnSpan1536k, False);
// Waterfall AGC + Noise Floor кнопки справа в той же панели
BtnWfAGC := MakeBtn(PanelSpanButtons, 'WF AGC', 379, 3, 62, BTN_SM, BtnWfAGCClick);
BtnWfNF := MakeBtn(PanelSpanButtons, 'WF NF', 445, 3, 58, BTN_SM, BtnWfNFClick);
BtnHidePanel := MakeBtn(PanelSpanButtons, '◀ HIDE', 510, 3, 64, BTN_SM, BtnHidePanelClick);
// Большой S-метр — на главной форме поверх двух панелей
// S-метр: Parent=Self, перекрывает Toolbar+SpanButtons вертикально
PanelSMeterRight := TPanel.Create(Self);
PanelSMeterRight.Parent := Self;
PanelSMeterRight.BevelOuter := bvNone;
PanelSMeterRight.Color := CLR_PANEL;
PbSMeterRight := TPaintBox.Create(Self);
PbSMeterRight.Parent := PanelSMeterRight;
PbSMeterRight.Align := alClient;
PbSMeterRight.OnPaint := PbSMeterRightPaint;
PbSMeterRight.Color := CLR_PANEL;
PbSpectrum := TPaintBox.Create(Self);
PbSpectrum.Parent := PanelRight;
PbSpectrum.OnPaint := PbSpectrumPaint;
PbSpectrum.OnMouseDown := PbSpectrumMouseDown;
PbSpectrum.OnDblClick := PbSpectrumDblClick;
PbSpectrum.OnMouseMove := PbSpectrumMouseMove;
PbSpectrum.OnMouseUp := PbSpectrumMouseUp;
PbRuler := TPaintBox.Create(Self);
PbRuler.Parent := PanelRight;
PbRuler.OnPaint := PbRulerPaint;
PbRuler.Cursor := crDefault;
PbWaterfall := TPaintBox.Create(Self);
PbWaterfall.Parent := PanelRight;
PbWaterfall.OnPaint := PbWaterfallPaint;
PbWaterfall.OnMouseDown := PbWaterfallMouseDown;
PbWaterfall.OnMouseMove := PbWaterfallMouseMove;
PbWaterfall.OnMouseUp := PbWaterfallMouseUp;
// SPAN кнопки как оверлей на спектре (в стиле web)
BtnSpan48k.Parent := PanelRight;
BtnSpan96k.Parent := PanelRight;
BtnSpan192k.Parent := PanelRight;
BtnSpan384k.Parent := PanelRight;
BtnSpan768k.Parent := PanelRight;
BtnSpan1536k.Parent := PanelRight;
LblSpan.Visible := False;
PositionSpanOverlayButtons;
// Initial layout
ResizeSpectrumPanels;
end;
// ===========================================================================
// Resize handler (normal method instead of anonymous procedure)
// ===========================================================================
procedure TMainForm.RightPanelResize(Sender: TObject);
begin
ResizeSpectrumPanels;
end;
procedure TMainForm.ResizeSMeter;
// PanelSMeterRight на главной форме (Parent=Self).
// По Y: от самого верха формы до низа PanelSpanButtons (Toolbar+Span = 66px).
// По X: 40% ширины PanelRight, прижат к правому краю.
const
LEFT_PANEL_W = 232; // ширина левой панели (LEFT_W)
MARGIN = 3;
var
FW, FH: Integer;
RightW: Integer; // ширина PanelRight
SMW: Integer; // ширина S-метра
SMLeft: Integer;
SMTop: Integer;
SMBot: Integer;
begin
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or
(PanelSpanButtons = nil) then Exit;
FW := ClientWidth;
// Ширина зоны спектра (PanelRight)
RightW := FW - LEFT_PANEL_W;
if RightW < 200 then Exit;
// Ширина S-метра = 40% от PanelRight
SMW := Round(RightW * 0.40);
// X: прижат к правому краю формы
SMLeft := FW - SMW - MARGIN;
// Y: от верха формы+margin до низа PanelSpanButtons-margin
SMTop := MARGIN;
SMBot := PanelToolbar.Height + PanelSpanButtons.Height - MARGIN;
PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop);
PanelSMeterRight.BringToFront;
end;
procedure TMainForm.ResizeSpectrumPanels;
var
RW, RH, SH, WH, TopOff: Integer;
begin
if PanelRight = nil then Exit;
RW := PanelRight.ClientWidth;
RH := PanelRight.ClientHeight;
TopOff := PanelSpanButtons.Height;
// S-метр позиционируется отдельно в ResizeSMeter
ResizeSMeter;
RH := RH - TopOff;
if RH < 10 then Exit;
SH := RH * 4 div 10;
WH := RH - SH - 18;
if WH < 10 then WH := 10;
PbSpectrum.SetBounds(0, TopOff, RW, SH);
PbRuler.SetBounds (0, TopOff + SH, RW, 18);
PbWaterfall.SetBounds(0, TopOff + SH + 18, RW, WH);
PositionSpanOverlayButtons;
// Обновляем переменные размеров
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := WH;
// Пересоздаём bitmap точно под новый размер
if (FSpectrumBitmap <> nil) and (RW > 0) and (SH > 0) then
begin
FSpectrumBitmap.SetSize(RW, SH);
FSpectrumBitmap.Canvas.Brush.Color := CLR_BG;
FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, RW, SH));
FreeAndNil(FSpecGradImg);
// Сбрасываем кэш фона/сетки и кэш точек кривой
FGridBitmapW := 0;
FGridBitmapH := 0;
FSpPtsLen := 0;
end;
if (FWaterfallBitmap <> nil) and (RW > 0) and (WH > 0) then
begin
FWaterfallBitmap.SetSize(RW, WH);
FWaterfallBitmap.Canvas.Brush.Color := clBlack;
FWaterfallBitmap.Canvas.FillRect(Rect(0, 0, RW, WH));
// Сбрасываем буфер пикселей водопада
FWfBitmapW := 0;
FWfBitmapH := 0;
SetLength(FWfPixels, 0);
end;
if (FRulerBitmap <> nil) and (RW > 0) then
begin
FRulerBitmap.SetSize(RW, 18);
FRulerLastFreq := -1.0;
FRulerLastVfo := -1.0;
end;
// Немедленно перерисовываем с новыми размерами
if RW > 0 then
begin
DrawSpectrum;
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
PbWaterfall.Invalidate;
end;
end;
// ===========================================================================
// Dark Theme
// ===========================================================================
procedure TMainForm.ApplyDarkTheme;
procedure DP(P: TPanel);
begin
P.Color := CLR_PANEL; P.Font.Color := CLR_TEXT;
end;
begin
Color := CLR_BG; Font.Color := CLR_TEXT;
DP(PanelToolbar); DP(PanelLeft);
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
DP(PanelBands); DP(PanelMode); DP(PanelFilter);
DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons);
PbSpectrum.Color := CLR_BG;
PbWaterfall.Color := CLR_BG;
if PanelSMeterRight <> nil then PanelSMeterRight.Color := CLR_PANEL;
if PbSMeterRight <> nil then PbSMeterRight.Color := CLR_PANEL;
PbFwdPower.Color := CLR_BG;
PbSWR.Color := CLR_BG;
end;
procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean);
begin
B.Active := Active;
B.ClrNorm := CLR_INACTIVE;
B.ClrActive := CLR_ACTIVE;
B.ClrHot := TColor($00323232);
B.ClrBorder := TColor($00505050);
B.ClrText := CLR_TEXT;
B.ClrTextAct := TColor($0000FF88);
B.Font.Name := 'Courier New';
B.Font.Size := 8;
end;
procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean);
begin
B.Active := Active;
B.ClrNorm := TColor($0013191B);
B.ClrActive := TColor($001E3C1E);
B.ClrHot := TColor($001E2D1E);
B.ClrBorder := TColor($005A7A5A);
B.ClrText := TColor($0090B890);
B.ClrTextAct := TColor($00AAFFAA);
B.Font.Name := 'Courier New';
B.Font.Size := 7;
end;
procedure TMainForm.PositionSpanOverlayButtons;
const
BTN_TOP = 6;
GAP = 3;
W48 = 40;
W96 = 40;
W192 = 44;
W384 = 44;
W768 = 44;
W1536 = 50;
HBTN = 18;
var
X, NeedW, BaseX, BaseY: Integer;
begin
if (PbSpectrum = nil) or (BtnSpan48k = nil) then Exit;
NeedW := W48 + W96 + W192 + W384 + W768 + W1536 + GAP * 5 + 8;
if PbSpectrum.Width < NeedW then
begin
BtnSpan48k.Visible := False;
BtnSpan96k.Visible := False;
BtnSpan192k.Visible := False;
BtnSpan384k.Visible := False;
BtnSpan768k.Visible := False;
BtnSpan1536k.Visible := False;
Exit;
end;
BaseX := PbSpectrum.Left;
BaseY := PbSpectrum.Top;
X := BaseX + 56;
BtnSpan48k.SetBounds(X, BaseY + BTN_TOP, W48, HBTN); Inc(X, W48 + GAP);
BtnSpan96k.SetBounds(X, BaseY + BTN_TOP, W96, HBTN); Inc(X, W96 + GAP);
BtnSpan192k.SetBounds(X, BaseY + BTN_TOP, W192, HBTN); Inc(X, W192 + GAP);
BtnSpan384k.SetBounds(X, BaseY + BTN_TOP, W384, HBTN); Inc(X, W384 + GAP);
BtnSpan768k.SetBounds(X, BaseY + BTN_TOP, W768, HBTN); Inc(X, W768 + GAP);
BtnSpan1536k.SetBounds(X, BaseY + BTN_TOP, W1536, HBTN);
BtnSpan48k.Visible := True;
BtnSpan96k.Visible := True;
BtnSpan192k.Visible := True;
BtnSpan384k.Visible := True;
BtnSpan768k.Visible := True;
BtnSpan1536k.Visible := True;
BtnSpan48k.BringToFront;
BtnSpan96k.BringToFront;
BtnSpan192k.BringToFront;
BtnSpan384k.BringToFront;
BtnSpan768k.BringToFront;
BtnSpan1536k.BringToFront;
end;
// ===========================================================================
// VFO
// ===========================================================================
function TMainForm.FormatFreq(Hz: Double): string;
var
Mhz, KHz, Rest: Integer;
begin
Mhz := Trunc(Hz / 1000000);
KHz := Trunc((Hz - Mhz * 1000000) / 1000);
Rest := Trunc(Hz) mod 1000;
Result := Format('%3d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end;
procedure TMainForm.UpdateVfoDisplay;
begin
FreqDispA.Frequency := Round(FVfoA);
FreqDispB.Frequency := Round(FVfoB);
if FActiveVfo = 0 then
begin
FreqDispA.ColorNormal := CLR_FREQ; FreqDispA.FontSize := 20;
FreqDispB.ColorNormal := CLR_FREQ_DIM; FreqDispB.FontSize := 14;
FreqDispA.Height := 36;
FreqDispB.Height := 28;
end else begin
FreqDispA.ColorNormal := CLR_FREQ_DIM; FreqDispA.FontSize := 14;
FreqDispB.ColorNormal := CLR_FREQ; FreqDispB.FontSize := 20;
FreqDispA.Height := 28;
FreqDispB.Height := 36;
end;
// FCenterFreq обновляется через ApplyVfoA, здесь не трогаем
// При изменении частоты — обновляем HP пакет с новыми частотами и ALEX
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel,
FTransmitting, True, True);
FNetwork.SendFullHP;
end;
// WDSP: частота NCO задаётся через HPSDR железо (DDC), не через WDSP напрямую
end;
// ===========================================================================
// Helpers
// ===========================================================================
// Замена MulDiv (Windows API) — простая целочисленная пропорция
function TMainForm.ScaleX(X, Total, Width: Integer): Integer;
begin
if Total = 0 then Result := 0
else Result := Round(X / Total * Width);
end;
// ===========================================================================
// Drawing
// ===========================================================================
procedure TMainForm.DrawBarMeter(ACanvas: TCanvas; R: TRect;
Value, MaxVal: Double; BarColor: TColor);
var
Pct, BarW: Integer;
begin
ACanvas.Brush.Color := TColor($00080808);
ACanvas.FillRect(R);
if MaxVal > 0 then
Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2))
else
Pct := 0;
BarW := Pct;
ACanvas.Brush.Color := BarColor;
ACanvas.Pen.Color := BarColor;
ACanvas.FillRect(Rect(R.Left+1, R.Top+1, R.Left+1+BarW, R.Bottom-1));
ACanvas.Brush.Style := bsClear;
ACanvas.Pen.Color := CLR_BORDER;
ACanvas.Rectangle(R);
end;
procedure TMainForm.ResetWfAvgBuf;
var i: Integer;
begin
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
FWfHigh := -50.0;
FWfLow := -120.0;
end;
procedure TMainForm.ResetSpectrumBuf;
var i: Integer;
begin
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
FWfHigh := -50.0;
FWfLow := -120.0;
end;
procedure TMainForm.FillDemoSpectrum;
var
i: Integer;
FreqOff, Noise, Sig: Double;
begin
for i := 0 to FSpectrumWidth - 1 do
begin
if FSpectrumWidth > 1 then
FreqOff := (i / (FSpectrumWidth - 1) - 0.5) * FSpanHz
else
FreqOff := 0;
Noise := -110 + (Random - 0.5) * 6;
if Abs(FreqOff) < 2000 then
Sig := -50 - Abs(FreqOff) / 200
else
Sig := -999;
FSpectrumBuf[i mod 1024] := Max(Noise, Sig);
end;
end;
procedure TMainForm.DrawMarkerLine(C: TCanvas; W, H: Integer);
var
MX: Integer;
MarkerFreq: Double;
MarkerLbl: string;
begin
MX := Round(FMarkerX / 1000.0 * W);
if (MX < 0) or (MX >= W) then Exit;
MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz;
MarkerLbl := FormatFreq(Round(MarkerFreq));
C.Pen.Color := TColor($004444FF); // красный (BGR формат)
C.Pen.Width := 1;
C.Pen.Style := psSolid;
C.MoveTo(MX, 0);
C.LineTo(MX, H);
C.Font.Color := TColor($004444FF);
C.Font.Size := 7;
C.Font.Name := 'Courier New';
if MX + 4 + C.TextWidth(MarkerLbl) < W then
C.TextOut(MX + 4, 4, MarkerLbl)
else
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
end;
// Вспомогательная функция: BGRA32 top-to-bottom описание для TLazIntfImage.
// Заменяет Init_BPP32_B8G8R8A8_BIO_TTB которая не всегда доступна в старых Lazarus.
procedure InitRawDesc32(var Desc: TRawImageDescription; AWidth, AHeight: Integer);
begin
FillChar(Desc, SizeOf(Desc), 0);
Desc.Format := ricfRGBA;
Desc.Width := AWidth;
Desc.Height := AHeight;
Desc.Depth := 32;
Desc.BitOrder := riboBitsInOrder;
Desc.ByteOrder := riboLSBFirst;
Desc.LineOrder := riloTopToBottom;
Desc.BitsPerPixel:= 32;
Desc.LineEnd := rileDWordBoundary;
// BGRA: B=bits 0..7, G=8..15, R=16..23, A=24..31
Desc.BlueShift := 0; Desc.BluePrec := 8;
Desc.GreenShift := 8; Desc.GreenPrec := 8;
Desc.RedShift := 16; Desc.RedPrec := 8;
Desc.AlphaShift := 24; Desc.AlphaPrec := 8;
end;
procedure TMainForm.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
// ═══════════════════════════════════════════════════════════════════════
// Попиксельный градиент через кэшированный FSpecGradImg.
// Старый подход: H/2 × MoveTo+LineTo = ~150-250 GDI calls = ~3-5ms
// Новый подход: записываем пиксели в FSpecGradImg + один BitBlt ≈ 1ms
// ═══════════════════════════════════════════════════════════════════════
var
Row, Col: Integer;
YMin: Integer;
Alpha: Integer;
RowPtr: PByte;
BPPg: Integer;
RG, GG: Byte;
Desc: TRawImageDescription;
begin
if FSpectrumBitmap = nil then Exit;
if (W <= 0) or (H <= 0) then Exit;
YMin := H;
for Col := 0 to W - 1 do
if SpPts[Col].Y < YMin then YMin := SpPts[Col].Y;
if YMin >= H then Exit;
// Создаём/переиспользуем FSpecGradImg
if (FSpecGradImg = nil) or
(FSpecGradImg.Width <> W) or (FSpecGradImg.Height <> H) then
begin
FreeAndNil(FSpecGradImg);
FSpecGradImg := TLazIntfImage.Create(W, H);
InitRawDesc32(Desc, W, H);
FSpecGradImg.DataDescription := Desc;
FSpecGradImg.CreateData;
end;
// Очищаем прозрачным
if FSpecGradImg.PixelData <> nil then
FillChar(FSpecGradImg.PixelData^, W * H * 4, 0);
BPPg := FSpecGradImg.DataDescription.BitsPerPixel div 8;
if BPPg < 3 then BPPg := 4;
for Row := YMin to H - 1 do
begin
Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200);
if Alpha <= 0 then Continue;
if Alpha > 200 then Alpha := 200;
GG := Byte(Alpha shr 1); // B-канал
RG := Byte(Alpha); // G-канал
RowPtr := FSpecGradImg.GetDataLineStart(Row);
if RowPtr = nil then Continue;
for Col := 0 to W - 1 do
begin
if SpPts[Col].Y <= Row then
begin
RowPtr[0] := GG; RowPtr[1] := RG; RowPtr[2] := 0;
if BPPg >= 4 then RowPtr[3] := Byte(Alpha);
end
else
begin
RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0;
if BPPg >= 4 then RowPtr[3] := 0;
end;
Inc(RowPtr, BPPg);
end;
end;
// Выводим через кэшированный bitmap → BitBlt на спектрум-bitmap
FSpecGradBmp.LoadFromIntfImage(FSpecGradImg);
LCLIntf.BitBlt(FSpectrumBitmap.Canvas.Handle, 0, 0, W, H,
FSpecGradBmp.Canvas.Handle, 0, 0, $CC0020);
end;
procedure TMainForm.DrawSpectrum;
var
C: TCanvas;
GC: TCanvas; // для рисования в FGridBitmap
i, Yp, W, H, GX: Integer;
DBmin, DBmax: Double;
dB: Double;
VfoX, X1, X2: Integer;
Lo_Hz, Hi_Hz, Half: Double;
AGCy, AGCHangY: Integer;
SrcF, Frac, dBv: Double;
S0, S1: Integer;
InvRange: Double;
begin
if FSpectrumBitmap = nil then Exit;
W := FSpectrumBitmap.Width;
H := FSpectrumBitmap.Height;
if (W <= 0) or (H <= 0) then Exit;
C := FSpectrumBitmap.Canvas;
DBmin := -130.0; DBmax := -20.0;
InvRange := 1.0 / (DBmax - DBmin);
// ── 1. Фон+сетка из кэша ───────────────────────────────────────────────
if (FGridBitmapW <> W) or (FGridBitmapH <> H) then
begin
FGridBitmapW := W;
FGridBitmapH := H;
FGridBitmap.SetSize(W, H);
GC := FGridBitmap.Canvas;
GC.Brush.Color := TColor($00050505);
GC.FillRect(Rect(0, 0, W, H));
GC.Font.Size := 7;
GC.Font.Name := 'Courier New';
GC.Font.Color := CLR_TEXTDIM;
dB := -30.0;
while dB >= DBmin do
begin
Yp := Round((DBmax - dB) * InvRange * H);
GC.Pen.Color := CLR_BORDER;
GC.Pen.Width := 1;
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
dB := dB - 10;
end;
for i := 0 to 8 do
begin
GX := ScaleX(i, 8, W);
GC.Pen.Color := CLR_BORDER;
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
end;
end;
LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020);
// ── 2. Полоса фильтра — заливка ДО спектра ─────────────────────────────
Half := FFilterBW / 2;
VfoX := Round((FVfoA - 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)));
C.Brush.Color := TColor($00102828);
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 линии ────────────────────────────────────────────────────────
if FWDSPReady then
begin
AGCy := Round((DBmax - FDSPEngine.AGCThresh) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
C.TextOut(4, AGCy - 9, 'AGC T');
end;
AGCHangY := Round((DBmax - FDSPEngine.AGCHangLevel) * InvRange * H);
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
begin
C.Pen.Color := TColor($00FFCC00); C.Pen.Width := 1; C.Pen.Style := psDot;
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); C.Font.Size := 6;
C.TextOut(4, AGCHangY + 2, 'AGC H');
end;
end
else
begin
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
end;
end;
// ── 4. Кривая спектра — кэшируем FSpPts без SetLength каждый кадр ──────
if FSpPtsLen <> W + 2 then
begin
SetLength(FSpPts, W + 2);
FSpPtsLen := W + 2;
end;
for i := 0 to W - 1 do
begin
SrcF := i * 1023.0 / Max(1, W - 1);
S0 := Min(Trunc(SrcF), 1023);
S1 := Min(S0 + 1, 1023);
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);
// ── 5. Градиент ─────────────────────────────────────────────────────────
C.Brush.Style := bsSolid;
C.Pen.Style := psClear;
DrawSpectrumGradient(FSpPts, W, H);
// ── 6. Линия спектра ────────────────────────────────────────────────────
C.Pen.Style := psSolid;
C.Pen.Color := TColor($0040FF80);
C.Pen.Width := 1;
C.Polyline(Slice(FSpPts, W)); // один вызов вместо W раз LineTo
// ── 7. Края фильтра + VFO поверх спектра ────────────────────────────────
C.Pen.Color := TColor($0040FFCC); C.Pen.Width := 1; C.Pen.Style := psSolid;
C.MoveTo(X1, 0); C.LineTo(X1, H);
C.MoveTo(X2, 0); C.LineTo(X2, H);
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 2;
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
C.Brush.Color := TColor($0000AAFF); C.Brush.Style := bsSolid; C.Pen.Width := 1;
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
if FMarkerActive then
DrawMarkerLine(C, W, H);
end;
procedure TMainForm.DrawWaterfall;
// ═══════════════════════════════════════════════════════════════════════
// Архитектура минимального CPU:
// FWfPixels[W*H] — весь водопад в памяти, строка 0 = верх (новейшая).
// Скролл = Move(FWfPixels[0]→FWfPixels[W], (H-1)*W*4) ~0.3ms
// Новая строка = инкрементный PLongWord по FWfPixels[0..W-1] ~0.1ms
// Вывод = TLazIntfImage.PixelData := FWfPixels + LoadFromIntfImage ~1ms
// Итого ~1.5ms vs старый IntfImage roundtrip ~5ms
// ═══════════════════════════════════════════════════════════════════════
var
W, H, X: Integer;
dB, frac: Double;
WatSrcF: Double;
WatS0, WatS1, V: Integer;
AvgDB: Double;
PeakDB: Double;
NFSum: Double;
NFCount: Integer;
WfHigh, WfLow, InvRange: Double;
Step: Double;
Px: PLongWord;
Desc: TRawImageDescription;
Row, Col: Integer;
RowPtr: PByte;
Src: PLongWord;
BPPi: Integer;
Pal: LongWord;
const
ALPHA_SPEC = 0.25;
ALPHA_HIGH = 0.02;
ALPHA_LOW = 0.005;
begin
if FWaterfallBitmap = nil then Exit;
W := FWaterfallBitmap.Width;
H := FWaterfallBitmap.Height;
if (W <= 0) or (H <= 0) then Exit;
// ── EMA + Peak + NF — один цикл по 1024 бинам ─────────────────────────
PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
for X := 0 to 1023 do
begin
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
FWfAvgBuf[X] := AvgDB;
if AvgDB > PeakDB then PeakDB := AvgDB;
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
end;
// ── AGC High/Low ────────────────────────────────────────────────────────
if FWfAGCEnabled then
begin
if PeakDB > FWfHigh then FWfHigh := PeakDB + 5.0
else FWfHigh := FWfHigh + ALPHA_HIGH * ((PeakDB + 5.0) - FWfHigh);
end;
if FWfNFEnabled and (NFCount > 10) then
FWfLow := FWfLow + ALPHA_LOW * ((NFSum / NFCount - 5.0) - FWfLow);
WfHigh := FWfHigh; WfLow := FWfLow;
if WfHigh < WfLow + 15.0 then WfHigh := WfLow + 15.0;
if WfHigh > 0.0 then WfHigh := 0.0;
if WfLow < -160 then WfLow := -160;
InvRange := 255.0 / (WfHigh - WfLow);
// ── Пересоздаём буфер при ресайзе ──────────────────────────────────────
if (FWfBitmapW <> W) or (FWfBitmapH <> H) then
begin
FWfBitmapW := W; FWfBitmapH := H;
SetLength(FWfPixels, W * H);
FillDWord(FWfPixels[0], W * H, 0);
FWfBitmap.SetSize(W, H);
// Сбрасываем кэш IntfImage — пересоздастся ниже с новым размером
FreeAndNil(FWfIntfImg);
end;
// ── Скролл: Move строк вниз ─────────────────────────────────────────────
if H > 1 then
Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W);
// ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ────────
Step := 1023.0 / Max(1, W - 1);
WatSrcF := 0.0;
Px := @FWfPixels[0];
for X := 0 to W - 1 do
begin
WatS0 := Trunc(WatSrcF);
if WatS0 > 1022 then WatS0 := 1022;
WatS1 := WatS0 + 1;
frac := WatSrcF - WatS0;
dB := FWfAvgBuf[WatS0] * (1.0 - frac) + FWfAvgBuf[WatS1] * frac;
V := Trunc((dB - WfLow) * InvRange);
if V < 0 then V := 0;
if V > 255 then V := 255;
// Палитра задана как RGB, а в буфере нужен BGRA.
// Меняем местами R/B и ставим alpha=255, чтобы совпадало с web/Thetis-градиентом.
Pal := WFALL_PALETTE[V];
Px^ := ((Pal and $000000FF) shl 16) or
(Pal and $0000FF00) or
((Pal and $00FF0000) shr 16) or
$FF000000;
Inc(Px);
WatSrcF := WatSrcF + Step;
end;
// ── Записываем FWfPixels в FWfBitmap через кэшированный TLazIntfImage ────
// FWfIntfImg создаётся один раз на весь lifetime (или при ресайзе).
// Move(FWfPixels → IntfImg.PixelData) = один memcpy без GDI roundtrip.
// LoadFromIntfImage = один вызов platformAPI → ~1ms на Linux GTK2.
if FWfIntfImg = nil then
begin
FWfIntfImg := TLazIntfImage.Create(W, H);
InitRawDesc32(Desc, W, H);
FWfIntfImg.DataDescription := Desc;
FWfIntfImg.CreateData;
end;
BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8;
if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then
Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4)
else
begin
Src := @FWfPixels[0];
for Row := 0 to H - 1 do
begin
RowPtr := FWfIntfImg.GetDataLineStart(Row);
if RowPtr = nil then begin Inc(Src, W); Continue; end;
for Col := 0 to W - 1 do
begin
RowPtr[0] := Byte(Src^ shr 16); // B
RowPtr[1] := Byte(Src^ shr 8); // G
RowPtr[2] := Byte(Src^); // R
if BPPi >= 4 then RowPtr[3] := $FF;
Inc(Src); Inc(RowPtr, BPPi);
end;
end;
end;
FWfBitmap.LoadFromIntfImage(FWfIntfImg);
end;
// ===========================================================================
// Широкий S-метр — точная копия Thetis/PowerSDR стиля
// Структура (высота H):
// верх (H*35%): dBm шкала (-120..-20..0), тики, цифровое значение слева
// середина (H*35%): полоса сигнала (зелёный бар + широкий пик + белый маркер)
// низ (H*30%): S-шкала (S1..S9..+60), S-значение слева
// ===========================================================================
procedure TMainForm.DrawSMeterWide(ACanvas: TCanvas; R: TRect;
Value, Peak, MinVal: Double;
out ZX1, ZX2, ZY1, ZY2: Integer);
const
DB_MIN = -127.0; // левый край шкалы
DB_MAX = -13.0; // правый край шкалы (S9+60)
DB_S9 = -73.0; // граница S9 (зелёная/оранжевая)
DB_OVR = -43.0; // граница "красной" зоны (+30 over S9)
// dBm верхняя шкала
DBM_MARKS: array[0..5] of Double = (-120,-100,-80,-60,-40,-20);
DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20');
// S-шкала нижняя: как в VfoOverlay
S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60');
// Цвета (BGR в TColor)
CLR_SMETER_BG = TColor($00181818); // фон = CLR_PANEL
CLR_BAR_GREEN = TColor($00091F00); // тёмно-зелёный бар до S9 (RGB 0,31,9)
CLR_BAR_OVER = TColor($00091528); // тёмно-красный бар после S9 (RGB 40,21,9) — тот же тон что зелёный но красн.
CLR_PEAK_FILL = TColor($00796F75); // светло-серый заполнитель пика (RGB 117,111,121)
CLR_PEAK_MARKER = TColor($00F8F8FF); // белый пик-маркер (RGB 255,248,248)
CLR_BAR_ORANGE = TColor($000A1A28); // оранжевая зона (>S9)
CLR_BAR_RED = TColor($00000832); // красная зона (>+30)
CLR_TICK_GREEN = TColor($00708070); // тики зелёной зоны
CLR_TICK_WHITE = TColor($00E0E0DC); // тики белые (dBm шкала)
CLR_TICK_BLUE = TColor($0070A090); // тики S9+ зоны
CLR_LABEL_DBM = TColor($0096D8DC); // цифровое значение dBm (желтоватый)
CLR_LABEL_S = TColor($00F0F0F0); // S-значение — белый
CLR_SCALE_DBM = TColor($00D0DCDC); // метки dBm шкалы (бело-серые)
CLR_SCALE_S_GREEN = TColor($00E0E0E0); // метки S1..S9 — белые
CLR_SCALE_S_BLUE = TColor($0090D090); // метки +10..+60 — как S1..S9 оригинал (желт-зелён)
CLR_BORDER = TColor($00303028); // рамки
LEFT_INFO = 62; // ширина левой информационной колонки (dBm + S-value)
TICK_LONG = 5; // длина длинного тика
TICK_MED = 3; // длина среднего тика
var
W, H: Integer;
BX, BW: Integer; // начало и ширина зоны шкалы
Y_UPPER_MID: Integer; // середина верхней зоны (метки dBm)
Y_BAR_TOP: Integer; // верх полосы сигнала
Y_BAR_BOT: Integer; // низ полосы сигнала
Y_LOWER_MID: Integer; // середина нижней зоны (метки S)
BarEnd: Integer; // правый край текущего сигнала
PeakLeft: Integer; // левый край peak fill
PeakRight: Integer; // правый край peak fill (= белый маркер)
MinX: Integer; // позиция минимума
S9X, OvrX: Integer; // позиции границ зон
i, X, TW: Integer;
Lbl: string;
SNum: Integer;
Over: Integer;
function DBtoX(dB: Double): Integer; inline;
begin
Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW);
if Result < BX then Result := BX;
if Result > BX + BW then Result := BX + BW;
end;
procedure HLine(Y, X1, X2: Integer; C: TColor);
begin
ACanvas.Pen.Color := C;
ACanvas.MoveTo(X1, Y); ACanvas.LineTo(X2, Y);
end;
procedure VLine(X, Y1, Y2: Integer; C: TColor);
begin
ACanvas.Pen.Color := C;
ACanvas.MoveTo(X, Y1); ACanvas.LineTo(X, Y2);
end;
procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
begin
ACanvas.Brush.Color := C;
ACanvas.Brush.Style := bsSolid;
ACanvas.Pen.Style := psClear;
if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
ACanvas.Pen.Style := psSolid;
end;
begin
W := R.Right - R.Left;
H := R.Bottom - R.Top;
// Инициализируем out-параметры
ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0;
if (W < 80) or (H < 16) then Exit;
// --- Геометрия ---
BX := R.Left + LEFT_INFO;
BW := W - LEFT_INFO - 4;
Y_UPPER_MID := R.Top + H * 3 div 10; // центр верхней зоны меток
Y_BAR_TOP := R.Top + H * 36 div 100; // верх бара
Y_BAR_BOT := R.Top + H * 66 div 100; // низ бара
Y_LOWER_MID := R.Top + H * 80 div 100; // центр нижней зоны меток
S9X := DBtoX(DB_S9);
OvrX := DBtoX(DB_OVR);
// ================================================================
// ФОН
// ================================================================
ACanvas.Brush.Color := CLR_SMETER_BG;
ACanvas.Brush.Style := bsSolid;
ACanvas.Pen.Style := psClear;
ACanvas.FillRect(R);
ACanvas.Pen.Style := psSolid;
// Лёгкий цветной фон зон шкалы в полосе бара
FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, TColor($00061006)); // тёмно-зелёный
FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, TColor($000A1006)); // тёмно-жёлтый
FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, TColor($00060A10)); // тёмно-красный
// ================================================================
// ПОЛОСА СИГНАЛА
// ================================================================
BarEnd := DBtoX(Value); // текущий уровень
PeakLeft := DBtoX(MinVal); // левый край зоны = отслеживаемый минимум
PeakRight:= DBtoX(Peak); // правый край зоны = отслеживаемый пик
// Заполняем out-параметры зоны для alpha-blend (после возврата)
ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight));
ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight));
ZY1 := Y_BAR_TOP + 1;
ZY2 := Y_BAR_BOT - 1;
// Бар: зелёный до S9, красноватый после S9 — в том же тёмном стиле
if BarEnd > BX then
begin
if BarEnd <= S9X then
// Весь бар в зелёной зоне
FillBar(BX, Y_BAR_TOP + 1, BarEnd, Y_BAR_BOT - 1, CLR_BAR_GREEN)
else
begin
// Зелёная часть до S9
FillBar(BX, Y_BAR_TOP + 1, S9X, Y_BAR_BOT - 1, CLR_BAR_GREEN);
// Красноватая часть после S9
FillBar(S9X, Y_BAR_TOP + 1, BarEnd, Y_BAR_BOT - 1, CLR_BAR_OVER);
end;
end;
// Белый маркер пика (2px) — правый край зоны
if (PeakRight > BX) and (PeakRight <= BX + BW) then
begin
ACanvas.Pen.Color := CLR_PEAK_MARKER;
ACanvas.Pen.Width := 2;
ACanvas.MoveTo(PeakRight, Y_BAR_TOP);
ACanvas.LineTo(PeakRight, Y_BAR_BOT);
ACanvas.Pen.Width := 1;
end;
// Рамка полосы
ACanvas.Brush.Style := bsClear;
ACanvas.Pen.Color := CLR_BORDER;
ACanvas.Rectangle(BX, Y_BAR_TOP, BX + BW, Y_BAR_BOT);
// ================================================================
// ВЕРХНЯЯ ШКАЛА — dBm метки (-120..-100..-20..0)
// ================================================================
ACanvas.Font.Name := 'Courier New';
ACanvas.Font.Size := 6;
ACanvas.Font.Style := [];
ACanvas.Brush.Style:= bsClear;
for i := 0 to High(DBM_MARKS) do
begin
X := DBtoX(DBM_MARKS[i]);
// Длинный тик вверх от полосы
VLine(X, Y_BAR_TOP - TICK_LONG, Y_BAR_TOP - 1, CLR_TICK_WHITE);
// Метка
Lbl := DBM_LABELS[i];
TW := ACanvas.TextWidth(Lbl);
ACanvas.Font.Color := CLR_SCALE_DBM;
ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl);
end;
// Мелкие тики каждые 10 dB между основными (каждые 5)
i := -125;
while i < -13 do
begin
X := DBtoX(i);
VLine(X, Y_BAR_TOP - TICK_MED, Y_BAR_TOP - 1, CLR_TICK_GREEN);
Inc(i, 5);
end;
// ================================================================
// НИЖНЯЯ ШКАЛА — S1..S9..+60
// ================================================================
for i := 0 to High(S_MARKS_DBM) do
begin
X := DBtoX(S_MARKS_DBM[i]);
// Тик вниз от полосы
if S_MARKS_DBM[i] >= DB_S9 then
VLine(X, Y_BAR_BOT + 1, Y_BAR_BOT + TICK_LONG, CLR_TICK_BLUE)
else
VLine(X, Y_BAR_BOT + 1, Y_BAR_BOT + TICK_LONG, CLR_TICK_GREEN);
// Метка
Lbl := S_MARKS_LBL[i];
TW := ACanvas.TextWidth(Lbl);
if S_MARKS_DBM[i] >= DB_S9 then
ACanvas.Font.Color := CLR_SCALE_S_BLUE
else
ACanvas.Font.Color := CLR_SCALE_S_GREEN;
ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG + 1, Lbl);
end;
// ================================================================
// ЛЕВАЯ КОЛОНКА — цифровое значение
// ================================================================
// dBm значение (крупно, вверху)
ACanvas.Font.Size := 8;
ACanvas.Font.Style := [fsBold];
ACanvas.Font.Color := CLR_LABEL_DBM;
Lbl := Format('%6.1f', [Value]);
ACanvas.TextOut(R.Left + 1, R.Top + 1, Lbl);
// 'dBm' мелко под значением
ACanvas.Font.Size := 6;
ACanvas.Font.Style := [];
ACanvas.Font.Color := TColor($00507070);
ACanvas.TextOut(R.Left + 1, Y_BAR_TOP - ACanvas.TextHeight('dBm') - 1, 'dBm');
// S-значение (внизу)
ACanvas.Font.Size := 8;
ACanvas.Font.Style := [fsBold];
ACanvas.Font.Color := CLR_LABEL_S;
if Value >= -73.0 then
begin
Over := Round(Value - (-73.0));
if Over < 5 then
Lbl := 'S9'
else
begin
Over := ((Over + 5) div 10) * 10;
if Over = 0 then Over := 10;
Lbl := Format('S9+%d', [Over]);
end;
end
else if Value <= -121.0 then
Lbl := 'S1'
else
begin
Lbl := 'S1';
for SNum := 1 to 8 do
if Value >= (-121.0 + (SNum - 1) * 6.0) then
Lbl := 'S' + IntToStr(SNum);
end;
ACanvas.TextOut(R.Left + 1, Y_BAR_BOT + TICK_LONG + 1, Lbl);
end;
// Полупрозрачная белая заливка зоны X1..X2, Y1..Y2.
// Вызывается ПОСЛЕ DrawSMeterWide — FSmBitmap уже содержит нарисованный фон.
// Используем TLazIntfImage для попиксельного alpha-blend.
procedure TMainForm.DrawSMeterZone(ACanvas: TCanvas; R: TRect;
X1, X2, Y1, Y2: Integer);
const
ALPHA = 16384; // ~25% осветления (от 0..65535)
var
Img: TLazIntfImage;
FC: TFPColor;
IX, IY: Integer;
begin
if (X2 <= X1) or (FSmBitmap = nil) then Exit;
// Получаем IntfImage из off-screen bitmap
Img := FSmBitmap.CreateIntfImage;
try
for IY := Y1 to Y2 - 1 do
for IX := X1 to X2 - 1 do
begin
FC := Img.Colors[IX, IY];
FC.Red := Min(65535, FC.Red + ALPHA);
FC.Green := Min(65535, FC.Green + ALPHA);
FC.Blue := Min(65535, FC.Blue + ALPHA);
Img.Colors[IX, IY] := FC;
end;
// Записываем изменения обратно в bitmap
FSmBitmap.LoadFromIntfImage(Img);
finally
Img.Free;
end;
end;
procedure TMainForm.PbSMeterRightPaint(Sender: TObject);
var
W, H: Integer;
ZX1, ZX2, ZY1, ZY2: Integer;
begin
W := PbSMeterRight.Width;
H := PbSMeterRight.Height;
if (W <= 0) or (H <= 0) then Exit;
// Создаём/пересоздаём off-screen буфер
if (FSmBitmap = nil) or (FSmBitmap.Width <> W) or (FSmBitmap.Height <> H) then
begin
FreeAndNil(FSmBitmap);
FSmBitmap := TBitmap.Create;
FSmBitmap.SetSize(W, H);
end;
// Рисуем основной S-метр в off-screen буфер
DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, W, H),
FLastSMeter, FSMeterPeak, FSMeterMin,
ZX1, ZX2, ZY1, ZY2);
// Полупрозрачная белая заливка зоны Min..Peak поверх нарисованного
DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, W, H), ZX1, ZX2, ZY1, ZY2);
// Выводим на экран одним блоком (без мерцания)
PbSMeterRight.Canvas.Draw(0, 0, FSmBitmap);
end;
procedure TMainForm.PbFwdPowerPaint(Sender: TObject);
begin
DrawBarMeter(PbFwdPower.Canvas, Rect(0,0,PbFwdPower.Width,PbFwdPower.Height),
FLastFwdW, 150, CLR_METER_ON);
end;
procedure TMainForm.PbSWRPaint(Sender: TObject);
begin
DrawBarMeter(PbSWR.Canvas, Rect(0,0,PbSWR.Width,PbSWR.Height),
FLastSWR - 1.0, 4.0, CLR_AMBER);
end;
procedure TMainForm.PbSpectrumPaint(Sender: TObject);
begin
if (FSpectrumBitmap.Width > 0) and (FSpectrumBitmap.Height > 0) then
PbSpectrum.Canvas.Draw(0, 0, FSpectrumBitmap)
else
begin
PbSpectrum.Canvas.Brush.Color := CLR_BG;
PbSpectrum.Canvas.FillRect(Rect(0,0,PbSpectrum.Width,PbSpectrum.Height));
end;
end;
// ---------------------------------------------------------------------------
// Ruler — полоса частотных меток между спектром и водопадом
// ---------------------------------------------------------------------------
procedure TMainForm.DrawRuler;
// ОПТИМИЗАЦИЯ: рисуем в offscreen FRulerBitmap, перерисовываем только когда
// изменился FCenterFreq. Экономим TextOut+LineTo на каждом тике таймера (20fps).
var
C: TCanvas;
W, H: Integer;
i, X: Integer;
FreqStart, FreqHz: Double;
VfoX: Integer;
Lbl: string;
TW: Integer;
begin
if PbRuler = nil then Exit;
W := PbRuler.Width;
H := PbRuler.Height;
if (W <= 0) or (H <= 0) then Exit;
// Пропускаем перерисовку если ни частота центра, ни VFO не изменились
if (Abs(FCenterFreq - FRulerLastFreq) < 0.5) and
(Abs(FVfoA - FRulerLastVfo) < 0.5) then Exit;
FRulerLastFreq := FCenterFreq;
FRulerLastVfo := FVfoA;
if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then
FRulerBitmap.SetSize(W, H);
C := FRulerBitmap.Canvas;
C.Brush.Color := TColor($00141414);
C.FillRect(Rect(0, 0, W, H));
C.Pen.Color := CLR_BORDER;
C.Pen.Width := 1;
C.MoveTo(0, 0); C.LineTo(W, 0);
C.MoveTo(0, H-1); C.LineTo(W, H-1);
C.Font.Name := 'Courier New';
C.Font.Size := 7;
FreqStart := FCenterFreq - FSpanHz / 2;
for i := 0 to 8 do
begin
X := ScaleX(i, 8, W);
C.Pen.Color := CLR_BORDER;
C.MoveTo(X, 0); C.LineTo(X, H div 2);
FreqHz := FreqStart + i * FSpanHz / 8;
Lbl := Format('%.3f', [FreqHz / 1e6]);
TW := C.TextWidth(Lbl);
C.Font.Color := TColor($00888888);
C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
end;
VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
if (VfoX >= 0) and (VfoX < W) then
begin
C.Pen.Color := TColor($0000FF80);
C.Pen.Width := 2;
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1);
C.Pen.Width := 1;
end;
end;
procedure TMainForm.PbRulerPaint(Sender: TObject);
begin
DrawRuler; // обновит FRulerBitmap только если FCenterFreq изменился
if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then
PbRuler.Canvas.Draw(0, 0, FRulerBitmap);
end;
procedure TMainForm.PbWaterfallPaint(Sender: TObject);
// Отображаем FWfBitmap — он уже содержит готовый кадр из DrawWaterfall.
// DrawWaterfall делает скролл через Move() в байтах IntfImage (~0.3ms),
// пишет новую строку, и один раз LoadFromIntfImage (~2ms).
// Здесь просто Canvas.Draw — данные уже в bitmap, GDI копирует за ~1ms.
var
W, H: Integer;
begin
W := PbWaterfall.Width;
H := PbWaterfall.Height;
if (W <= 0) or (H <= 0) then Exit;
if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then
PbWaterfall.Canvas.Draw(0, 0, FWfBitmap)
else
begin
PbWaterfall.Canvas.Brush.Color := clBlack;
PbWaterfall.Canvas.FillRect(Rect(0, 0, W, H));
end;
if FMarkerActive then
DrawMarkerLine(PbWaterfall.Canvas, W, H);
end;
// ===========================================================================
// Timers
// ===========================================================================
procedure TMainForm.MeterTimerTick(Sender: TObject);
const
AVG_ALPHA = 0.25; // EMA для сглаженного среднего уровня
ZONE_DB = 6.0; // полуширина зоны вокруг среднего (дБ)
// Адаптивный alpha: базовый + ускорение при большой разнице
PEAK_BASE = 0.06; // базовая скорость пика
MIN_BASE = 0.06; // базовая скорость минимума
ACCEL_FACTOR = 0.018; // ускорение на каждый dB разницы
MAX_ALPHA = 0.85; // максимальная скорость (не мгновенно)
var
PeakTarget, MinTarget: Double;
PeakDiff, MinDiff: Double;
PeakAlpha, MinAlpha: Double;
begin
if not FRunning then
begin
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
Exit;
end;
// Сглаженное среднее (EMA)
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FLastSMeter * AVG_ALPHA;
// Цели для Peak и Min
PeakTarget := Max(FLastSMeter, FSMeterAvg + ZONE_DB);
MinTarget := Min(FLastSMeter, FSMeterAvg - ZONE_DB);
// Адаптивный alpha: чем дальше от цели — тем быстрее догоняем
PeakDiff := Abs(PeakTarget - FSMeterPeak);
MinDiff := Abs(MinTarget - FSMeterMin);
PeakAlpha := Min(MAX_ALPHA, PEAK_BASE + PeakDiff * ACCEL_FACTOR);
MinAlpha := Min(MAX_ALPHA, MIN_BASE + MinDiff * ACCEL_FACTOR);
FSMeterPeak := FSMeterPeak + PeakAlpha * (PeakTarget - FSMeterPeak);
FSMeterMin := FSMeterMin + MinAlpha * (MinTarget - FSMeterMin);
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
end;
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
var
NowTick: QWord;
ElapsedTick: QWord;
begin
// Синхронизируем размеры если PaintBox изменился (после ресайза)
if (PbSpectrum.Width <> FSpectrumWidth) or
(PbSpectrum.Height <> FSpectrumHeight) or
(FSpectrumWidth = 0) then
ResizeSpectrumPanels;
ResizeSMeter;
if FSpectrumWidth <= 0 then Exit;
// Проверка таймаута — трансивер не отвечает
if FRunning and FNetwork.Running then
begin
NowTick := GetTickCount64;
ElapsedTick := NowTick - FRXStartTime;
// Ждём первый пакет 5 секунд после старта
if (FRXLastPktTime = 0) and (ElapsedTick > 5000) then
begin
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
FRunning := False;
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
StatusBar1.Panels[4].Text := 'ТРАНСИВЕР НЕДОСТУПЕН';
BtnMOX.Enabled := False;
Exit;
end;
// После первого пакета — следим чтобы поток не прерывался более 3 сек
if (FRXLastPktTime > 0) and ((NowTick - FRXLastPktTime) > 3000) then
begin
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
FRunning := False;
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
StatusBar1.Panels[4].Text := 'ПОТЕРЯ СВЯЗИ С ТРАНСИВЕРОМ';
BtnMOX.Enabled := False;
Exit;
end;
end;
if FRunning then
begin
// Обновляем StatusBar из таймера — без Synchronize в сетевом потоке
StatusBar1.Panels[3].Text :=
Format('RX DDC%d | seq=%d | pkts=%d',
[FLastDDCIndex, FLastDDCSeq, FRXPacketCount]);
if FWDSPReady then
begin
FDSPEngine.UpdateSpectrum;
FLastSMeter := FDSPEngine.GetSMeterDBm;
FDSPEngine.SetSpectrumWidth(FSpectrumWidth); // актуальная ширина для SetAGC/SetAGCTop
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
end;
// Обновляем оверлей если видим
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
begin
FVfoOverlay.UpdateSMeter(FLastSMeter);
FVfoOverlay.UpdateVfo(FVfoA);
PositionVfoOverlay;
end;
// Рисуем только при новых спектральных данных/изменении UI (FSpectrumDirty).
// Это резко снижает CPU в холостом режиме.
if FSpectrumDirty then
begin
DrawSpectrum;
PbSpectrum.Invalidate;
// Водопад рисуем на каждом новом спектральном кадре для плавности.
DrawWaterfall;
PbWaterfall.Invalidate;
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
PbRuler.Invalidate;
FSpectrumDirty := False;
end;
end
else
begin
// Не подключены — показываем чистый экран (без анимации)
if not FNetwork.Connected then
begin
// Очищаем до фона один раз при отключении
// (просто не вызываем Draw* — бitmaps остаются как были)
// Но при первом старте покажем пустой фон:
if FSpectrumBitmap.Width <> FSpectrumWidth then
begin
FSpectrumBitmap.SetSize(FSpectrumWidth, FSpectrumHeight);
ResetSpectrumBuf;
DrawSpectrum;
PbSpectrum.Invalidate;
end;
end
else
begin
// Подключены, но не запущены — статичный спектр (шум)
DrawSpectrum;
PbSpectrum.Invalidate;
FSpectrumDirty := False;
end;
end;
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
if FSpectrumDirty and (not FRunning) then
begin
DrawSpectrum;
DrawWaterfall;
PbSpectrum.Invalidate;
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
PbRuler.Invalidate;
PbWaterfall.Invalidate;
FSpectrumDirty := False;
end;
// Пушим текущее состояние в веб-клиенты (если есть)
if Assigned(FWebServer) then
FWebServer.PushSpectrum(
FSpectrumBuf, 1024,
FWfAvgBuf,
FLastSMeter,
FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop,
FSpanHz, FVolume,
FWfAGCEnabled, FWfNFEnabled,
FCurrentBand,
FRunning and FNetwork.Connected,
FRunning, FMuted, FCTun,
BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0,
FCenterFreq, FFilter);
end;
// ===========================================================================
// Network callbacks → sync helpers
// ===========================================================================
procedure TMainForm.OnDeviceFound(const Dev: THPSDRDevice);
var
BoardName, Entry: string;
Sync: TDeviceFoundSync;
M: TThreadMethod;
begin
BoardName := BoardTypeName(Dev.BoardType);
Entry := Format('%s %s FW:%d DDC:%d',
[Dev.IPAddress, BoardName,
Dev.FirmwareVersion, Dev.NumDDCs]);
Sync := TDeviceFoundSync.Create(Self, Dev, Entry);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
end;
procedure TMainForm.DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
var
Idx: Integer;
begin
Idx := Length(FDevices);
SetLength(FDevices, Idx + 1);
FDevices[Idx].Dev := Dev;
FDevices[Idx].Display := Entry;
Inc(FDeviceCount);
if Assigned(FDeviceDialog) then
FDeviceDialog.AddDiscovered(Dev.IPAddress, Entry, Dev.BoardType);
StatusBar1.Panels[4].Text := Format('Found: %s', [Dev.IPAddress]);
end;
procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus);
var
ExcPwr, FwdPwr, RevPwr: Word;
SupplyV, FwdW, SWRV: Double;
Sync: TStatusUISync;
M: TThreadMethod;
begin
ExcPwr := (Status.ExciterPwr0Hi shl 8) or Status.ExciterPwr0Lo;
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
SupplyV := ADCToSupplyVolts((Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo);
// FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
// ExciterPwr это мощность TX, не уровень принятого сигнала.
FwdW := ADCToWatts100(FwdPwr);
if FwdW > 0 then
SWRV := 1 + Sqrt(RevPwr / FwdPwr)
else
SWRV := 1.0;
Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV,
(Status.StatusBits and HPS_PLL_LOCKED) <> 0);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
end;
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
var
PLLStr: string;
begin
FLastFwdW := FwdW;
FLastSWR := SWRV;
LblFwdPwr.Caption := Format('%.0fW', [FwdW]);
LblSWRVal.Caption := Format('SWR:%.1f', [SWRV]);
if PLLLock then PLLStr := 'PLL OK' else PLLStr := 'PLL?';
StatusBar1.Panels[2].Text :=
Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s',
[SupplyV, FwdW, SWRV, PLLStr]);
// Panels[3] зарезервирована для RX stats (DDC seq/pkt count)
end;
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
var
SamplesPerFrame: Integer;
begin
// Вызывается из сетевого потока — UI не трогаем напрямую
// Счётчик принятых пакетов
Inc(FRXPacketCount);
FRXLastPktTime := GetTickCount64; // фиксируем время последнего пакета
// 1. Подаём IQ данные в DSP — только с активного DDC
if (DDCIndex = FActiveDDC) and FWDSPReady then
begin
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
Integer(Data.SamplesPerFrame[1]);
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
end;
// 2. Запоминаем последний seq — StatusBar обновит таймер (без Synchronize в горячем пути)
FLastDDCSeq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
FLastDDCIndex := DDCIndex;
end;
procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
begin
StatusBar1.Panels[3].Text :=
Format('RX DDC%d | seq=%d | pkts=%d', [DDCIdx, Seq, FRXPacketCount]);
end;
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
begin
// TODO: WDSP TXA
end;
// ===========================================================================
// Button handlers
// ===========================================================================
{ Поток открытия — отдельный класс, без анонимных процедур }
// Открываем WDSP в фоновом потоке чтобы не блокировать UI при нажатии START
type
TWDSPOpenThread = class(TThread)
private
FForm: TMainForm;
FResult: Boolean;
procedure SyncDone;
protected
procedure Execute; override;
public
constructor Create(AForm: TMainForm);
end;
constructor TWDSPOpenThread.Create(AForm: TMainForm);
begin
inherited Create(False);
FForm := AForm;
FResult := False;
FreeOnTerminate := True;
end;
procedure TWDSPOpenThread.Execute;
var
M: TThreadMethod;
begin
try
FResult := FForm.FDSPEngine.Open;
except
FResult := False;
end;
M := SyncDone;
TThread.Synchronize(nil, M);
end;
procedure TWDSPOpenThread.SyncDone;
begin
FForm.OnWDSPOpenDone(FResult);
end;
type
TDiscoverThread = class(TThread)
private
FNet: THPSDRNetwork;
FForm: TMainForm;
protected
procedure Execute; override;
public
constructor Create(ANet: THPSDRNetwork; AForm: TMainForm);
end;
constructor TDiscoverThread.Create(ANet: THPSDRNetwork; AForm: TMainForm);
begin
FNet := ANet;
FForm := AForm;
FreeOnTerminate := True;
inherited Create(False);
end;
procedure TDiscoverThread.Execute;
var
Devs: THPSDRDeviceArray;
Sync: TDDCSeqSync;
M: TThreadMethod;
begin
Devs := FNet.Discover(2000);
if Length(Devs) = 0 then
begin
// DDCIdx = -1 is the sentinel for "no device found"
Sync := TDDCSeqSync.Create(FForm, -1, 0);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
end;
// Devices found are signalled via OnDeviceFound callback during Discover()
end;
{ Перегруженный DoUpdateDDCSeq принимает -1 как «нет устройств» }
procedure TMainForm.DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
begin
if DDCIdx = -1 then
begin
if Assigned(FDeviceDialog) then
begin
FDeviceDialog.ClearDiscovered;
FDeviceDialog.AddDiscovered('', '-- no device found --');
end;
StatusBar1.Panels[4].Text := 'No hardware found';
end
else
StatusBar1.Panels[3].Text := Format('DDC%d seq=%d', [DDCIdx, Seq]);
end;
procedure TMainForm.RecreateDSPEngine(ASampleRate: Integer);
begin
if ASampleRate <= 0 then Exit;
if Assigned(FDSPEngine) then
begin
FDSPEngine.Close;
FreeAndNil(FDSPEngine);
end;
FWDSPReady := False;
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
end;
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
begin
// Открываем диалог выбора устройства
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
FDeviceDialog.ShowModal;
// Результат диалога не обрабатываем здесь — START сам спросит если нужно
end;
procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject);
// Запускается когда пользователь нажимает DISCOVER внутри диалога
begin
SetLength(FDevices, 0);
FDeviceCount := 0;
StatusBar1.Panels[4].Text := 'Discovering...';
FNetwork.DirectIP := '';
TDiscoverThread.Create(FNetwork, Self);
end;
procedure TMainForm.BtnStartStopClick(Sender: TObject);
var
Idx, i: Integer;
Dev: THPSDRDevice;
GenPkt: TGeneralPacket;
DUCPkt: TDUCSpecificPacket;
G_Settings: TGlobalSettings;
PreG: TGlobalSettings;
PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings;
PreloadRate: Integer;
FoundByIP: Boolean;
AutoIP: string;
begin
if FNetwork.Connected then
begin
// --- STOP ---
if FRunning then
begin
FNetwork.UpdateState(FCenterFreq, FCenterFreq, 0, False, True, True);
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
FRunning := False;
FTransmitting := False;
if FWDSPReady then FDSPEngine.SetTXRun(False);
StyleButton(BtnMOX, False);
end;
FRXPacketCount := 0;
FActiveDDC := 0;
StatusBar1.Panels[3].Text := 'RX: waiting...';
ResetSpectrumBuf;
DrawSpectrum;
PbSpectrum.Invalidate;
FNetwork.Disconnect;
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
StatusBar1.Panels[4].Text := 'Not connected';
StatusBar1.Panels[0].Text := 'IP: --';
StatusBar1.Panels[1].Text := 'Board: --';
BtnMOX.Enabled := False;
Exit;
end;
// --- START ---
AutoIP := '';
FPendingBoardType := 0;
if Assigned(FDeviceDialog) then
AutoIP := FDeviceDialog.GetAutoStartIP;
if AutoIP <> '' then
begin
FPendingIP := AutoIP;
FPendingBoardType := FDeviceDialog.GetAutoStartBoardType;
end
else
begin
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FDeviceDialog.ShowModal <> mrOk then Exit;
if not FDeviceDialog.DialogResult.Accepted then Exit;
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
FPendingBoardType := FDeviceDialog.GetSavedBoardType(
FDeviceDialog.DialogResult.SavedIdx);
end;
// Создаём запись устройства из IP + BoardType из диалога если есть
FillChar(Dev, SizeOf(Dev), 0);
Dev.IPAddress := FPendingIP;
Dev.BoardType := FPendingBoardType;
// Подбираем sample rate до открытия WDSP, чтобы не делать двойной Open:
// 1) Open на 192k, 2) сразу ChangeSampleRate+Open ещё раз.
// Если устройство уже было обнаружено (есть MAC) и есть сохранённый профиль,
// создаём WDSPEngine сразу с нужной частотой.
PreloadRate := FSampleRate;
FoundByIP := False;
for i := 0 to High(FDevices) do
if SameText(FDevices[i].Dev.IPAddress, Dev.IPAddress) then
begin
FoundByIP := True;
if FSettings.LoadDevice(FDevices[i].Dev.MAC, PreG, PreBands) and (PreG.SampleRate > 0) then
PreloadRate := PreG.SampleRate;
Break;
end;
if FoundByIP and (PreloadRate > 0) then
begin
FSampleRate := PreloadRate;
FSpanHz := PreloadRate;
if (not FWDSPReady) and (Assigned(FDSPEngine)) and (FDSPEngine.SampleRate <> PreloadRate) then
RecreateDSPEngine(PreloadRate);
end;
// --- Открываем WDSP асинхронно чтобы не блокировать UI ---
FPendingDev := Dev;
BtnStartStop.Enabled := False;
StatusBar1.Panels[3].Text := 'Opening WDSP...';
if FWDSPReady then
// WDSP уже открыт — сразу подключаемся
DoConnectDevice(Dev)
else
// Открываем в фоновом потоке
TWDSPOpenThread.Create(Self);
end;
procedure TMainForm.OnWDSPOpenDone(Success: Boolean);
begin
// Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке
FWDSPReady := Success;
if not Success then
begin
StatusBar1.Panels[3].Text := 'WDSP open failed';
BtnStartStop.Enabled := True;
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
ShowMessage('WDSP not loaded.' + LineEnding +
IfThen(FDSPEngine.LastError <> '', FDSPEngine.LastError, 'Unknown WDSP init error.') + LineEnding +
'Put wdsp.dll рядом с ewsdr.exe (той же разрядности, что и приложение).');
Exit;
end;
DoConnectDevice(FPendingDev);
end;
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
var
GenPkt: TGeneralPacket;
DUCPkt: TDUCSpecificPacket;
G_Settings: TGlobalSettings;
begin
BtnStartStop.Enabled := True;
if not FNetwork.Connect(Dev) then
begin
ShowMessage('Failed to connect to ' + Dev.IPAddress +
IfThen(FNetwork.LastError <> '', ': ' + FNetwork.LastError, ''));
Exit;
end;
// --- Загружаем настройки устройства по MAC ---
Move(FNetwork.Device.MAC[0], FDevMAC[0], 6);
FDevConnected := True;
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
FVolume := G_Settings.Volume;
FDriveLevel := G_Settings.DriveLevel;
FActiveVfo := G_Settings.ActiveVfo;
FCurrentBand := G_Settings.LastBand;
// SampleRate — глобальный, загружаем до RestoreBand
if G_Settings.SampleRate > 0 then
begin
FSampleRate := G_Settings.SampleRate;
FSpanHz := FSampleRate;
end;
BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton;
BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton;
BtnSNB.Tag := Ord(G_Settings.SNBEnabled); UpdateSNBButton;
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
RestoreBand(FCurrentBand);
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
StatusBar1.Panels[1].Text := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType);
// General packet
FillChar(GenPkt, SizeOf(GenPkt), 0);
GenPkt.Command := CMD_GENERAL;
GenPkt.Flags37 := $08;
GenPkt.Flags38 := $01;
GenPkt.PAConfig := $01;
if FNetwork.Device.BoardType = 5 then
GenPkt.AlexEnable := $03
else
GenPkt.AlexEnable := $01;
FNetwork.SendGeneralPacket(GenPkt);
if FNetwork.Device.BoardType in [3, 4, 5] then
FActiveDDC := 2
else
FActiveDDC := 0;
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0);
// Синхронизируем samplerate WDSP если нужно
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(FSampleRate);
FWDSPReady := FDSPEngine.Initialized;
end;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
ApplyNoiseFilterButtonsToDSP;
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
// SetAGC вызывает UpdateAGCLines внутри — линии на спектре обновятся
end;
// DUC specific
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
DUCPkt.NumDACs := 1;
DUCPkt.SidetoneLevel := 50;
DUCPkt.SidetoneFreqLo := 600;
DUCPkt.KeyerSpeed := 20;
DUCPkt.KeyerWeight := 50;
DUCPkt.DUC0RateHi := Hi(192);
DUCPkt.DUC0RateLo := Lo(192);
DUCPkt.DUC0Bits := 24;
FNetwork.SendDUCSpecific(DUCPkt);
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, False, True, True);
FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel);
FRunning := True;
FRXStartTime := GetTickCount64;
FRXLastPktTime := 0;
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
if FWDSPReady then
FDSPEngine.UpdateAGCLines(FSpectrumWidth); // ещё не получили ни одного пакета
BtnStartStop.Caption := 'STOP';
StyleButton(BtnStartStop, True);
StatusBar1.Panels[4].Text := 'RUNNING ' + FormatFreq(FVfoA);
BtnMOX.Enabled := True;
end;
procedure TMainForm.FreqDispAChanged(Sender: TObject; NewFreq: Int64);
begin
ApplyVfoA(NewFreq);
end;
procedure TMainForm.FreqDispBChanged(Sender: TObject; NewFreq: Int64);
begin
FVfoB := NewFreq;
if FActiveVfo = 1 then
begin
FCenterFreq := FVfoB;
FCenterFreq := FVfoB;
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(FVfoB, FVfoB, FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
end;
// ---------------------------------------------------------------------------
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
// ---------------------------------------------------------------------------
procedure TMainForm.UpdateFilterButtons;
var
i: Integer;
Names: array[0..FILT_COUNT-1] of string;
DefIdx: Integer;
begin
case FMode of
0, 1: begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
DefIdx := FILT_SSB_DEF;
end;
3, 4: begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
DefIdx := FILT_CW_DEF;
end;
5: begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_FM_NAMES[i];
DefIdx := FILT_FM_DEF;
end;
else begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i];
DefIdx := FILT_AM_DEF;
end;
end;
FFilter := DefIdx;
case FMode of
0, 1: FFilterBW := FILT_SSB_BW[DefIdx];
3, 4: FFilterBW := FILT_CW_BW[DefIdx];
5: FFilterBW := FILT_FM_BW[DefIdx];
else FFilterBW := FILT_AM_BW[DefIdx];
end;
for i := 0 to FILT_COUNT-1 do
begin
BtnFilter[i].Caption := Names[i];
StyleButton(BtnFilter[i], i = FFilter);
end;
end;
// ---------------------------------------------------------------------------
// ApplyVfoA — единая точка смены частоты VFO A
//
// CTUN ВЫКЛ (классический режим):
// FCenterFreq = FVfoA — спектр центрирован на VFO, маркер всегда в центре
//
// CTUN ВКЛ (piHPSDR/Thetis режим):
// FCenterFreq статичен — спектр/водопад не двигаются
// FVfoA движется по дисплею (маркер гуляет)
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
// hits the edge of the display")
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
var
Offset: Double;
FiltLo: Double;
FiltHi: Double;
HalfSpan: Double;
Scrolled: Boolean;
begin
FVfoA := NewFreq;
Scrolled := False;
if not FCTun then
begin
// ---- CTUN OFF ----
// DDC = VFO, сдвига нет
FCenterFreq := FVfoA;
if FWDSPReady then
FDSPEngine.SetShift(0.0);
end
else
begin
// ---- CTUN ON ----
// DDC (FCenterFreq) стоит на месте.
// SetRXAShiftFreq сдвигает спектр внутри WDSP так чтобы
// демодулятор принимал сигнал на FVfoA, а не на FCenterFreq.
// Shift = FVfoA - FCenterFreq (в Гц)
Offset := FVfoA - FCenterFreq;
if FWDSPReady then
FDSPEngine.SetShift(Offset);
// Проверяем не вышла ли полоса фильтра за край дисплея
HalfSpan := FSpanHz / 2;
case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
begin
// Полоса вышла за край → прокручиваем центр
if FiltHi > HalfSpan then
FCenterFreq := FVfoA - HalfSpan * 0.5
else
FCenterFreq := FVfoA + HalfSpan * 0.5;
// Пересчитываем сдвиг после прокрутки
if FWDSPReady then
FDSPEngine.SetShift(FVfoA - FCenterFreq);
// DDC перестраивается на новый FCenterFreq
Scrolled := True;
end;
end;
// Обновляем VFO дисплей
FreqDispA.Frequency := Round(FVfoA);
// DDC: передаём FCenterFreq (при CTUN OFF = FVfoA, при CTUN ON = фиксирован)
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
// Перерисовываем спектр всегда (маркер и полоса двигаются)
// Если идёт drag — не вызываем Draw напрямую, таймер подхватит
if FSpecDrag then
FSpectrumDirty := True
else
begin
DrawSpectrum;
PbSpectrum.Invalidate;
if Scrolled then
begin
DrawWaterfall;
PbWaterfall.Invalidate;
end;
end;
end;
// ---------------------------------------------------------------------------
// CTUN toggle
// ---------------------------------------------------------------------------
procedure TMainForm.BtnCTunClick(Sender: TObject);
begin
FCTun := not FCTun;
StyleButton(BtnCTun, FCTun);
FBandCache[FCurrentBand].CTun := FCTun;
if not FCTun then
begin
// Выключили CTUN: центрируемся на VFO, shift=0
FCenterFreq := FVfoA;
if FWDSPReady then
FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end
else
begin
// Включили CTUN: фиксируем текущее положение дисплея
// FCenterFreq остаётся как есть — спектр не прыгает
end;
DrawSpectrum;
DrawWaterfall;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
// ---------------------------------------------------------------------------
// Клик/драг по спектру/водопаду
// ---------------------------------------------------------------------------
// Перевод X пикселя в частоту
function PixelToFreq(PixelX, PanelWidth: Integer;
CenterFreq, SpanHz: Double): Double;
begin
Result := CenterFreq + (PixelX / PanelWidth - 0.5) * SpanHz;
end;
procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
var
ClickFreq: Int64;
StepHz: Int64;
begin
if PanelWidth <= 0 then Exit;
StepHz := 100;
// Пиксель → частота через текущий центр дисплея (FCenterFreq)
ClickFreq := Round(FCenterFreq + (PixelX / PanelWidth - 0.5) * FSpanHz);
ClickFreq := (ClickFreq div StepHz) * StepHz;
ApplyVfoA(ClickFreq);
end;
procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
var
dPix: Integer;
dFreq: Double;
begin
if PanelWidth <= 0 then Exit;
dPix := PixelX - FSpecDragX0;
dFreq := dPix / PanelWidth * FSpanHz;
if FCTun then
begin
// CTUN ON: drag двигает окно просмотра (FCenterFreq/DDC)
// VFO остаётся, shift обновляется = FVfoA - новый FCenterFreq
FCenterFreq := FSpecDragFreq - dFreq;
if FWDSPReady then
FDSPEngine.SetShift(FVfoA - FCenterFreq);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
FSpectrumDirty := True; // таймер перерисует в ближайшем тике
end
else
begin
// CTUN OFF: drag двигает VFO, FCenterFreq следует через ApplyVfoA
ApplyVfoA(Round(FSpecDragFreq - dFreq));
end;
end;
// Спектр
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
// Левая кнопка: сбрасываем маркер если активен
if FMarkerActive then
begin
FMarkerActive := False;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
FSpecDrag := True;
FSpecDragX0 := X;
if FCTun then FSpecDragFreq := FCenterFreq
else FSpecDragFreq := FVfoA;
PbSpectrum.Cursor := crSizeWE;
end
else if Button = mbRight then
begin
// Правая кнопка: toggle маркера
if FMarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FMarkerX) < 8) then
begin
// Клик рядом с текущим маркером — убираем
FMarkerActive := False;
end
else
begin
// Ставим маркер в новое место
FMarkerActive := True;
FMarkerX := Round(X / PbSpectrum.Width * 1000);
end;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
end;
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
begin
if FSpecDrag and (ssLeft in Shift) then
DoSpectrumDrag(X, PbSpectrum.Width);
// Маркер следует за курсором — обновление произойдёт в ближайшем тике таймера (50ms)
if FMarkerActive then
begin
FMarkerX := Round(X / PbSpectrum.Width * 1000);
FSpectrumDirty := True;
end;
end;
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
if (Abs(X - FSpecDragX0) < 4) then
DoSpectrumClick(X, PbSpectrum.Width); // это был клик, не драг
FSpecDrag := False;
PbSpectrum.Cursor := crDefault;
end;
end;
// Водопад (аналогично)
procedure TMainForm.PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
if FMarkerActive then
begin
FMarkerActive := False;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
FSpecDrag := True;
FSpecDragX0 := X;
if FCTun then FSpecDragFreq := FCenterFreq
else FSpecDragFreq := FVfoA;
PbWaterfall.Cursor := crSizeWE;
end
else if Button = mbRight then
begin
if FMarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FMarkerX) < 8) then
FMarkerActive := False
else
begin
FMarkerActive := True;
FMarkerX := Round(X / PbWaterfall.Width * 1000);
end;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
end;
procedure TMainForm.PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
begin
if FSpecDrag and (ssLeft in Shift) then
DoSpectrumDrag(X, PbWaterfall.Width);
if FMarkerActive then
begin
FMarkerX := Round(X / PbWaterfall.Width * 1000);
FSpectrumDirty := True;
end;
end;
procedure TMainForm.PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
if (Abs(X - FSpecDragX0) < 4) then
DoSpectrumClick(X, PbWaterfall.Width);
FSpecDrag := False;
PbWaterfall.Cursor := crDefault;
end;
end;
procedure TMainForm.BtnBandClick(Sender: TObject);
var
NewBand: Integer;
begin
NewBand := (Sender as TFlatButton).Tag;
if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit;
if NewBand = FCurrentBand then Exit; // уже на этом диапазоне
// Сохраняем настройки текущего диапазона перед сменой
if FDevConnected then
begin
SaveCurrentBand;
FSettings.Save;
end;
// Переключаем диапазон
FCurrentBand := NewBand;
// Восстанавливаем настройки нового диапазона
RestoreBand(FCurrentBand);
end;
procedure TMainForm.ApplyModeFilter;
var
Lo, Hi, Half: Integer;
begin
Half := FFilterBW div 2;
case FMode of
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB: верхний срез -100 Гц
1: begin Lo := 100; Hi := FFilterBW; end; // USB
2: begin Lo := -Half; Hi := Half; end; // DSB
3: begin Lo := -Half; Hi := Half; end; // CWL
4: begin Lo := -Half; Hi := Half; end; // CWU
5: begin Lo := -Half; Hi := Half; end; // FM
6: begin Lo := -Half; Hi := Half; end; // AM
7: begin Lo := -Half; Hi := Half; end; // SAM
else Lo := -Half; Hi := Half;
end;
if FWDSPReady then
FDSPEngine.SetFilter(Lo, Hi);
end;
procedure TMainForm.BtnModeClick(Sender: TObject);
var
i, N: Integer;
begin
N := (Sender as TFlatButton).Tag;
FMode := N;
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then
FDSPEngine.SetMode(N);
UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим
ApplyModeFilter;
end;
procedure TMainForm.BtnFilterClick(Sender: TObject);
var
i, N: Integer;
begin
N := (Sender as TFlatButton).Tag;
FFilter := N;
// Устанавливаем полосу по текущему режиму
case FMode of
0, 1: FFilterBW := FILT_SSB_BW[N];
3, 4: FFilterBW := FILT_CW_BW[N];
5: FFilterBW := FILT_FM_BW[N];
else FFilterBW := FILT_AM_BW[N]; // DSB, AM, SAM
end;
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter);
ApplyModeFilter;
end;
procedure TMainForm.BtnVfoSwapClick(Sender: TObject);
var Tmp: Double;
begin
Tmp := FVfoA; FVfoA := FVfoB; FVfoB := Tmp;
UpdateVfoDisplay;
UpdateFilterButtons;
if FRunning then FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel);
end;
procedure TMainForm.BtnVfoACopyBClick(Sender: TObject);
begin
FVfoB := FVfoA; UpdateVfoDisplay;
end;
procedure TMainForm.BtnVfoBCopyAClick(Sender: TObject);
begin
FVfoA := FVfoB; UpdateVfoDisplay;
if FRunning then FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel);
end;
procedure TMainForm.BtnMOXClick(Sender: TObject);
begin
FTransmitting := not FTransmitting;
if FTransmitting then
begin
BtnMOX.ClrNorm := TColor($00000044);
BtnMOX.ClrText := TColor($000000FF);
BtnMOX.ClrTextAct := TColor($000000FF);
BtnMOX.Active := True;
end else
StyleButton(BtnMOX, False);
if FWDSPReady then FDSPEngine.SetTXRun(FTransmitting);
end;
procedure TMainForm.BtnMuteClick(Sender: TObject);
begin
FMuted := not FMuted;
StyleButton(BtnMute, FMuted);
if FMuted then BtnMute.Caption := 'UNMUTE'
else BtnMute.Caption := 'MUTE';
if FWDSPReady then FDSPEngine.SetMute(FMuted);
end;
procedure TMainForm.UpdateNRButton;
begin
case BtnNR.Tag of
1: BtnNR.Caption := 'NR';
2: BtnNR.Caption := 'NR2';
3: BtnNR.Caption := 'NR3';
4: BtnNR.Caption := 'NR4';
else
BtnNR.Caption := 'NR';
end;
StyleButton(BtnNR, BtnNR.Tag <> 0);
BtnNR.Invalidate;
end;
procedure TMainForm.UpdateNBButton;
begin
case BtnNB.Tag of
1: BtnNB.Caption := 'NB';
2: BtnNB.Caption := 'NB2';
else
BtnNB.Caption := 'NB';
end;
StyleButton(BtnNB, BtnNB.Tag <> 0);
BtnNB.Invalidate;
end;
procedure TMainForm.UpdateSNBButton;
begin
BtnSNB.Caption := 'SNB';
StyleButton(BtnSNB, BtnSNB.Tag <> 0);
BtnSNB.Invalidate;
end;
procedure TMainForm.UpdateANFButton;
begin
BtnANF.Caption := 'ANF';
StyleButton(BtnANF, BtnANF.Tag <> 0);
BtnANF.Invalidate;
end;
procedure TMainForm.ApplyNoiseFilterButtonsToDSP;
begin
if not FWDSPReady then Exit;
FDSPEngine.SetNRMode(BtnNR.Tag);
FDSPEngine.SetNBMode(BtnNB.Tag);
FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
FDSPEngine.SetANF(BtnANF.Tag <> 0);
end;
procedure TMainForm.BtnNRClick(Sender: TObject);
begin
BtnNR.Tag := (BtnNR.Tag + 1) mod 5;
UpdateNRButton;
if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag);
end;
procedure TMainForm.BtnNBClick(Sender: TObject);
begin
BtnNB.Tag := (BtnNB.Tag + 1) mod 3;
UpdateNBButton;
if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag);
end;
procedure TMainForm.BtnSNBClick(Sender: TObject);
begin
BtnSNB.Tag := 1 - BtnSNB.Tag;
UpdateSNBButton;
if FWDSPReady then FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
end;
procedure TMainForm.BtnANFClick(Sender: TObject);
begin
BtnANF.Tag := 1 - BtnANF.Tag;
UpdateANFButton;
if FWDSPReady then FDSPEngine.SetANF(BtnANF.Tag <> 0);
end;
procedure TMainForm.BtnWfAGCClick(Sender: TObject);
begin
FWfAGCEnabled := not FWfAGCEnabled;
StyleButton(BtnWfAGC, FWfAGCEnabled);
if not FWfAGCEnabled then
FWfHigh := -50.0; // сбрасываем к умолчанию при выключении
end;
procedure TMainForm.BtnWfNFClick(Sender: TObject);
begin
FWfNFEnabled := not FWfNFEnabled;
StyleButton(BtnWfNF, FWfNFEnabled);
if not FWfNFEnabled then
FWfLow := -120.0;
end;
procedure TMainForm.BtnHidePanelClick(Sender: TObject);
begin
FPanelHidden := not FPanelHidden;
PanelLeft.Visible := not FPanelHidden;
if FPanelHidden then
begin
BtnHidePanel.Caption := '▶ SHOW';
// PbSpectrum — TGraphicControl, не TWinControl, используем PanelRight
FVfoOverlay.Parent := PanelRight;
FVfoOverlay.Width := 260;
FVfoOverlay.Height := 130;
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
FVfoOverlay.Visible := True;
FVfoOverlay.BringToFront;
PositionVfoOverlay;
end
else
begin
BtnHidePanel.Caption := '◀ HIDE';
FVfoOverlay.Visible := False;
end;
ResizeSMeter;
end;
procedure TMainForm.PositionVfoOverlay;
var
VfoX, OW, SpTop: Integer;
begin
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
OW := FVfoOverlay.Width;
SpTop := PbSpectrum.Top; // смещение PbSpectrum внутри PanelRight
if PbSpectrum.Width > 0 then
VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * PbSpectrum.Width)
else
VfoX := PbSpectrum.Width div 2;
// Всегда справа от VFO-линии
FVfoOverlay.Left := Min(PanelRight.Width - OW - 4, VfoX + 6);
FVfoOverlay.Top := SpTop + 6;
end;
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
begin
// Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели)
end;
procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
begin
FMode := Mode;
FFilterBW := FilterBW;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
ApplyModeFilter;
end;
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
begin
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
end;
end;
procedure TMainForm.BtnSpanClick(Sender: TObject);
var
NewRate: Integer;
DDCRate: Word;
begin
NewRate := (Sender as TFlatButton).Tag;
if NewRate = FSampleRate then Exit;
FSampleRate := NewRate;
FSpanHz := NewRate;
DDCRate := NewRate div 1000;
// Обновляем кнопки сразу — UI отзывчив
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
Application.ProcessMessages;
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
// SampleRate — глобальный: сохраняем в global settings
if FDevConnected then
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
// 1. Останавливаем WDSP и сбрасываем очередь старых пакетов
if FWDSPReady then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(NewRate); // Close + FlushQueue + Open
end;
// 2. Отправляем новый rate трансиверу
// (после остановки DSP — новые пакеты сразу идут с правильным rate)
if FNetwork.Connected then
FNetwork.ConfigureDDCs(1, DDCRate, 0);
// 3. Восстанавливаем настройки DSP
FWDSPReady := FDSPEngine.Initialized;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
ApplyNoiseFilterButtonsToDSP;
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
if FNetwork.Connected then
FDSPEngine.SetShift(FVfoA - FCenterFreq);
end;
end;
// ===========================================================================
// Веб-интерфейс: callbacks от TWebServer (вызываются из WS-потока)
// Все изменения состояния выполняем через TThread.Synchronize чтобы
// не трогать UI и WDSP из чужого потока.
// ===========================================================================
procedure TMainForm.WebOnFreq(Hz: Double);
begin
FWebSyncFreq := Hz;
FWebSyncM := SyncWebFreq;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebFreq;
begin
FVfoA := FWebSyncFreq;
if not FCTun then FCenterFreq := FVfoA;
UpdateVfoDisplay;
if FWDSPReady then
begin
FDSPEngine.SetShift(FVfoA - FCenterFreq);
if FRunning then
FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel);
end;
end;
procedure TMainForm.WebOnMode(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebMode;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebMode;
var i: Integer;
begin
if (FWebSyncInt < 0) or (FWebSyncInt >= MODE_COUNT) then Exit;
FMode := FWebSyncInt;
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then FDSPEngine.SetMode(FMode);
UpdateFilterButtons;
ApplyModeFilter;
FBandCache[FCurrentBand].Mode := FMode;
end;
procedure TMainForm.WebOnFilter(BW: Integer);
begin
FWebSyncInt := BW;
FWebSyncM := SyncWebFilter;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebFilter;
var
best, dist, i, j: Integer;
BW_arr: array[0..FILT_COUNT-1] of Integer;
Idx: Integer;
begin
// Negative value = -(idx+1) means filter index was passed directly
if FWebSyncInt < 0 then
begin
Idx := (-FWebSyncInt) - 1;
if (Idx >= 0) and (Idx < FILT_COUNT) then
begin
FFilter := Idx;
case FMode of
0,1: FFilterBW := FILT_SSB_BW[Idx];
3,4: FFilterBW := FILT_CW_BW[Idx];
5: FFilterBW := FILT_FM_BW[Idx];
else FFilterBW := FILT_AM_BW[Idx];
end;
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
ApplyModeFilter;
FBandCache[FCurrentBand].FilterBW := FFilterBW;
end;
Exit;
end;
// Positive value = bandwidth in Hz, find closest filter
FFilterBW := FWebSyncInt;
case FMode of
0,1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
3,4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
5: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end;
best := 0; dist := MaxInt;
for i := 0 to FILT_COUNT-1 do
if Abs(BW_arr[i] - FWebSyncInt) < dist then
begin
dist := Abs(BW_arr[i] - FWebSyncInt);
best := i;
end;
FFilter := best;
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
ApplyModeFilter;
FBandCache[FCurrentBand].FilterBW := FFilterBW;
end;
procedure TMainForm.WebOnAGC(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebAGC;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebAGC;
var i: Integer;
begin
if (FWebSyncInt < 0) or (FWebSyncInt > 4) then Exit;
FAGCMode := FWebSyncInt;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
if FWDSPReady then
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
FBandCache[FCurrentBand].AGCMode := FAGCMode;
end;
procedure TMainForm.WebOnAGCTop(DB: Integer);
begin
FWebSyncInt := DB;
FWebSyncM := SyncWebAGCTop;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebAGCTop;
begin
FAGCTop := Max(20, Min(120, FWebSyncInt));
TrkAGC.Position := FAGCTop;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
FBandCache[FCurrentBand].AGCTop := FAGCTop;
end;
procedure TMainForm.WebOnBand(Idx: Integer);
begin
FWebSyncInt := Idx;
FWebSyncM := SyncWebBand;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebBand;
begin
if (FWebSyncInt < 0) or (FWebSyncInt >= BAND_COUNT) then Exit;
if FWebSyncInt = FCurrentBand then Exit;
if FDevConnected then begin SaveCurrentBand; FSettings.Save; end;
FCurrentBand := FWebSyncInt;
RestoreBand(FCurrentBand);
end;
procedure TMainForm.WebOnSpan(Hz: Integer);
begin
FWebSyncInt := Hz;
FWebSyncM := SyncWebSpan;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebSpan;
var
ValidSpans: array[0..5] of Integer;
Found: Boolean;
i: Integer;
Btn: TFlatButton;
begin
ValidSpans[0]:=48000; ValidSpans[1]:=96000; ValidSpans[2]:=192000;
ValidSpans[3]:=384000; ValidSpans[4]:=768000; ValidSpans[5]:=1536000;
Found := False;
for i := 0 to 5 do
if ValidSpans[i] = FWebSyncInt then begin Found := True; Break; end;
if not Found then Exit;
if FWebSyncInt = FSampleRate then Exit;
Btn := nil;
if FWebSyncInt = 48000 then Btn := BtnSpan48k
else if FWebSyncInt = 96000 then Btn := BtnSpan96k
else if FWebSyncInt = 192000 then Btn := BtnSpan192k
else if FWebSyncInt = 384000 then Btn := BtnSpan384k
else if FWebSyncInt = 768000 then Btn := BtnSpan768k
else if FWebSyncInt = 1536000 then Btn := BtnSpan1536k;
if Assigned(Btn) then BtnSpanClick(Btn);
end;
procedure TMainForm.WebOnVolume(V: Integer);
begin
FWebSyncInt := V;
FWebSyncM := SyncWebVolume;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebVolume;
begin
FVolume := Max(0, Min(100, FWebSyncInt));
TrkVolume.Position := FVolume;
if FWDSPReady then FDSPEngine.SetVolume(FVolume / 100.0);
end;
procedure TMainForm.WebOnWfAGC(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebWfAGC;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebWfAGC;
begin
FWfAGCEnabled := FWebSyncBool;
StyleButton(BtnWfAGC, FWfAGCEnabled);
end;
procedure TMainForm.WebOnWfNF(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebWfNF;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebWfNF;
begin
FWfNFEnabled := FWebSyncBool;
StyleButton(BtnWfNF, FWfNFEnabled);
end;
// ── Run ────────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnRun(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebRun;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebRun;
begin
// Toggle only if state differs
if FWebSyncBool <> FRunning then
BtnStartStopClick(nil);
end;
// ── Mute ───────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnMute(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebMute;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebMute;
begin
if FWebSyncBool <> FMuted then
BtnMuteClick(nil);
end;
// ── CTUN ──────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnCtun(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebCtun;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebCtun;
begin
if FWebSyncBool <> FCTun then
BtnCTunClick(nil);
end;
// ── NR ────────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnNR(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebNR;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebNR;
begin
if FWebSyncInt < 0 then FWebSyncInt := 0;
if FWebSyncInt > 4 then FWebSyncInt := 4;
if BtnNR.Tag <> FWebSyncInt then
begin
BtnNR.Tag := FWebSyncInt;
UpdateNRButton;
if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag);
end;
end;
// ── NB ────────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnNB(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebNB;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebNB;
begin
if FWebSyncInt < 0 then FWebSyncInt := 0;
if FWebSyncInt > 2 then FWebSyncInt := 2;
if BtnNB.Tag <> FWebSyncInt then
begin
BtnNB.Tag := FWebSyncInt;
UpdateNBButton;
if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag);
end;
end;
procedure TMainForm.WebOnSNB(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebSNB;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebSNB;
begin
if FWebSyncBool <> (BtnSNB.Tag <> 0) then
BtnSNBClick(BtnSNB);
end;
// ── ANF ───────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnANF(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebANF;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebANF;
begin
if FWebSyncBool <> (BtnANF.Tag <> 0) then
BtnANFClick(BtnANF);
end;
procedure TMainForm.TrkDriveChange(Sender: TObject);
begin
FDriveLevel := Round(TrkDrive.Position / 100.0 * 255);
if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
end;
procedure TMainForm.TrkVolumeChange(Sender: TObject);
begin
FVolume := TrkVolume.Position;
if FWDSPReady then
FDSPEngine.SetVolume(FVolume / 100.0);
end;
// ===========================================================================
// Mouse wheel — перестройка VFO
// ===========================================================================
procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
// Колёсико на форме/спектре/водопаде — меняет VFO с квантованием
// Шаг зависит от модификаторов: Ctrl+Shift=1МГц, Ctrl=1кГц, Shift=10Гц, иначе 100Гц
// Квантование: результат выровнен до кратного шагу (как в FreqDisplay)
// CTUN логика: через ApplyVfoA
var
Step: Int64;
Delta: Int64;
Base: Int64;
NewFreq: Int64;
begin
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
else if ssCtrl in Shift then Step := 1000
else if ssShift in Shift then Step := 10
else Step := 100;
Delta := 1;
if WheelDelta < 0 then Delta := -1;
// Квантование (та же логика что в FreqDisplay.ChangeByDigit)
Base := (Round(FVfoA) div Step) * Step;
if Delta > 0 then
NewFreq := Base + Step
else
begin
if Round(FVfoA) = Base then
NewFreq := Base - Step
else
NewFreq := Base; // снэп к нижней границе
end;
NewFreq := Max(30000, Min(60000000, NewFreq));
ApplyVfoA(NewFreq);
Handled := True;
end;
procedure TMainForm.BtnAGCModeClick(Sender: TObject);
const
AGCModes: array[0..4] of TWDSPAGCMode = (
agcFast, agcMedium, agcSlow, agcLong, agcOff);
var
i, N: Integer;
begin
N := (Sender as TFlatButton).Tag;
FAGCMode := N;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = N);
if FWDSPReady then
FDSPEngine.SetAGC(AGCModes[N], 50.0);
FBandCache[FCurrentBand].AGCMode := FAGCMode;
end;
procedure TMainForm.TrkAGCChange(Sender: TObject);
begin
FAGCTop := TrkAGC.Position;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
// Обновляем кэш диапазона
FBandCache[FCurrentBand].AGCTop := FAGCTop;
DrawSpectrum;
PbSpectrum.Invalidate;
end;
// ===========================================================================
// DSP/Audio callbacks — вызываются из рабочих потоков
// ===========================================================================
procedure TMainForm.OnAudioReady(const Left, Right: array of Single;
Count: Integer);
var
MonoBuf: array[0..1023] of Single;
i: Integer;
begin
if Assigned(FWebServer) and FWebServer.WebClientActive then
begin
for i := 0 to Count - 1 do
MonoBuf[i] := (Left[i] + Right[i]) * 0.5;
FWebServer.PushAudio(@MonoBuf[0], Count);
Exit;
end;
FAudioOut.Write(Left, Right, Count);
end;
procedure TMainForm.OnSpectrumReady(const Pixels: array of Single;
Count: Integer);
var
i, N: Integer;
begin
// Храним RAW данные от WDSP (всегда 1024 точки).
// Интерполяция на ширину экрана делается в DrawSpectrum/DrawWaterfall.
// Вызывается из DSP-потока — Single-запись атомарна.
N := Min(Count, 1024);
for i := 0 to N - 1 do
FSpectrumBuf[i] := Pixels[i];
FSpectrumDirty := True;
end;
procedure TMainForm.AfterShowTick(Sender: TObject);
begin
// Вызывается один раз через 200 мс после старта формы
FAfterShowTimer.Enabled := False;
// Открываем PortAudio — теперь FPC уже перехватывает сигналы
try
if not FAudioOut.Open then
StatusBar1.Panels[3].Text := 'Audio: ' + FAudioOut.LastError
else
StatusBar1.Panels[3].Text := 'Audio OK (PortAudio)';
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Audio: ' + E.Message;
end;
end;
end.