start work tx

This commit is contained in:
2026-03-26 22:50:55 +03:00
parent 7664907b3e
commit ae532a33cd
9 changed files with 1373 additions and 149 deletions
+148 -26
View File
@@ -56,10 +56,25 @@ type
statusFlags: LongWord;
userData: Pointer): LongInt; cdecl;
TPaDeviceInfo = record
structVersion: Integer;
name: PAnsiChar;
hostApi: Integer;
maxInputChannels: Integer;
maxOutputChannels: Integer;
defaultLowInputLatency: TPaTime;
defaultLowOutputLatency: TPaTime;
defaultHighInputLatency: TPaTime;
defaultHighOutputLatency:TPaTime;
defaultSampleRate: Double;
end;
PPaDeviceInfo = ^TPaDeviceInfo;
TPa_Initialize = function: TPaError; cdecl;
TPa_Terminate = function: TPaError; cdecl;
TPa_GetDefaultOutputDevice = function: TPaDeviceIndex; cdecl;
TPa_GetDeviceInfo = function(device: TPaDeviceIndex): Pointer; cdecl;
TPa_GetDeviceCount = function: TPaDeviceIndex; cdecl;
TPa_GetDeviceInfo = function(device: TPaDeviceIndex): PPaDeviceInfo; cdecl;
TPa_OpenStream = function(stream: PPaStream;
inputParam: PPaStreamParameters;
outputParam: PPaStreamParameters;
@@ -89,10 +104,13 @@ type
FOutPt: Integer; // audio_buffer_outpt (read)
FMutex: TCriticalSection;
FDeviceIndex: Integer; // -1 = default output device
// PortAudio functions
FPa_Initialize: TPa_Initialize;
FPa_Terminate: TPa_Terminate;
FPa_GetDefaultOutputDevice: TPa_GetDefaultOutputDevice;
FPa_GetDeviceCount: TPa_GetDeviceCount;
FPa_GetDeviceInfo: TPa_GetDeviceInfo;
FPa_OpenStream: TPa_OpenStream;
FPa_StartStream: TPa_StartStream;
FPa_StopStream: TPa_StopStream;
@@ -106,16 +124,28 @@ type
constructor Create(SampleRate: Integer = 48000);
destructor Destroy; override;
function Initialize: Boolean; // загружает PA, вызывает Pa_Initialize (без открытия стрима)
function Open: Boolean;
procedure Close;
// Перечисление устройств вывода: заполняет Names списком имён.
// Возвращает True если PA инициализирована и устройства доступны.
// Indices[i] — индекс PA устройства для Names[i].
function EnumOutputDevices(Names: TStrings; out Indices: array of Integer;
out Count: Integer): Boolean;
// Найти индекс устройства по имени (-1 если не найдено)
function FindDeviceByName(const AName: string): Integer;
// Пишем стерео double сэмплы — как audio_write() в оригинале
procedure WriteDouble(Left, Right: Double);
// Convenience: массив Single
procedure Write(const Left, Right: array of Single; Count: Integer);
property IsOpen: Boolean read FOpen;
property LastError: string read GetLastError;
property IsOpen: Boolean read FOpen;
property LastError: string read GetLastError;
property DeviceIndex: Integer read FDeviceIndex write FDeviceIndex;
property SampleRate: Integer read FSampleRate write FSampleRate;
end;
// Глобальный callback (cdecl, не метод)
@@ -202,12 +232,13 @@ end;
constructor TAudioOutput.Create(SampleRate: Integer);
begin
inherited Create;
FSampleRate := SampleRate;
FOpen := False;
FPAInited := False;
FStream := nil;
FLibHandle := NilHandle;
FLastError := '';
FSampleRate := SampleRate;
FDeviceIndex := -1; // -1 = default output device
FOpen := False;
FPAInited := False;
FStream := nil;
FLibHandle := NilHandle;
FLastError := '';
FInPt := 0;
FOutPt := 0;
FillChar(FBuf, SizeOf(FBuf), 0);
@@ -258,6 +289,8 @@ begin
FPa_Initialize := TPa_Initialize(GetProcAddress(FLibHandle, 'Pa_Initialize'));
FPa_Terminate := TPa_Terminate(GetProcAddress(FLibHandle, 'Pa_Terminate'));
FPa_GetDefaultOutputDevice := TPa_GetDefaultOutputDevice(GetProcAddress(FLibHandle, 'Pa_GetDefaultOutputDevice'));
FPa_GetDeviceCount := TPa_GetDeviceCount(GetProcAddress(FLibHandle, 'Pa_GetDeviceCount'));
FPa_GetDeviceInfo := TPa_GetDeviceInfo(GetProcAddress(FLibHandle, 'Pa_GetDeviceInfo'));
FPa_OpenStream := TPa_OpenStream(GetProcAddress(FLibHandle, 'Pa_OpenStream'));
FPa_StartStream := TPa_StartStream(GetProcAddress(FLibHandle, 'Pa_StartStream'));
FPa_StopStream := TPa_StopStream(GetProcAddress(FLibHandle, 'Pa_StopStream'));
@@ -275,6 +308,105 @@ begin
Result := True;
end;
// ---------------------------------------------------------------------------
// Initialize — загружает PA библиотеку и вызывает Pa_Initialize без открытия стрима.
// Нужно для перечисления устройств в настройках до старта аудио.
// ---------------------------------------------------------------------------
function TAudioOutput.Initialize: Boolean;
var
Err: TPaError;
begin
Result := False;
if not LoadLib then Exit;
if FPAInited then begin Result := True; Exit; end;
Err := FPa_Initialize();
if Err <> PA_NO_ERROR then
begin
FLastError := 'Pa_Initialize: ';
if Assigned(FPa_GetErrorText) then
FLastError := FLastError + string(FPa_GetErrorText(Err))
else
FLastError := FLastError + IntToStr(Err);
Exit;
end;
FPAInited := True;
Result := True;
end;
// ---------------------------------------------------------------------------
// EnumOutputDevices — перечисляет устройства вывода.
// Names получает имена устройств; Indices — соответствующие PA-индексы;
// Count — реальное количество (может быть меньше Length(Indices)).
// ---------------------------------------------------------------------------
function TAudioOutput.EnumOutputDevices(Names: TStrings;
out Indices: array of Integer; out Count: Integer): Boolean;
var
I, N: Integer;
Info: PPaDeviceInfo;
DevName: string;
begin
Count := 0;
Result := False;
if not Initialize then Exit;
if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit;
N := FPa_GetDeviceCount();
if N <= 0 then Exit;
for I := 0 to N - 1 do
begin
Info := FPa_GetDeviceInfo(I);
if Info = nil then Continue;
if Info^.maxOutputChannels <= 0 then Continue;
if Count >= Length(Indices) then Break;
if Info^.name <> nil then
DevName := string(AnsiString(Info^.name))
else
DevName := Format('Device %d', [I]);
Names.Add(DevName);
Indices[Count] := I;
Inc(Count);
end;
Result := Count > 0;
end;
// ---------------------------------------------------------------------------
// FindDeviceByName — возвращает PA device index по имени (-1 если не найден).
// ---------------------------------------------------------------------------
function TAudioOutput.FindDeviceByName(const AName: string): Integer;
var
I, N: Integer;
Info: PPaDeviceInfo;
DevName: string;
begin
Result := -1;
if AName = '' then Exit;
if not Initialize then Exit;
if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit;
N := FPa_GetDeviceCount();
for I := 0 to N - 1 do
begin
Info := FPa_GetDeviceInfo(I);
if Info = nil then Continue;
if Info^.maxOutputChannels <= 0 then Continue;
if Info^.name <> nil then
DevName := string(AnsiString(Info^.name))
else
DevName := Format('Device %d', [I]);
if SameText(DevName, AName) then
begin
Result := I;
Exit;
end;
end;
end;
// ---------------------------------------------------------------------------
// Open — точная последовательность как в audio_open_output()
// ---------------------------------------------------------------------------
@@ -287,28 +419,18 @@ var
begin
Result := False;
if FOpen then begin Result := True; Exit; end;
if not LoadLib then Exit;
if not Initialize then Exit; // загрузка PA + Pa_Initialize
// Pa_Initialize — один раз (как в audio_get_cards)
if not FPAInited then
// Выбор устройства: FDeviceIndex или default
if FDeviceIndex >= 0 then
Dev := FDeviceIndex
else
begin
Err := FPa_Initialize();
if Err <> PA_NO_ERROR then
begin
FLastError := 'Pa_Initialize: ';
if Assigned(FPa_GetErrorText) then
FLastError := FLastError + string(FPa_GetErrorText(Err))
else
FLastError := FLastError + IntToStr(Err);
Exit;
end;
FPAInited := True;
Dev := FPa_GetDefaultOutputDevice();
end;
Dev := FPa_GetDefaultOutputDevice();
if Dev = PA_NO_DEV then
begin
FLastError := 'No default output device';
FLastError := 'No output device available';
Exit;
end;
+33 -21
View File
@@ -19,7 +19,7 @@ uses
{$ELSE}
Sockets, BaseUnix,
{$ENDIF}
HPSDRProtocol;
HPSDRProtocol, SyncObjs;
{$IFDEF WINDOWS}
// ---------------------------------------------------------------------------
@@ -129,6 +129,8 @@ type
FPAEnabled: Boolean;
FAlexEnabled: Boolean;
FSendLock: TCriticalSection; // защита concurrent UDP sends
function DoCreateSocket: TSocket;
procedure DoCloseSocket(var S: TSocket);
function DoSendTo(S: TSocket; const Buf; BufLen: Integer;
@@ -423,11 +425,13 @@ begin
FIsTransmitting := False;
FPAEnabled := True;
FAlexEnabled := True;
FSendLock := TCriticalSection.Create;
end;
destructor THPSDRNetwork.Destroy;
begin
Disconnect;
FSendLock.Free;
inherited;
end;
@@ -529,7 +533,12 @@ begin
Addr.sin_family := AF_INET;
Addr.sin_port := htons(DestPort);
Addr.sin_addr := StrToNetAddr(DestIP);
Result := fpSendTo(S, @Buf, BufLen, 0, @Addr, SizeOf(Addr)) = BufLen;
FSendLock.Enter;
try
Result := fpSendTo(S, @Buf, BufLen, 0, @Addr, SizeOf(Addr)) = BufLen;
finally
FSendLock.Leave;
end;
end;
function THPSDRNetwork.DoRecvFrom(S: TSocket; var Buf; BufLen: Integer;
@@ -767,19 +776,11 @@ end;
procedure THPSDRNetwork.HandleMicData(const Buf: array of Byte; Len: Integer);
var
Pkt: TMicDataPacket;
Sync: TMicSync;
M: TThreadMethod;
Pkt: TMicDataPacket;
begin
if not Assigned(FOnMic) then Exit;
Move(Buf[0], Pkt, SizeOf(Pkt));
Sync := TMicSync.Create(FOnMic, Pkt);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
FOnMic(Pkt); // прямой вызов из receive thread — TX обработка не касается UI
end;
// ---------------------------------------------------------------------------
@@ -820,8 +821,14 @@ begin
Result := 0;
// TX relay при передаче
// Bit 27 ($08000000) = T/R relay для всех плат
// Bit 18 ($00040000) = TxRx Status для Orion MkII / Saturn / G2
if Transmitting then
Result := Result or $08000000; // ALEX_TX_RELAY
begin
Result := Result or $08000000; // ALEX_TX_RELAY bit27
if IsOrion2 then
Result := Result or $00040000; // TxRx Status bit18 (ANAN-7/8000, Saturn)
end;
// RX HPF (для ANAN-100/200) или BPF (для ANAN-7000)
if IsOrion2 then
@@ -876,15 +883,15 @@ var
begin
FillChar(Pkt, SizeOf(Pkt), 0);
// ANAN-7000/8000 имеют 2 ADC
if FDevice.BoardType in [4, 5] then // ORION=4, ORION2=5
// ANAN-7000/8000/Saturn имеют 2 ADC
if FDevice.BoardType in [4, 5, 10] then // ORION=4, ORION2=5, SATURN=10
Pkt.NumADCs := 2
else
Pkt.NumADCs := 1;
// Для ANGELIA/ORION/ORION2 DDC начинается с индекса 2 (DDC0/1 — PureSignal)
// Для ANGELIA/ORION/ORION2/SATURN DDC начинается с индекса 2 (DDC0/1 — PureSignal)
// Для HERMES/HL2 — с 0
if FDevice.BoardType in [3, 4, 5] then // ANGELIA=3, ORION=4, ORION2=5
if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION2=5, SATURN=10
DDCBase := 2
else
DDCBase := 0;
@@ -960,11 +967,16 @@ begin
Inc(FSeqHP);
// Byte 4: Run | PTT
if FRunning then Buf[4] := HP_RUN;
if FRunning then
begin
Buf[4] := HP_RUN;
if FIsTransmitting then Buf[4] := Buf[4] or HP_PTT0;
end;
// DDC base: ORION/ORION2/ANGELIA начинают с DDC2
IsOrion2 := (FDevice.BoardType = 5); // ORION-II = 5
if FDevice.BoardType in [3, 4, 5] then // ANGELIA=3, ORION=4, ORION2=5
// DDC base: ORION/ORION2/ANGELIA/SATURN начинают с DDC2
// IsOrion2: платы с Alex BPF и TxRx Status bit18 (ORION_MK2=5, SATURN=10)
IsOrion2 := FDevice.BoardType in [5, 10]; // ORION_MK2=5, SATURN=10
if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION_MK2=5, SATURN=10
DDCBase := 2
else
DDCBase := 0;
+569 -46
View File
@@ -26,7 +26,7 @@ uses
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput,
WDSPEngine, AudioOutput, AudioInput,
IntfGraphics, FPImage,
Settings,
WebServer;
@@ -150,8 +150,9 @@ type
FSWRV: Double;
FSupplyV: Double;
FPLLLock: Boolean;
FHWPTT: Boolean;
public
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL: Boolean);
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean);
procedure Execute;
end;
@@ -180,9 +181,12 @@ type
FPendingBoardType: Integer; // BoardType устройства для подключения
// ---- DSP + Audio ----
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FWDSPReady: Boolean; // True когда WDSP открыт и работает
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FAudioIn: TAudioInput;
FAudioOutDevName: string; // текущее имя выходного устройства
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
FWDSPReady: Boolean; // True когда WDSP открыт и работает
// ---- State ----
FVfoA: Double;
@@ -203,8 +207,11 @@ type
FMarkerActive: Boolean; // True = линия видима
FMarkerX: Integer; // X в пикселях (относительно ширины панели)
FDriveLevel: Byte;
FPAMaxPower: Double;
FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон
FRunning: Boolean;
FTransmitting: Boolean;
FHWPTTActive: Boolean; // True = hardware PTT нажата
FMuted: Boolean;
FVolume: Integer;
FLastSMeter: Double;
@@ -223,16 +230,25 @@ type
// ---- Spectrum ----
FSpectrumBuf: array[0..1023] of Single;
FWfAvgBuf: array[0..1023] of Single;
FWaterfallBuf: array[0..1023] of Single; // WDSP waterfall pixout=1 data
FSpectrumBitmap: TBitmap;
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FWfHigh: Double;
FWfLow: Double;
// Spectrum grid settings
FSpecRefLevel: Double; // dBm top of scale (e.g. -20)
FSpecRange: Double; // dB range (e.g. 110 → bottom = -130)
FSpecGridStep: Double; // grid step in dB (e.g. 10)
FWaterfallBitmap:TBitmap;
FWaterfallTemp: TBitmap;
FSpectrumWidth: Integer;
FSpectrumHeight: Integer;
FWaterfallHeight:Integer;
// ---- Visibility settings ----
FShowSpectrum: Boolean;
FShowWaterfall: Boolean;
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30)
// ---- Splitter между спектром и водопадом ----
FSplitter: TPanel; // визуальная полоса-разделитель
FSplitterDrag: Boolean; // True = идёт перетаскивание
@@ -268,6 +284,7 @@ type
FWebSyncBool: Boolean;
FWebSyncM: TThreadMethod;
FCurrentBand: Integer; // текущий активный диапазон 0..10
FSettingsForm: TObject; // TSettingsForm (cast при использовании)
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
@@ -283,8 +300,9 @@ type
// ---- Toolbar ----
PanelToolbar: TPanel;
BtnDiscover: TFlatButton;
BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
BtnSettings: TFlatButton; // открыть диалог настроек
// ---- Left panel ----
PanelLeft: TPanel;
@@ -397,10 +415,11 @@ type
// DSP/Audio callbacks (вызываются из рабочих потоков)
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(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 DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
@@ -503,11 +522,18 @@ type
procedure BtnWfAGCClick(Sender: TObject);
procedure BtnWfNFClick(Sender: TObject);
procedure BtnHidePanelClick(Sender: TObject);
procedure BtnSettingsClick(Sender: TObject);
procedure InvalidateGridCache;
procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject);
procedure TrkVolumeChange(Sender: TObject);
function CalcDriveByte: Byte;
function ActiveTXFreqHz: Double;
procedure ApplyMOX(Active: Boolean);
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject);
procedure PbFwdPowerPaint(Sender: TObject);
@@ -527,6 +553,18 @@ type
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
public
// Методы для SettingsForm — немедленное применение настроек
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
SpecAvgCount: Integer; SpecBackmult: Double);
procedure ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
WfBackmult: Double);
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
procedure ApplyFPS(FPS: Integer);
published
// Обработчики событий формы — должны быть в published для RTTI/LFM
procedure FormCreate(Sender: TObject);
@@ -542,6 +580,8 @@ var
implementation
uses SettingsForm;
{$R *.lfm}
// ===========================================================================
@@ -581,7 +621,7 @@ begin
end;
constructor TStatusUISync.Create(AForm: TObject;
FW, SW, SV: Double; PLL: Boolean);
FW, SW, SV: Double; PLL, HWPTT: Boolean);
begin
inherited Create;
FForm := AForm;
@@ -589,11 +629,12 @@ begin
FSWRV := SW;
FSupplyV := SV;
FPLLLock := PLL;
FHWPTT := HWPTT;
end;
procedure TStatusUISync.Execute;
begin
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock);
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock, FHWPTT);
end;
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
@@ -647,9 +688,13 @@ begin
end;
function TMainForm.MakeGlobalSettings: TGlobalSettings;
var i: Integer;
begin
Result.Volume := FVolume;
Result.DriveLevel := FDriveLevel;
Result.DriveLevel := TrkDrive.Position;
Result.PAMaxPower := FPAMaxPower;
for i := 0 to BAND_COUNT - 1 do
Result.PABandCal[i] := FPABandCal[i];
Result.ActiveVfo := FActiveVfo;
Result.NRMode := BtnNR.Tag;
Result.NBMode := BtnNB.Tag;
@@ -661,6 +706,43 @@ begin
Result.WfNFEnabled := FWfNFEnabled;
Result.LastBand := FCurrentBand;
Result.SampleRate := FSampleRate;
// Display settings
if FWDSPReady then
begin
Result.FFTSize := FDSPEngine.FFTSize;
Result.WindowType := FDSPEngine.WindowType;
Result.SpecDetector := FDSPEngine.SpecDetector;
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
Result.SpecAvgCount := FDSPEngine.SpecAvgCount;
Result.SpecBackmult := FDSPEngine.SpecBackmult;
Result.WfDetector := FDSPEngine.WfDetector;
Result.WfAvgMode := FDSPEngine.WfAvgMode;
Result.WfAvgCount := FDSPEngine.WfAvgCount;
Result.WfBackmult := FDSPEngine.WfBackmult;
end
else
begin
Result.FFTSize := 4096;
Result.WindowType := 5;
Result.SpecDetector := 0;
Result.SpecAvgMode := 3;
Result.SpecAvgCount := 2;
Result.SpecBackmult := 0.45;
Result.WfDetector := 2;
Result.WfAvgMode := 3;
Result.WfAvgCount := 2;
Result.WfBackmult := 0.45;
end;
Result.SpecRefLevel := FSpecRefLevel;
Result.SpecRange := FSpecRange;
Result.SpecGridStep := FSpecGridStep;
Result.AudioSampleRate := FAudioOut.SampleRate;
Result.ShowSpectrum := FShowSpectrum;
Result.ShowWaterfall := FShowWaterfall;
Result.DisplayFPS := FDisplayFPS;
// Audio device names
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
end;
procedure TMainForm.SaveCurrentBand;
@@ -751,7 +833,7 @@ end;
procedure TMainForm.FormCreate(Sender: TObject);
var
bi_, StartupRate: Integer;
bi_, StartupRate, i: Integer;
StartupVfoA, StartupVfoB: Double;
begin
FVfoA := 14200000;
@@ -765,9 +847,11 @@ begin
FCTun := False;
FFilterBW := 2700;
FSpecDrag := False;
FDriveLevel := 100;
FRunning := False;
FTransmitting := False;
FDriveLevel := 0;
FPAMaxPower := 100.0;
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
FRunning := False;
FTransmitting := False;
FMuted := False;
FVolume := 70;
FCenterFreq := FVfoA;
@@ -782,6 +866,11 @@ begin
FWfAGCEnabled := False;
FWfNFEnabled := False;
FDevConnected := False;
// Spectrum grid settings defaults
FSpecRefLevel := -20.0;
FSpecRange := 110.0;
FSpecGridStep := 10.0;
FSettingsForm := nil;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями
@@ -847,7 +936,10 @@ begin
FVfoOverlay := TVfoOverlay.Create(Self);
FVfoOverlay.OnSelect := OnModeFilterSelect;
FVfoOverlay.Visible := False;
FPanelHidden := False;
FPanelHidden := False;
FShowSpectrum := True;
FShowWaterfall := True;
FDisplayFPS := 20;
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
FSplitterDrag := False;
@@ -894,12 +986,15 @@ begin
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
// Audio output — создаём объект сейчас, открываем после показа формы
// Audio output/input — создаём объекты сейчас, открываем после показа формы
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
FAudioOut := TAudioOutput.Create(48000);
FAudioIn := TAudioInput.Create(48000);
FMeterTimer := TTimer.Create(Self);
FMeterTimer.Interval := 100;
@@ -937,6 +1032,8 @@ begin
FNetwork.Free;
FAudioOut.Close;
FAudioOut.Free;
FAudioIn.Close;
FAudioIn.Free;
FDSPEngine.Close;
FDSPEngine.Free;
FSpectrumBitmap.Free;
@@ -1034,6 +1131,7 @@ begin
X := 4;
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick);
// ---- Status bar ----
StatusBar1 := TStatusBar.Create(Self);
@@ -1546,6 +1644,125 @@ begin
TopOff := PanelSpanButtons.Height;
// S-метр позиционируется отдельно в ResizeSMeter
ResizeSMeter;
// ---- Оба скрыты ----
if (not FShowSpectrum) and (not FShowWaterfall) then
begin
PbSpectrum.Visible := False;
PbRuler.Visible := False;
PanelSplitter.Visible := False;
PbWaterfall.Visible := False;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := 0;
PositionSpanOverlayButtons;
Exit;
end;
// ---- Только спектр (водопад скрыт) ----
if FShowSpectrum and (not FShowWaterfall) then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_SH then AvailH := MIN_SH;
SH := AvailH;
WH := 0;
PbSpectrum.SetBounds(0, TopOff, RW, SH);
PbSpectrum.Visible := True;
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
PbRuler.Visible := True;
PanelSplitter.Visible := False;
PbWaterfall.Visible := False;
PositionSpanOverlayButtons;
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := 0;
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) then
begin
FWaterfallBitmap.SetSize(1, 1);
FWfBitmapW := 0;
FWfBitmapH := 0;
SetLength(FWfPixels, 0);
end;
if (FRulerBitmap <> nil) and (RW > 0) then
begin
FRulerBitmap.SetSize(RW, RULER_H);
FRulerLastFreq := -1.0;
FRulerLastVfo := -1.0;
end;
if RW > 0 then
begin
DrawSpectrum;
PbSpectrum.Invalidate;
PbRuler.Invalidate;
end;
Exit;
end;
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
if (not FShowSpectrum) and FShowWaterfall then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_WH then AvailH := MIN_WH;
SH := 0;
WH := AvailH;
PbSpectrum.Visible := False;
PanelSplitter.Visible := False;
PbWaterfall.SetBounds(0, TopOff, RW, WH);
PbWaterfall.Visible := True;
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
PbRuler.Visible := True;
PositionSpanOverlayButtons;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := WH;
if (FSpectrumBitmap <> nil) then
begin
FSpectrumBitmap.SetSize(1, 1);
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, RULER_H);
FRulerLastFreq := -1.0;
FRulerLastVfo := -1.0;
end;
if RW > 0 then
begin
DrawWaterfall;
PbWaterfall.Invalidate;
PbRuler.Invalidate;
end;
Exit;
end;
// ---- Оба видимы: стандартный режим со сплиттером ----
PbSpectrum.Visible := True;
PbRuler.Visible := True;
PanelSplitter.Visible := True;
PbWaterfall.Visible := True;
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
if AvailH < (MIN_SH + MIN_WH) then Exit;
@@ -1564,8 +1781,7 @@ begin
// Линейка частот — всегда прижата к низу спектра
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
// Сплиттер — между линейкой и водопадом
if PanelSplitter <> nil then
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
// Водопад — под сплиттером
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
@@ -1814,12 +2030,21 @@ 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;
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
for i := 0 to 1023 do FWaterfallBuf[i] := -120.0;
FWfHigh := -50.0;
FWfLow := -120.0;
end;
procedure TMainForm.InvalidateGridCache;
// Сбрасываем кэш фона/сетки — перерисуется на следующем тике DrawSpectrum
begin
FGridBitmapW := 0;
FGridBitmapH := 0;
FSpectrumDirty := True;
end;
procedure TMainForm.FillDemoSpectrum;
var
i: Integer;
@@ -2035,7 +2260,8 @@ begin
if (W <= 0) or (H <= 0) then Exit;
C := FSpectrumBitmap.Canvas;
DBmin := -130.0; DBmax := -20.0;
DBmax := FSpecRefLevel;
DBmin := FSpecRefLevel - FSpecRange;
InvRange := 1.0 / (DBmax - DBmin);
// ── 1. Фон+сетка из кэша ───────────────────────────────────────────────
@@ -2056,7 +2282,7 @@ begin
GC.Font.Name := 'Courier New';
GC.Font.Color := TColor($00A8C9D9);
GC.Brush.Style := bsClear;
dB := -40.0;
dB := DBmax - FSpecGridStep;
while dB >= DBmin do
begin
Yp := Round((DBmax - dB) * InvRange * H);
@@ -2064,7 +2290,7 @@ begin
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;
dB := dB - FSpecGridStep;
end;
for i := 0 to 8 do
begin
@@ -2206,14 +2432,31 @@ begin
H := FWaterfallBitmap.Height;
if (W <= 0) or (H <= 0) then Exit;
// ── EMA + Peak + NF — один цикл по 1024 бинам ─────────────────────────
// ── Источник данных водопада ─────────────────────────────────────────────
// При работе WDSP: FWaterfallBuf заполнен из pixout=1 (уже усреднённые WDSP данные)
// Без WDSP (demo): делаем EMA FSpectrumBuf → FWfAvgBuf
PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
for X := 0 to 1023 do
if FWDSPReady then
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;
// Используем WDSP waterfall данные напрямую
for X := 0 to 1023 do
begin
AvgDB := FWaterfallBuf[X];
FWfAvgBuf[X] := AvgDB; // синхронизируем для AGC логики
if AvgDB > PeakDB then PeakDB := AvgDB;
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
end;
end
else
begin
// Fallback: Pascal EMA от демо-спектра
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;
end;
// ── AGC High/Low ────────────────────────────────────────────────────────
@@ -2789,10 +3032,16 @@ var
NowTick: QWord;
ElapsedTick: QWord;
begin
// Синхронизируем размеры если PaintBox изменился (после ресайза)
if (PbSpectrum.Width <> FSpectrumWidth) or
(PbSpectrum.Height <> FSpectrumHeight) or
(FSpectrumWidth = 0) then
// Синхронизируем размеры если размер панели изменился (после ресайза).
// Важно: проверяем только видимые панели — когда спектр скрыт,
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
// что вызвало бы сброс буфера водопада на каждом тике.
if (FSpectrumWidth = 0) or
(FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or
(PbSpectrum.Height <> FSpectrumHeight))) or
(FShowWaterfall and not FShowSpectrum and
((PbWaterfall.Width <> FSpectrumWidth) or
(PbWaterfall.Height <> FWaterfallHeight))) then
ResizeSpectrumPanels;
ResizeSMeter;
if FSpectrumWidth <= 0 then Exit;
@@ -2977,7 +3226,8 @@ begin
else
SWRV := 1.0;
Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV,
(Status.StatusBits and HPS_PLL_LOCKED) <> 0);
(Status.StatusBits and HPS_PLL_LOCKED) <> 0,
(Status.StatusBits and HPS_PTT) <> 0);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
@@ -2986,7 +3236,7 @@ begin
end;
end;
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
var
PLLStr: string;
begin
@@ -2998,7 +3248,15 @@ begin
StatusBar1.Panels[2].Text :=
Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s',
[SupplyV, FwdW, SWRV, PLLStr]);
// Panels[3] зарезервирована для RX stats (DDC seq/pkt count)
// Hardware PTT (foot switch / mic PTT) — обнаружение фронта
if HWPTT <> FHWPTTActive then
begin
FHWPTTActive := HWPTT;
// Hardware PTT включает/выключает передачу только если BtnMOX не нажата
if not BtnMOX.Active then
ApplyMOX(HWPTT);
end;
end;
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
@@ -3034,7 +3292,8 @@ end;
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
begin
// TODO: WDSP TXA
if FTransmitting and FWDSPReady then
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
end;
// ===========================================================================
@@ -3147,8 +3406,10 @@ begin
end;
FWDSPReady := False;
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
end;
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
@@ -3297,6 +3558,7 @@ var
GenPkt: TGeneralPacket;
DUCPkt: TDUCSpecificPacket;
G_Settings: TGlobalSettings;
i: Integer;
begin
BtnStartStop.Enabled := True;
@@ -3312,8 +3574,14 @@ begin
FDevConnected := True;
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
FVolume := G_Settings.Volume;
FDriveLevel := G_Settings.DriveLevel;
FActiveVfo := G_Settings.ActiveVfo;
// PA settings
FPAMaxPower := G_Settings.PAMaxPower;
for i := 0 to BAND_COUNT - 1 do
FPABandCal[i] := G_Settings.PABandCal[i];
// Slider position (0..100) and calibrated drive byte
TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100);
FDriveLevel := CalcDriveByte;
FCurrentBand := G_Settings.LastBand;
// SampleRate — глобальный, загружаем до RestoreBand
if G_Settings.SampleRate > 0 then
@@ -3327,6 +3595,53 @@ begin
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
FShowSpectrum := G_Settings.ShowSpectrum;
FShowWaterfall := G_Settings.ShowWaterfall;
if G_Settings.DisplayFPS > 0 then
ApplyFPS(G_Settings.DisplayFPS);
// Настройки дисплея и сетки
if G_Settings.FFTSize > 0 then
begin
FSpecRefLevel := G_Settings.SpecRefLevel;
FSpecRange := G_Settings.SpecRange;
if G_Settings.SpecGridStep > 0 then FSpecGridStep := G_Settings.SpecGridStep;
InvalidateGridCache;
if FWDSPReady then
begin
FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode,
G_Settings.SpecAvgCount, G_Settings.SpecBackmult);
FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
G_Settings.WfAvgCount, G_Settings.WfBackmult);
end;
// Аудио устройство вывода
FAudioOut.Close;
if G_Settings.AudioSampleRate > 0 then
FAudioOut.SampleRate := G_Settings.AudioSampleRate;
if G_Settings.AudioOutDevice <> '' then
begin
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(G_Settings.AudioOutDevice);
FAudioOutDevName := G_Settings.AudioOutDevice;
end
else
begin
FAudioOut.DeviceIndex := -1;
FAudioOutDevName := '';
end;
FAudioOut.Open;
// Аудио устройство ввода (TX mic)
if G_Settings.AudioInDevice <> '' then
begin
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
FAudioInDevName := G_Settings.AudioInDevice;
FAudioIn.Open;
end
else
begin
FAudioIn.DeviceIndex := -1;
FAudioInDevName := '';
end;
end;
RestoreBand(FCurrentBand);
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
@@ -4117,9 +4432,14 @@ begin
end;
end;
procedure TMainForm.BtnMOXClick(Sender: TObject);
function TMainForm.ActiveTXFreqHz: Double;
begin
FTransmitting := not FTransmitting;
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
end;
procedure TMainForm.ApplyMOX(Active: Boolean);
begin
FTransmitting := Active;
if FTransmitting then
begin
BtnMOX.ClrNorm := TColor($00000044);
@@ -4128,7 +4448,19 @@ begin
BtnMOX.Active := True;
end else
StyleButton(BtnMOX, False);
if FWDSPReady then FDSPEngine.SetTXRun(FTransmitting);
if FWDSPReady then
FDSPEngine.SetTXRun(FTransmitting);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel,
FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
procedure TMainForm.BtnMOXClick(Sender: TObject);
begin
ApplyMOX(not FTransmitting);
end;
procedure TMainForm.BtnMuteClick(Sender: TObject);
@@ -4784,11 +5116,62 @@ end;
procedure TMainForm.TrkDriveChange(Sender: TObject);
begin
FDriveLevel := Round(TrkDrive.Position / 100.0 * 255);
FDriveLevel := CalcDriveByte;
if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
end;
function TMainForm.CalcDriveByte: Byte;
var Cal: Double;
begin
if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 100.0;
Result := Round(TrkDrive.Position / 100.0 * Cal / 100.0 * 255.0);
end;
procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer);
var
IData: array[0..239] of Integer;
QData: array[0..239] of Integer;
Offset, n, i: Integer;
begin
if not FNetwork.Connected then Exit;
Offset := 0;
while Offset < Count do
begin
FillChar(IData, SizeOf(IData), 0);
FillChar(QData, SizeOf(QData), 0);
n := Count - Offset;
if n > 240 then n := 240;
for i := 0 to n - 1 do
begin
IData[i] := Round(Buf[(Offset + i) * 2] * 8388607.0);
QData[i] := Round(Buf[(Offset + i) * 2 + 1] * 8388607.0);
end;
FNetwork.SendDUCIQ(IData, QData);
Inc(Offset, 240);
end;
end;
procedure TMainForm.OnPASettingsChange(MaxPower: Double;
const BandCal: array of Double);
var i: Integer;
begin
FPAMaxPower := MaxPower;
for i := 0 to BAND_COUNT - 1 do
begin
if i <= High(BandCal) then
FPABandCal[i] := BandCal[i]
else
FPABandCal[i] := 100.0;
end;
FDriveLevel := CalcDriveByte;
if FNetwork.Connected and FNetwork.Running then
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
end;
procedure TMainForm.TrkVolumeChange(Sender: TObject);
begin
FVolume := TrkVolume.Position;
@@ -4903,6 +5286,146 @@ begin
FSpectrumDirty := True;
end;
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
Count: Integer);
var
i, N: Integer;
begin
// Заполняем буфер водопада из WDSP pixout=1.
// Вызывается из DSP-потока — Single-запись атомарна.
N := Min(Count, 1024);
for i := 0 to N - 1 do
FWaterfallBuf[i] := Pixels[i];
end;
// ===========================================================================
// Settings apply methods (вызываются из TSettingsForm)
// ===========================================================================
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
SpecAvgCount: Integer; SpecBackmult: Double);
begin
if FWDSPReady then
FDSPEngine.SetFFTParams(FFTSize, WinType);
if FWDSPReady then
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgCount, SpecBackmult);
end;
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
WfBackmult: Double);
begin
if FWDSPReady then
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgCount, WfBackmult);
end;
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
begin
FSpecRefLevel := RefLevel;
FSpecRange := Range;
if GridStep > 0 then FSpecGridStep := GridStep;
InvalidateGridCache;
end;
procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string);
begin
if FAudioOut.IsOpen then
FAudioOut.Close;
FAudioOut.DeviceIndex := DevIndex;
FAudioOutDevName := DevName;
try
if not FAudioOut.Open then
StatusBar1.Panels[3].Text := 'Audio out: ' + FAudioOut.LastError
else
StatusBar1.Panels[3].Text := 'Audio out: ' + IfThen(DevName = '', 'Default', DevName);
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Audio out err: ' + E.Message;
end;
end;
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
begin
if FAudioIn.IsOpen then
FAudioIn.Close;
FAudioIn.DeviceIndex := DevIndex;
FAudioInDevName := DevName;
if DevIndex >= 0 then
begin
try
if not FAudioIn.Open then
StatusBar1.Panels[3].Text := 'Mic: ' + FAudioIn.LastError
else
StatusBar1.Panels[3].Text := 'Mic: ' + DevName;
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
end;
end
else
StatusBar1.Panels[3].Text := 'Mic: none';
end;
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
begin
FShowSpectrum := ShowSpectrum;
FShowWaterfall := ShowWaterfall;
ResizeSpectrumPanels;
end;
procedure TMainForm.ApplyFPS(FPS: Integer);
begin
if FPS < 1 then FPS := 1;
if FPS > 60 then FPS := 60;
FDisplayFPS := FPS;
FSpectrumTimer.Interval := 1000 div FPS;
end;
procedure TMainForm.BtnSettingsClick(Sender: TObject);
var
SF: TSettingsForm;
begin
if not Assigned(FSettingsForm) then
begin
SF := TSettingsForm.Create(Self);
FSettingsForm := SF;
SF.OnDisplayChange := ApplyDisplayParams;
SF.OnWaterfallChange := ApplyWaterfallParams;
SF.OnGridChange := ApplyGridParams;
SF.OnAudioDevChange := ApplyAudioDevice;
SF.OnAudioInDevChange := ApplyAudioInputDevice;
SF.OnVisibilityChange := ApplyVisibility;
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
end;
SF := TSettingsForm(FSettingsForm);
SF.LoadPASettings(FPAMaxPower, FPABandCal);
// Перечисляем PA устройства
SF.RefreshAudioDevices(FAudioOut, FAudioIn);
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
SF.LoadFPS(FDisplayFPS);
// Загружаем текущие значения
if FWDSPReady then
SF.LoadValues(
FDSPEngine.FFTSize,
FDSPEngine.WindowType,
FDSPEngine.SpecDetector,
FDSPEngine.SpecAvgMode,
FDSPEngine.SpecAvgCount,
FDSPEngine.SpecBackmult,
FDSPEngine.WfDetector,
FDSPEngine.WfAvgMode,
FDSPEngine.WfAvgCount,
FDSPEngine.WfBackmult,
FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName)
else
SF.LoadValues(
4096, 5, 0, 3, 2, 0.45,
2, 3, 2, 0.45,
FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName);
SF.Show;
end;
procedure TMainForm.AfterShowTick(Sender: TObject);
begin
+115 -1
View File
@@ -48,6 +48,32 @@ type
WindowTop: Integer;
WindowWidth: Integer;
WindowHeight: Integer;
// --- Display settings ---
FFTSize: Integer; // 1024,2048,4096,8192,16384
WindowType: Integer; // 0=Rect,1=BH4,2=Hann,3=FlatTop,4=Hamming,5=Kaiser,6=BH7
SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
SpecAvgCount: Integer; // frames to average
SpecBackmult: Double; // 0.0..1.0 recursive averaging multiplier
WfDetector: Integer; // waterfall detector
WfAvgMode: Integer; // waterfall average mode
WfAvgCount: Integer; // waterfall average count
WfBackmult: Double; // waterfall backmult
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
SpecGridStep: Double; // grid step in dB, e.g. 10
// --- Audio settings ---
AudioOutDevice: string; // PortAudio output device name ('' = default)
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
AudioInDevice: string; // PortAudio input device name for TX mic ('' = none)
// --- Visibility settings ---
ShowSpectrum: Boolean; // True = draw spectrum on main form
ShowWaterfall: Boolean; // True = draw waterfall on main form
// --- Display FPS ---
DisplayFPS: Integer; // spectrum/waterfall timer fps (5..30)
// --- PA (Power Amplifier) settings ---
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
end;
TSettingsManager = class
@@ -61,9 +87,11 @@ type
function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
function JD(O: TJSONObject; const K: string; Def: Double): Double;
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
function JS(O: TJSONObject; const K: string; const Def: string): string;
procedure JW(O: TJSONObject; const K: string; V: Integer); overload;
procedure JW(O: TJSONObject; const K: string; V: Double); overload;
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
procedure JWS(O: TJSONObject; const K: string; const V: string);
public
constructor Create(const FilePath: string = SETTINGS_FILE);
destructor Destroy; override;
@@ -110,12 +138,35 @@ begin
end;
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
var i: Integer;
begin
FillChar(G, SizeOf(G), 0);
G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0;
G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False;
G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5;
G.SampleRate := 192000;
G.PAMaxPower := 100.0;
for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0;
// Display defaults
G.FFTSize := 4096;
G.WindowType := 5; // Kaiser
G.SpecDetector := 0; // Peak
G.SpecAvgMode := 3; // Log Recursive
G.SpecAvgCount := 2;
G.SpecBackmult := 0.45;
G.WfDetector := 2; // Average
G.WfAvgMode := 3; // Log Recursive
G.WfAvgCount := 2;
G.WfBackmult := 0.45;
G.SpecRefLevel := -20.0;
G.SpecRange := 110.0;
G.SpecGridStep := 10.0;
G.AudioOutDevice := '';
G.AudioSampleRate := 48000;
G.AudioInDevice := '';
G.ShowSpectrum := True;
G.ShowWaterfall := True;
G.DisplayFPS := 20;
end;
constructor TSettingsManager.Create(const FilePath: string);
@@ -226,6 +277,17 @@ begin
O.Add(K, V);
end;
function TSettingsManager.JS(O: TJSONObject; const K: string; const Def: string): string;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsString; except Result:=Def; end else Result:=Def; end;
procedure TSettingsManager.JWS(O: TJSONObject; const K: string; const V: string);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
function TSettingsManager.LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean;
@@ -249,6 +311,32 @@ begin
G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False);
G.LastBand := JI(GObj,'last_band',5);
G.SampleRate := JI(GObj,'sample_rate',192000);
// Display settings
G.FFTSize := JI(GObj,'fft_size',4096);
G.WindowType := JI(GObj,'win_type',5);
G.SpecDetector := JI(GObj,'spec_detector',0);
G.SpecAvgMode := JI(GObj,'spec_avg_mode',3);
G.SpecAvgCount := JI(GObj,'spec_avg_count',2);
G.SpecBackmult := JD(GObj,'spec_backmult',0.45);
G.WfDetector := JI(GObj,'wf_detector',2);
G.WfAvgMode := JI(GObj,'wf_avg_mode',3);
G.WfAvgCount := JI(GObj,'wf_avg_count',2);
G.WfBackmult := JD(GObj,'wf_backmult',0.45);
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
G.SpecRange := JD(GObj,'spec_range',110.0);
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
// Audio settings
G.AudioOutDevice := JS(GObj,'audio_out_device','');
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
G.AudioInDevice := JS(GObj,'audio_in_device','');
// Visibility settings
G.ShowSpectrum := JB(GObj,'show_spectrum',True);
G.ShowWaterfall := JB(GObj,'show_waterfall',True);
G.DisplayFPS := JI(GObj,'display_fps',20);
// PA settings
G.PAMaxPower := JD(GObj,'pa_max_power',100.0);
for i := 0 to CFG_BAND_COUNT-1 do
G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i),100.0);
for i := 0 to CFG_BAND_COUNT-1 do
begin
@@ -268,7 +356,7 @@ end;
procedure TSettingsManager.SaveGlobal(const MAC: array of Byte;
const G: TGlobalSettings);
var O: TJSONObject;
var O: TJSONObject; i: Integer;
begin
O := GetGlobalObj(GetDevObj(MacToStr(MAC)));
JW(O,'volume',G.Volume); JW(O,'drive_level',G.DriveLevel);
@@ -283,6 +371,32 @@ begin
JW(O,'wf_nf_enabled',G.WfNFEnabled);
JW(O,'last_band',G.LastBand);
JW(O,'sample_rate',G.SampleRate);
// Display settings
JW(O,'fft_size',G.FFTSize);
JW(O,'win_type',G.WindowType);
JW(O,'spec_detector',G.SpecDetector);
JW(O,'spec_avg_mode',G.SpecAvgMode);
JW(O,'spec_avg_count',G.SpecAvgCount);
JW(O,'spec_backmult',G.SpecBackmult);
JW(O,'wf_detector',G.WfDetector);
JW(O,'wf_avg_mode',G.WfAvgMode);
JW(O,'wf_avg_count',G.WfAvgCount);
JW(O,'wf_backmult',G.WfBackmult);
JW(O,'spec_ref_level',G.SpecRefLevel);
JW(O,'spec_range',G.SpecRange);
JW(O,'spec_grid_step',G.SpecGridStep);
// Audio settings
JWS(O,'audio_out_device',G.AudioOutDevice);
JW(O,'audio_sample_rate',G.AudioSampleRate);
JWS(O,'audio_in_device',G.AudioInDevice);
// Visibility settings
JW(O,'show_spectrum',G.ShowSpectrum);
JW(O,'show_waterfall',G.ShowWaterfall);
JW(O,'display_fps',G.DisplayFPS);
// PA settings
JW(O,'pa_max_power',G.PAMaxPower);
for i := 0 to CFG_BAND_COUNT-1 do
JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]);
end;
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
+173
View File
@@ -30,6 +30,10 @@ uses
FlatButton, AudioOutput, AudioInput;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
TOnPASettingsChange = procedure(MaxPower: Double;
const BandCal: array of Double) of object;
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
SpecAvgMode, SpecAvgCount: Integer; SpecBackmult: Double) of object;
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer;
@@ -92,10 +96,15 @@ type
FChkShowWaterfall: TCheckBox;
FCmbFPS: TComboBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TSpinEdit;
FEdBandCal: array[0..10] of TFloatSpinEdit;
// ---- Close button ----
FBtnClose: TFlatButton;
// ---- Callbacks ----
FOnPAChange: TOnPASettingsChange;
FOnDisplayChange: TOnDisplayParamChange;
FOnWaterfallChange: TOnWaterfallParamChange;
FOnGridChange: TOnGridParamChange;
@@ -111,6 +120,7 @@ type
procedure BuildAudioTab;
procedure BuildDisplayTab;
procedure BuildRX1Tab;
procedure BuildPATab;
procedure ApplyTheme;
// Helpers
@@ -139,6 +149,8 @@ type
procedure OnTXDevChange(Sender: TObject);
procedure OnVisibilityChkChange(Sender: TObject);
procedure OnFPSCmbChange(Sender: TObject);
procedure OnMaxPowerChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange;
@@ -171,7 +183,10 @@ type
procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
// Загрузить текущий FPS (без fire события)
procedure LoadFPS(FPS: Integer);
// Загрузить PA настройки (без fire событий)
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange;
@@ -216,6 +231,9 @@ const
FPS_VALUES: array[0..5] of Integer = (5, 10, 15, 20, 25, 30);
FPS_NAMES: array[0..5] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps');
PA_BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
{ TSettingsForm }
constructor TSettingsForm.Create(AOwner: TComponent);
@@ -312,6 +330,7 @@ begin
BuildAudioTab;
BuildDisplayTab;
BuildPATab;
// Close button
FBtnClose := TFlatButton.Create(Self);
@@ -1030,6 +1049,160 @@ begin
end;
end;
// ---------------------------------------------------------------------------
// BuildPATab — вкладка PA Settings: MaxPower + per-band calibration
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildPATab;
const
LBL_W = 46; // ширина метки диапазона
ED_W = 72; // ширина SpinEdit (с кнопками)
COL_W = LBL_W + ED_W + 8; // ширина одной пары
COLS = 4; // колонок на строку
ROW_H = 32; // высота строки (spin edits чуть выше plain edit)
GRP_PAD = 12; // отступ внутри группы
GRP_TOP = 24; // высота caption группы
ROWS = 3; // строк для 11 диапазонов (ceil(11/4))
function MakeFloatSpin(AParent: TWinControl; ALeft, ATop: Integer;
AChange: TNotifyEvent): TFloatSpinEdit;
var E: TFloatSpinEdit;
begin
E := TFloatSpinEdit.Create(Self);
E.Parent := AParent;
E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2);
E.Color := CLR_PANEL;
E.Font.Color := CLR_TEXT;
E.Font.Name := 'Courier New';
E.Font.Size := 8;
E.MinValue := 38.8;
E.MaxValue := 100.0;
E.Increment := 0.1;
E.DecimalPlaces := 1;
E.Value := 100.0;
E.OnChange := AChange;
Result := E;
end;
var
Grp: TGroupBox;
Sep: TPanel;
Lbl: TLabel;
Spin: TSpinEdit;
i, r, c, X, Y: Integer;
begin
// ── Top padding ───────────────────────────────────────────────────────────
Sep := TPanel.Create(Self);
Sep.Parent := FTabPA;
Sep.BevelOuter := bvNone;
Sep.Color := CLR_BG;
Sep.Align := alTop;
Sep.Height := 10;
// ── Group Power & Calibration ─────────────────────────────────────────────
Grp := TGroupBox.Create(Self);
Grp.Parent := FTabPA;
Grp.Caption := 'Power & Calibration';
Grp.Color := CLR_GROUP;
Grp.Font.Color := CLR_ACCENT;
Grp.Font.Name := 'Courier New';
Grp.Font.Size := 8;
Grp.Align := alTop;
Grp.Height := GRP_TOP + ROW_H + 20 + ROWS * ROW_H + GRP_PAD + 16;
Grp.AutoSize := False;
// Max Power строка
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Max Power (W):';
Lbl.SetBounds(GRP_PAD, GRP_TOP + 6, 120, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := 'Courier New';
Lbl.Font.Size := 8;
Spin := TSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(GRP_PAD + 124, GRP_TOP, ED_W, BTN_H + 2);
Spin.Color := CLR_PANEL;
Spin.Font.Color := CLR_TEXT;
Spin.Font.Name := 'Courier New';
Spin.Font.Size := 8;
Spin.MinValue := 5;
Spin.MaxValue := 200;
Spin.Increment := 1;
Spin.Value := 100;
Spin.OnChange := OnMaxPowerChange;
FEdMaxPower := Spin;
// Метка для калибровки
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Calibration per band (38.8 ... 100.0):';
Lbl.SetBounds(GRP_PAD, GRP_TOP + ROW_H + 8, 280, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := 'Courier New';
Lbl.Font.Size := 8;
// Grid 4 columns × 3 rows for 11 bands
for i := 0 to 10 do
begin
r := i div COLS;
c := i mod COLS;
X := GRP_PAD + c * (COL_W + 4);
Y := GRP_TOP + ROW_H + 26 + r * ROW_H;
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := PA_BAND_NAMES[i] + ':';
Lbl.SetBounds(X, Y + 5, LBL_W, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := 'Courier New';
Lbl.Font.Size := 8;
FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange);
FEdBandCal[i].Tag := i;
end;
end;
procedure TSettingsForm.LoadPASettings(MaxPower: Double;
const BandCal: array of Double);
var i, n: Integer;
begin
FLoading := True;
try
FEdMaxPower.Value := Round(EnsureRange(MaxPower, 5.0, 200.0));
n := Min(10, High(BandCal));
for i := 0 to n do
FEdBandCal[i].Value := EnsureRange(BandCal[i], 38.8, 100.0);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnMaxPowerChange(Sender: TObject);
var
BandCal: array[0..10] of Double;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnPAChange) then Exit;
for i := 0 to 10 do
BandCal[i] := FEdBandCal[i].Value;
FOnPAChange(FEdMaxPower.Value, BandCal);
end;
procedure TSettingsForm.OnBandCalChange(Sender: TObject);
var
BandCal: array[0..10] of Double;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnPAChange) then Exit;
for i := 0 to 10 do
BandCal[i] := FEdBandCal[i].Value;
FOnPAChange(FEdMaxPower.Value, BandCal);
end;
procedure TSettingsForm.BtnCloseClick(Sender: TObject);
begin
Close;
+327 -55
View File
@@ -103,6 +103,9 @@ const
IQ_QUEUE_SIZE = 64; // кол-во слотов (степень двойки для AND-маски)
IQ_PKT_MAXBYTES = 1428; // max байт IQ данных в пакете (238*6)
// TX mic ring buffer — должен быть степенью двойки
TX_MIC_RING = 8192;
type
// Пакет в очереди между сетевым и DSP потоком
TIQQueueItem = record
@@ -118,6 +121,11 @@ type
Count: Integer) of object;
TOnSpectrumReady = procedure(const Pixels: array of Single;
Count: Integer) of object;
// TX IQ callback — вызывается из TTXDSPThread при готовности блока
// Buf: interleaved [I0,Q0,I1,Q1,...] doubles, Count — кол-во IQ пар
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
TOnWaterfallReady = procedure(const Pixels: array of Single;
Count: Integer) of object;
{ TWDSPEngine }
TWDSPEngine = class
@@ -140,8 +148,8 @@ type
FRXAccQ: array of Double;
FRXAccPos: Integer;
// Snapshot буфер для Spectrum0
FSnapBuf: array of Double;
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSpecBuf: array of Double;
// Аудио выходные буферы — аллоцируем один раз
FOutL: array of Single;
FOutR: array of Single;
@@ -152,14 +160,36 @@ type
FQueueTail: Integer; // читает DSP поток
FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent)
FDSPRunning: Boolean;
// Double буфер для Spectrum0 (принимает PDouble, не PdINREAL)
FSpecBuf: array of Double; // Double буфер для Spectrum0 (PDouble)
FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
FFlp: array[0..0] of Integer; // for SetAnalyzer flp parameter
FOnAudio: TOnAudioReady;
FOnSpectrum: TOnSpectrumReady;
// Настройки анализатора
FFFTSize: Integer;
FWindowType: Integer;
FSpecDetector: Integer;
FSpecAvgMode: Integer;
FSpecAvgCount: Integer;
FSpecBackmult: Double;
FWfDetector: Integer;
FWfAvgMode: Integer;
FWfAvgCount: Integer;
FWfBackmult: Double;
FOnAudio: TOnAudioReady;
FOnSpectrum: TOnSpectrumReady;
FOnWaterfall: TOnWaterfallReady;
FOnTXIQ: TOnTXIQReady;
// TX mic ring buffer (lock-free: один writer — receive thread,
// один reader — TX DSP thread)
FTXMicRing: array[0..TX_MIC_RING-1] of Double;
FTXMicHead: Integer; // пишет receive thread
FTXMicTail: Integer; // читает TX DSP thread
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
FTXThread: TThread;
FTXActive: Boolean;
FMode: Integer;
FSMeter: Double;
@@ -187,11 +217,13 @@ type
procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenAnalyzer;
procedure CloseAnalyzer;
procedure ApplyAnalyzerSettings;
procedure ApplyNRState;
procedure ApplyNBState;
procedure ApplySNBState;
procedure ApplyANFState;
procedure ApplyNoiseFilterState;
procedure ProcessTXBlock; // вызывается из TTXDSPThread
public
constructor Create(SampleRate: Integer = 192000;
@@ -236,6 +268,15 @@ type
procedure SetMicGain(GainDB: Double);
procedure SetTXRun(Run: Boolean);
// TX mic вход — вызывается из receive thread (thread-safe lock-free)
// Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов
procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
// Настройки анализатора (применяются немедленно если открыт)
procedure SetFFTParams(FFTSize, WinType: Integer);
procedure SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
procedure SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer; Backmult: Double);
// Spectrum — вызывать из таймера (~20 fps)
procedure UpdateSpectrum;
procedure GetSpectrumData(var Pixels: array of Single; var Count: Integer);
@@ -259,12 +300,69 @@ type
property SNBEnabled: Boolean read FSNBEnabled;
property LastError: string read FLastError;
property SMeter: Double read FSMeter;
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
property FFTSize: Integer read FFFTSize;
property WindowType: Integer read FWindowType;
property SpecDetector: Integer read FSpecDetector;
property SpecAvgMode: Integer read FSpecAvgMode;
property SpecAvgCount: Integer read FSpecAvgCount;
property SpecBackmult: Double read FSpecBackmult;
property WfDetector: Integer read FWfDetector;
property WfAvgMode: Integer read FWfAvgMode;
property WfAvgCount: Integer read FWfAvgCount;
property WfBackmult: Double read FWfBackmult;
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
property TXActive: Boolean read FTXActive;
end;
implementation
// ===========================================================================
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
// Аналог tx_thread в piHPSDR/transmitter.c
// ===========================================================================
type
TTXDSPThread = class(TThread)
private
FEngine: TWDSPEngine;
protected
procedure Execute; override;
public
constructor Create(AEngine: TWDSPEngine);
end;
constructor TTXDSPThread.Create(AEngine: TWDSPEngine);
begin
FEngine := AEngine;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TTXDSPThread.Execute;
var
Avail: Integer;
begin
while not Terminated do
begin
RTLEventWaitFor(FEngine.FTXMicSem, 50);
if Terminated then Break;
if not FEngine.FTXActive then Continue;
// Обрабатываем все накопившиеся блоки
repeat
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
and (TX_MIC_RING - 1);
if Avail >= FEngine.FAudioBufSize then
FEngine.ProcessTXBlock
else
Break;
until Terminated;
end;
end;
// ===========================================================================
// DSP поток — обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
@@ -367,6 +465,17 @@ begin
FSNBEnabled := False;
FANFEnabled := False;
FRXAccPos := 0;
// Analyzer defaults
FFFTSize := 4096;
FWindowType := 5; // Kaiser
FSpecDetector := 0; // Peak
FSpecAvgMode := 3; // Log Recursive
FSpecAvgCount := 2;
FSpecBackmult := 0.45;
FWfDetector := 2; // Average
FWfAvgMode := 3; // Log Recursive
FWfAvgCount := 2;
FWfBackmult := 0.45;
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
@@ -374,10 +483,9 @@ begin
SetLength(FTXOut, FBufSize * 2);
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
SetLength(FSnapBuf, FBufSize * 2);
SetLength(FSpecBuf, FBufSize * 2);
SetLength(FOutL, FAudioBufSize);
SetLength(FOutR, FAudioBufSize);
SetLength(FSpecBuf, FBufSize * 2);
// Очередь и DSP поток
FQueueHead := 0;
@@ -385,12 +493,22 @@ begin
FDSPRunning := False;
FQueueSem := RTLEventCreate;
FDSPThread := nil;
// TX инициализация
FTXMicHead := 0;
FTXMicTail := 0;
FTXActive := False;
FTXThread := nil;
FTXMicSem := RTLEventCreate;
FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
end;
destructor TWDSPEngine.Destroy;
begin
SetTXRun(False); // останавливаем TX поток если запущен
Close;
RTLEventDestroy(FQueueSem);
RTLEventDestroy(FTXMicSem);
inherited;
end;
@@ -400,10 +518,7 @@ end;
procedure TWDSPEngine.OpenAnalyzer;
var
Success: Integer;
MaxW: Integer;
Ovrlp: Integer;
FRAME_RATE: Integer;
Success: Integer;
begin
if FAnalyzerOpen then Exit;
@@ -411,30 +526,41 @@ begin
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
if Success <> 0 then Exit;
FRAME_RATE := 20;
// max_w: WDSP аллоцирует ring-buffer и FFT планы на max_w сэмплов.
// Формула: fft_size + min(0.1*samplerate, 0.1*fft_size*fps)
// = 4096 + min(19200, 8192) = 12288 → 1700ms инициализации!
// Реально нам нужен только один FFT-кадр без усреднения по времени.
// MaxW = sz = 4096 — минимально допустимое значение (должно быть ≥ sz).
// piHPSDR использует именно sz без добавки keep_time.
MaxW := 4096;
Ovrlp := 0;
FFlp[0] := 0;
SetAnalyzer(DISP_ID, 1, 1, 1, @FFlp[0], 4096, FAudioBufSize, 1, 14.0,
Ovrlp, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW);
SetDisplayDetectorMode(DISP_ID, 0, DETECTOR_MODE_AVERAGE);
SetDisplayAverageMode(DISP_ID, 0, AVERAGE_MODE_LOG_RECURSIVE);
SetDisplayNumAverage(DISP_ID, 0, Max(2, Trunc(FRAME_RATE * 0.12)));
SetDisplayAvBackmult(DISP_ID, 0, 0.45);
SetDisplaySampleRate(DISP_ID, FSampleRate);
SetDisplayNormOneHz(DISP_ID, 0, 0);
ApplyAnalyzerSettings;
FAnalyzerOpen := True;
end;
procedure TWDSPEngine.ApplyAnalyzerSettings;
// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
var
MaxW: Integer;
begin
// MaxW = FFTSize — минимально допустимо (piHPSDR approach)
MaxW := FFFTSize;
// n_pixout=2: pixout=0 — спектр, pixout=1 — водопад
SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], FFFTSize, FAudioBufSize, FWindowType, 14.0,
0, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW);
// Спектр (pixout=0)
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount);
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult);
SetDisplayNormOneHz(DISP_ID, 0, 0);
// Водопад (pixout=1)
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
SetDisplayNormOneHz(DISP_ID, 1, 0);
SetDisplaySampleRate(DISP_ID, FSampleRate);
end;
procedure TWDSPEngine.CloseAnalyzer;
begin
if not FAnalyzerOpen then Exit;
@@ -501,13 +627,16 @@ begin
SetRXAPanelRun(RXA_CHAN, 1);
SetChannelState(RXA_CHAN, 1, 0);
// TXA: mic @ FAudioRate → IQ @ FSampleRate (DUC rate)
// out_size = FAudioBufSize * FSampleRate/FAudioRate = FBufSize (4096 @ 192kHz)
// FTXOut уже имеет размер FBufSize*2 — подходит идеально
OpenChannel(
TXA_CHAN,
FAudioBufSize,
FAudioBufSize,
FAudioRate,
FAudioRate,
FAudioRate,
FSampleRate,
1,
0,
0.010, 0.010, 0.010, 0.010,
@@ -668,18 +797,19 @@ begin
FRXIn[i * 2 + 1] := FRXAccQ[i];
end;
// Копируем входные IQ ДО fexchange0 для Spectrum0
// fexchange0 in-place перезапишет FRXIn выходными данными @ 48kHz
// Копируем IQ в FSpecBuf ДО fexchange0: fexchange0 in-place перезаписывает FRXIn
if FAnalyzerOpen then
for i := 0 to FBufSize * 2 - 1 do
FSpecBuf[i] := FRXIn[i]; // Single→Double, входные IQ @ 192kHz
begin
Move(FRXIn[0], FSpecBuf[0], FBufSize * 2 * SizeOf(Double));
end;
// DSP обработка
Err := 0;
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
// Spectrum0 с входными данными (скопированными до fexchange0)
if FAnalyzerOpen then
// Подаём RX IQ в анализатор только когда не передаём.
// Во время TX анализатор кормит ProcessTXBlock (TX сигнал на той же частоте).
if FAnalyzerOpen and not FTXActive then
Spectrum0(1, DISP_ID, 0, 0, @FSpecBuf[0]);
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
@@ -973,11 +1103,92 @@ end;
procedure TWDSPEngine.SetTXRun(Run: Boolean);
begin
if not FInitialized then Exit;
FTXActive := Run;
if Run then
SetChannelState(TXA_CHAN, 1, 0) // run, no delay
else
SetChannelState(TXA_CHAN, 0, 1); // stop, with slew
begin
if FInitialized then
SetChannelState(TXA_CHAN, 1, 0); // run, no delay
// Сброс mic ring buffer и запуск TX потока
FTXMicHead := 0;
FTXMicTail := 0;
if not Assigned(FTXThread) then
begin
FTXThread := TTXDSPThread.Create(Self);
FTXThread.Priority := tpHighest;
end;
end else begin
if FInitialized then
SetChannelState(TXA_CHAN, 0, 1); // stop, with slew
// Останавливаем TX поток
if Assigned(FTXThread) then
begin
FTXThread.Terminate;
RTLEventSetEvent(FTXMicSem);
FTXThread.WaitFor;
FreeAndNil(FTXThread);
end;
FTXMicHead := 0;
FTXMicTail := 0;
end;
end;
procedure TWDSPEngine.ProcessTXBlock;
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
// через WDSP TXA, выдаёт FBufSize IQ пар @ FSampleRate в FOnTXIQ callback
var
i, tail: Integer;
Err: Integer;
begin
if not FInitialized then Exit;
// Читаем FAudioBufSize сэмплов из ring buffer → FTXIn
tail := FTXMicTail;
for i := 0 to FAudioBufSize - 1 do
begin
FTXIn[i * 2] := FTXMicRing[tail];
FTXIn[i * 2 + 1] := 0.0; // Q = 0, mic — монофонический сигнал
tail := (tail + 1) and (TX_MIC_RING - 1);
end;
FTXMicTail := tail; // Атомарно обновляем tail (один writer — TX thread)
Err := 0;
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
// TX сигнал → анализатор: спектр/водопад показывают переданный сигнал
// FTXOut: FBufSize IQ пар @ FSampleRate — тот же формат что и FSpecBuf
if FAnalyzerOpen then
Spectrum0(1, DISP_ID, 0, 0, @FTXOut[0]);
// FTXOut содержит FBufSize IQ пар @ FSampleRate (interleaved double)
if Assigned(FOnTXIQ) then
FOnTXIQ(FTXOut, FBufSize);
end;
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
// Вызывается из receive thread — lock-free, кладёт сэмплы в ring buffer
// 16-bit big-endian samples из аппаратного микрофона
var
i, cnt, newHead: Integer;
S: SmallInt;
begin
cnt := N;
if cnt > Length(Src) then cnt := Length(Src);
for i := 0 to cnt - 1 do
begin
// Аппаратный mic приходит в big-endian, нужно swap байт
S := SmallInt(((Src[i] and $FF) shl 8) or ((Src[i] shr 8) and $FF));
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
if newHead <> FTXMicTail then // буфер не полон
begin
FTXMicRing[FTXMicHead] := S / 32768.0;
FTXMicHead := newHead;
end;
// если буфер полон — сэмпл отбрасывается (overrun)
end;
RTLEventSetEvent(FTXMicSem); // будим TX поток
end;
// ---------------------------------------------------------------------------
@@ -985,9 +1196,12 @@ end;
// ---------------------------------------------------------------------------
procedure TWDSPEngine.UpdateSpectrum;
// Вызывается из таймера главного потока (~20fps)
// Spectrum0 уже вызван в сетевом потоке после каждого fexchange0
// Здесь только читаем готовые пиксели
// Вызывается из таймера главного потока (настраиваемый FPS).
// Здесь вызываем Spectrum0 — WDSP берёт снапшот из своего кольцевого
// буфера (который fexchange0 непрерывно заполняет из DSP-потока).
// Это обеспечивает равномерный FPS независимо от размера FFT:
// при FFT=16384 WDSP использует уже накопленные данные из overlap-save
// буфера, а не ждёт следующего полного блока.
var
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
Flag: Integer;
@@ -995,15 +1209,27 @@ var
begin
if not FInitialized or not FAnalyzerOpen then Exit;
// Спектр (pixout=0)
Flag := 0;
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
if Flag = 0 then Exit; // нет нового кадра — ждём следующего тика
if Flag <> 0 then
begin
for i := 0 to SPECTRUM_PIXELS - 1 do
FSpectrumPixels[i] := PixBuf[i];
if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
end;
for i := 0 to SPECTRUM_PIXELS - 1 do
FSpectrumPixels[i] := PixBuf[i];
if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, SPECTRUM_PIXELS);
// Водопад (pixout=1)
Flag := 0;
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
if Flag <> 0 then
begin
for i := 0 to SPECTRUM_PIXELS - 1 do
FWaterfallPixels[i] := PixBuf[i];
if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, SPECTRUM_PIXELS);
end;
end;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
@@ -1024,4 +1250,50 @@ begin
Result := FSMeter;
end;
// ---------------------------------------------------------------------------
// Настройки анализатора (применяются немедленно)
// ---------------------------------------------------------------------------
procedure TWDSPEngine.SetFFTParams(FFTSize, WinType: Integer);
begin
// Ограничения: FFT size должен быть >= bf_sz (FAudioBufSize = 1024)
// и степенью двойки, не превышать m_size=16384 (из XCreateAnalyzer)
if FFTSize < FAudioBufSize then FFTSize := FAudioBufSize;
if FFTSize > 16384 then FFTSize := 16384;
if WinType < 0 then WinType := 0;
if WinType > 6 then WinType := 6;
FFFTSize := FFTSize;
FWindowType := WinType;
if FAnalyzerOpen then
ApplyAnalyzerSettings;
end;
procedure TWDSPEngine.SetSpectrumDisplay(Detector, AvgMode, AvgCount: Integer;
Backmult: Double);
begin
FSpecDetector := Detector;
FSpecAvgMode := AvgMode;
FSpecAvgCount := AvgCount;
FSpecBackmult := Backmult;
if not FAnalyzerOpen then Exit;
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
SetDisplayNumAverage(DISP_ID, 0, FSpecAvgCount);
SetDisplayAvBackmult(DISP_ID, 0, FSpecBackmult);
end;
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode, AvgCount: Integer;
Backmult: Double);
begin
FWfDetector := Detector;
FWfAvgMode := AvgMode;
FWfAvgCount := AvgCount;
FWfBackmult := Backmult;
if not FAnalyzerOpen then Exit;
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
SetDisplayNumAverage(DISP_ID, 1, FWfAvgCount);
SetDisplayAvBackmult(DISP_ID, 1, FWfBackmult);
end;
end.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -99,10 +99,18 @@
<Filename Value="AudioOutput.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="AudioInput.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="DeviceForm.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="SettingsForm.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="FlatButton.pas"/>
<IsPartOfProject Value="True"/>