mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 04:47:35 +00:00
WIP: TX implementation — per-device settings, separate analyzer, sound-card mic
- Settings: TTXSettings record + LoadTX/SaveTX, JSON section "tx" per MAC - WDSPEngine: separate TX_DISP_ID analyzer with own FFT/Window/Det/Avg parameters, ApplyTXAnalyzerSettings + setters - WDSPEngine: full TX chain setters (Filter/Compressor/Leveler/ALC/ PhaseRot/EQ/AMCarrier/FM/CTCSS) + ApplyTXChainSettings - WDSPEngine: ProcessTXBlock now feeds TX analyzer in DISPLAY_BLOCK_SIZE chunks (was passing whole FTXOut → analyzer ignored most of it) - WDSPEngine: ActiveDisplayID switches RX/TX in UpdateSpectrum - WDSPEngine: RX audio muted while TX active to avoid local feedback - WDSPEngine: default EQ frequencies in Create (zero freqs broke TX chain) - WDSPEngine: PushTXMicSampleD + OnPullMicSamples for sound-card path - AudioInput: Available counter for non-blocking pull - MainForm: PullSoundCardMic, BuildMicLineSelectByte, SendDUCSpecific with mic bias/boost/line/PTT bits, OnTXSettingsChange - MainForm: auto-switch MicSource on TX device select, TX diagnostic in StatusBar panel [5] - SettingsForm: new Transmit nav page with Microphone, Filter, Dynamic processing, EQ, AM/FM/CTCSS, TX Display sections NOT YET WORKING: sound-card mic produces ring activity but signal still not visible on TX spectrum/waterfall — needs further investigation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,9 @@ type
|
|||||||
// Читает следующий моно-сэмпл из кольцевого буфера.
|
// Читает следующий моно-сэмпл из кольцевого буфера.
|
||||||
// Возвращает 0.0 если буфер пуст.
|
// Возвращает 0.0 если буфер пуст.
|
||||||
function ReadSample: Double;
|
function ReadSample: Double;
|
||||||
|
// Сколько сэмплов реально лежит в ring буфере (0..MY_MIC_RING_SIZE-1).
|
||||||
|
// Использовать для чтения только доступных данных без пуш-тишины.
|
||||||
|
function Available: Integer;
|
||||||
|
|
||||||
property IsOpen: Boolean read FOpen;
|
property IsOpen: Boolean read FOpen;
|
||||||
property LastError: string read GetLastError;
|
property LastError: string read GetLastError;
|
||||||
@@ -534,4 +537,13 @@ begin
|
|||||||
FOutPt := newpt;
|
FOutPt := newpt;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TAudioInput.Available: Integer;
|
||||||
|
var d: Integer;
|
||||||
|
begin
|
||||||
|
// Считаем сколько непрочитанных сэмплов между OutPt (читатель) и InPt (писатель).
|
||||||
|
d := FInPt - FOutPt;
|
||||||
|
if d < 0 then d := d + MY_MIC_RING_SIZE;
|
||||||
|
Result := d;
|
||||||
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
+178
-18
@@ -158,6 +158,7 @@ type
|
|||||||
FAudioIn: TAudioInput;
|
FAudioIn: TAudioInput;
|
||||||
FAudioOutDevName: string; // текущее имя выходного устройства
|
FAudioOutDevName: string; // текущее имя выходного устройства
|
||||||
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
|
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
|
||||||
|
FTXSettings: TTXSettings; // полный TX-конфиг (загружается per-MAC)
|
||||||
FWDSPReady: Boolean; // True когда WDSP открыт и работает
|
FWDSPReady: Boolean; // True когда WDSP открыт и работает
|
||||||
|
|
||||||
// ---- State ----
|
// ---- State ----
|
||||||
@@ -536,6 +537,7 @@ type
|
|||||||
procedure ApplyMOX(Active: Boolean);
|
procedure ApplyMOX(Active: Boolean);
|
||||||
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
|
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
|
||||||
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
|
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
|
||||||
|
procedure OnTXSettingsChange(const T: TTXSettings);
|
||||||
procedure BtnAGCModeClick(Sender: TObject);
|
procedure BtnAGCModeClick(Sender: TObject);
|
||||||
procedure TrkAGCChange(Sender: TObject);
|
procedure TrkAGCChange(Sender: TObject);
|
||||||
procedure MeterTimerTick(Sender: TObject);
|
procedure MeterTimerTick(Sender: TObject);
|
||||||
@@ -559,6 +561,12 @@ type
|
|||||||
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||||
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||||||
|
// TX settings: применить FTXSettings к WDSP и переслать DUC Specific
|
||||||
|
// (mic-биты Boost/Bias/LineIn/PTT попадают в byte 50 DUCSpecific).
|
||||||
|
procedure ApplyTXSettingsToDSP;
|
||||||
|
procedure SendDUCSpecificFromSettings;
|
||||||
|
function PullSoundCardMic(MaxN: Integer): Integer;
|
||||||
|
function BuildMicLineSelectByte: Byte;
|
||||||
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
|
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
|
||||||
procedure ApplyFPS(FPS: Integer);
|
procedure ApplyFPS(FPS: Integer);
|
||||||
|
|
||||||
@@ -942,6 +950,8 @@ begin
|
|||||||
begin
|
begin
|
||||||
SaveCurrentBand;
|
SaveCurrentBand;
|
||||||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||||||
|
if FDevConnected then
|
||||||
|
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||||||
FSettings.Save;
|
FSettings.Save;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -1105,6 +1115,8 @@ begin
|
|||||||
FNetwork.OnHPStatus := OnHPStatusCB;
|
FNetwork.OnHPStatus := OnHPStatusCB;
|
||||||
FNetwork.OnDDCIQ := OnDDCIQCB;
|
FNetwork.OnDDCIQ := OnDDCIQCB;
|
||||||
FNetwork.OnMicPacket := OnMicPacketCB;
|
FNetwork.OnMicPacket := OnMicPacketCB;
|
||||||
|
// Дефолты TX (на случай если устройство ещё не выбрано).
|
||||||
|
TSettingsManager.DefaultTX(FTXSettings);
|
||||||
|
|
||||||
BuildUI;
|
BuildUI;
|
||||||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||||||
@@ -1129,6 +1141,9 @@ begin
|
|||||||
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
||||||
FDSPEngine.OnWaterfall := OnWaterfallReady;
|
FDSPEngine.OnWaterfall := OnWaterfallReady;
|
||||||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||||||
|
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
|
||||||
|
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
|
||||||
|
FDSPEngine.OnPullMicSamples := PullSoundCardMic;
|
||||||
|
|
||||||
// Audio output/input — создаём объекты сейчас, открываем после показа формы
|
// Audio output/input — создаём объекты сейчас, открываем после показа формы
|
||||||
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
|
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
|
||||||
@@ -1182,6 +1197,8 @@ begin
|
|||||||
begin
|
begin
|
||||||
SaveCurrentBand;
|
SaveCurrentBand;
|
||||||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||||||
|
if FDevConnected then
|
||||||
|
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||||||
end;
|
end;
|
||||||
// Размер окна сохраняем всегда (не зависит от подключения)
|
// Размер окна сохраняем всегда (не зависит от подключения)
|
||||||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||||||
@@ -1298,6 +1315,7 @@ begin
|
|||||||
Add; Items[2].Width := 320; Items[2].Text := 'Supply: -- | FWD: -- | SWR: --';
|
Add; Items[2].Width := 320; Items[2].Text := 'Supply: -- | FWD: -- | SWR: --';
|
||||||
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
|
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
|
||||||
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
|
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
|
||||||
|
Add; Items[5].Width := 280; Items[5].Text := 'TX: --';
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ---- Left panel ----
|
// ---- Left panel ----
|
||||||
@@ -2520,6 +2538,24 @@ begin
|
|||||||
FSpecView.LastSWR := FLastSWR;
|
FSpecView.LastSWR := FLastSWR;
|
||||||
|
|
||||||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||||||
|
|
||||||
|
// Диагностика TX mic-пути — в отдельной панели [5], чтобы её не перетирали
|
||||||
|
// другие источники (DDC seq, audio-логи и т.п.).
|
||||||
|
if FWDSPReady then
|
||||||
|
begin
|
||||||
|
if FTransmitting then
|
||||||
|
begin
|
||||||
|
if FTXSettings.MicSource = 1 then
|
||||||
|
StatusBar1.Panels[5].Text :=
|
||||||
|
Format('TX sc: avail=%d ring=%d',
|
||||||
|
[FAudioIn.Available, FDSPEngine.TXMicRingAvail])
|
||||||
|
else
|
||||||
|
StatusBar1.Panels[5].Text :=
|
||||||
|
Format('TX hw: ring=%d', [FDSPEngine.TXMicRingAvail]);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
StatusBar1.Panels[5].Text := 'TX: idle';
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||||||
@@ -2807,10 +2843,101 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
|
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
|
||||||
begin
|
begin
|
||||||
if FTransmitting and FWDSPReady then
|
// HW mic-стрим уходит в WDSP только если выбран источник Radio.
|
||||||
|
// Иначе сэмплы игнорируются (mic берётся со звуковой карты).
|
||||||
|
if FTransmitting and FWDSPReady and
|
||||||
|
(FTXSettings.MicSource = Ord(txmsRadio)) then
|
||||||
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
|
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TX settings — применение к WDSP, сборка DUC Specific, sound-card pull
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function TMainForm.BuildMicLineSelectByte: Byte;
|
||||||
|
// HPSDR Protocol-2, DUC Specific byte 50:
|
||||||
|
// bit 0 — mic boost (+20 dB)
|
||||||
|
// bit 1 — Line in вместо mic in
|
||||||
|
// bit 2 — Mic-jack PTT enable (tip)
|
||||||
|
// bit 4 — Mic bias on (электретный капсюль)
|
||||||
|
// Биты 3 (PTT tip-bias-ring) и 5..7 не используются.
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
if FTXSettings.MicBoost then Result := Result or $01;
|
||||||
|
if FTXSettings.MicLineIn then Result := Result or $02;
|
||||||
|
if FTXSettings.MicPTTEnabled then Result := Result or $04;
|
||||||
|
if FTXSettings.MicBias then Result := Result or $10;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.SendDUCSpecificFromSettings;
|
||||||
|
var
|
||||||
|
DUCPkt: TDUCSpecificPacket;
|
||||||
|
begin
|
||||||
|
if not (FNetwork.Connected and FNetwork.Running) then Exit;
|
||||||
|
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||||||
|
DUCPkt.NumDACs := 1;
|
||||||
|
DUCPkt.SidetoneLevel := 50;
|
||||||
|
DUCPkt.SidetoneFreqHi := Hi(600);
|
||||||
|
DUCPkt.SidetoneFreqLo := Lo(600);
|
||||||
|
DUCPkt.KeyerSpeed := 20;
|
||||||
|
DUCPkt.KeyerWeight := 50;
|
||||||
|
DUCPkt.DUC0RateHi := Hi(192);
|
||||||
|
DUCPkt.DUC0RateLo := Lo(192);
|
||||||
|
DUCPkt.DUC0Bits := 24;
|
||||||
|
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
||||||
|
FNetwork.SendDUCSpecific(DUCPkt);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.ApplyTXSettingsToDSP;
|
||||||
|
begin
|
||||||
|
if not FWDSPReady then Exit;
|
||||||
|
// Mic source — сначала, чтобы PullSoundCardMic уже видел правильное значение
|
||||||
|
FDSPEngine.SetTXMicSource(TTXMicSource(FTXSettings.MicSource));
|
||||||
|
FDSPEngine.SetMicGain(FTXSettings.MicGainDB);
|
||||||
|
FDSPEngine.SetTXFilterFull(FTXSettings.FilterLow, FTXSettings.FilterHigh,
|
||||||
|
FTXSettings.FilterNC, FTXSettings.FilterMP,
|
||||||
|
FTXSettings.FilterWindow);
|
||||||
|
FDSPEngine.SetTXCompressor(FTXSettings.CompressorOn, FTXSettings.CompressorGain);
|
||||||
|
FDSPEngine.SetTXLeveler(FTXSettings.LevelerOn, FTXSettings.LevelerTop,
|
||||||
|
FTXSettings.LevelerDecay);
|
||||||
|
FDSPEngine.SetTXALC(FTXSettings.ALCOn, FTXSettings.ALCMaxGain,
|
||||||
|
FTXSettings.ALCDecay);
|
||||||
|
FDSPEngine.SetTXPhaseRot(FTXSettings.PhaseRotOn, FTXSettings.PhaseRotStages,
|
||||||
|
FTXSettings.PhaseRotFreq);
|
||||||
|
FDSPEngine.SetTXEQ(FTXSettings.EQOn, FTXSettings.EQNumBands,
|
||||||
|
FTXSettings.EQGains, FTXSettings.EQFreqs);
|
||||||
|
FDSPEngine.SetTXAMCarrierLevel(FTXSettings.AMCarrierLevel);
|
||||||
|
FDSPEngine.SetTXFMParams(FTXSettings.FMDeviation, FTXSettings.FMLowCut,
|
||||||
|
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||||||
|
FDSPEngine.SetTXCTCSS(FTXSettings.CTCSSOn, FTXSettings.CTCSSFreq);
|
||||||
|
// TX display
|
||||||
|
FDSPEngine.SetTXFFTParams(FTXSettings.TXFFTSize, FTXSettings.TXWindowType);
|
||||||
|
FDSPEngine.SetTXSpectrumDisplay(FTXSettings.TXSpecDetector,
|
||||||
|
FTXSettings.TXSpecAvgMode,
|
||||||
|
FTXSettings.TXSpecAvgTimeMS);
|
||||||
|
FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector,
|
||||||
|
FTXSettings.TXWfAvgMode,
|
||||||
|
FTXSettings.TXWfAvgTimeMS);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TMainForm.PullSoundCardMic(MaxN: Integer): Integer;
|
||||||
|
// Колбэк, который TTXDSPThread зовёт при MicSource=SoundCard.
|
||||||
|
// Тянем только реально доступные сэмплы из FAudioIn — иначе мы пушили бы
|
||||||
|
// нули и mic-цепь в WDSP получала бы тишину вместо реального сигнала.
|
||||||
|
var
|
||||||
|
i, n: Integer;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit;
|
||||||
|
n := FAudioIn.Available;
|
||||||
|
if n > MaxN then n := MaxN;
|
||||||
|
for i := 0 to n - 1 do
|
||||||
|
begin
|
||||||
|
FDSPEngine.PushTXMicSampleD(FAudioIn.ReadSample);
|
||||||
|
Inc(Result);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Button handlers
|
// Button handlers
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -3015,7 +3142,6 @@ var
|
|||||||
Idx, i: Integer;
|
Idx, i: Integer;
|
||||||
Dev: THPSDRDevice;
|
Dev: THPSDRDevice;
|
||||||
GenPkt: TGeneralPacket;
|
GenPkt: TGeneralPacket;
|
||||||
DUCPkt: TDUCSpecificPacket;
|
|
||||||
G_Settings: TGlobalSettings;
|
G_Settings: TGlobalSettings;
|
||||||
PreG: TGlobalSettings;
|
PreG: TGlobalSettings;
|
||||||
PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
||||||
@@ -3144,7 +3270,6 @@ end;
|
|||||||
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
|
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
|
||||||
var
|
var
|
||||||
GenPkt: TGeneralPacket;
|
GenPkt: TGeneralPacket;
|
||||||
DUCPkt: TDUCSpecificPacket;
|
|
||||||
G_Settings: TGlobalSettings;
|
G_Settings: TGlobalSettings;
|
||||||
i: Integer;
|
i: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -3163,6 +3288,8 @@ begin
|
|||||||
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
|
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
|
||||||
FCATLastGlobal := G_Settings;
|
FCATLastGlobal := G_Settings;
|
||||||
CATApplySettings(G_Settings);
|
CATApplySettings(G_Settings);
|
||||||
|
// TX-настройки per-device — отдельная JSON-секция "tx"
|
||||||
|
FSettings.LoadTX(FDevMAC, FTXSettings);
|
||||||
FVolume := G_Settings.Volume;
|
FVolume := G_Settings.Volume;
|
||||||
FActiveVfo := G_Settings.ActiveVfo;
|
FActiveVfo := G_Settings.ActiveVfo;
|
||||||
// PA settings
|
// PA settings
|
||||||
@@ -3231,12 +3358,15 @@ begin
|
|||||||
FAudioOutDevName := '';
|
FAudioOutDevName := '';
|
||||||
end;
|
end;
|
||||||
FAudioOut.Open;
|
FAudioOut.Open;
|
||||||
// Аудио устройство ввода (TX mic)
|
// Аудио устройство ввода (TX mic). Если в настройках выбрана конкретная
|
||||||
|
// soundcard — открываем её и принудительно переключаем MicSource на SoundCard,
|
||||||
|
// иначе пользователь нажмёт MOX и не поймёт почему его микрофон молчит.
|
||||||
if G_Settings.AudioInDevice <> '' then
|
if G_Settings.AudioInDevice <> '' then
|
||||||
begin
|
begin
|
||||||
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
|
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
|
||||||
FAudioInDevName := G_Settings.AudioInDevice;
|
FAudioInDevName := G_Settings.AudioInDevice;
|
||||||
FAudioIn.Open;
|
FAudioIn.Open;
|
||||||
|
FTXSettings.MicSource := 1; // SoundCard
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
@@ -3286,18 +3416,6 @@ begin
|
|||||||
end;
|
end;
|
||||||
FSpecView.AGCTop := FAGCTop;
|
FSpecView.AGCTop := FAGCTop;
|
||||||
|
|
||||||
// DUC specific packet (строим здесь, отправляем после Run=1)
|
|
||||||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
|
||||||
DUCPkt.NumDACs := 1;
|
|
||||||
DUCPkt.SidetoneLevel := 50;
|
|
||||||
DUCPkt.SidetoneFreqHi := Hi(600);
|
|
||||||
DUCPkt.SidetoneFreqLo := Lo(600);
|
|
||||||
DUCPkt.KeyerSpeed := 20;
|
|
||||||
DUCPkt.KeyerWeight := 50;
|
|
||||||
DUCPkt.DUC0RateHi := Hi(192);
|
|
||||||
DUCPkt.DUC0RateLo := Lo(192);
|
|
||||||
DUCPkt.DUC0Bits := 24;
|
|
||||||
|
|
||||||
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
|
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
|
||||||
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
|
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
|
||||||
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
|
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
|
||||||
@@ -3307,9 +3425,12 @@ begin
|
|||||||
FRXStartTime := GetTickCount64;
|
FRXStartTime := GetTickCount64;
|
||||||
FRXLastPktTime := 0;
|
FRXLastPktTime := 0;
|
||||||
|
|
||||||
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах
|
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
|
||||||
|
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
|
||||||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0);
|
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0);
|
||||||
FNetwork.SendDUCSpecific(DUCPkt);
|
SendDUCSpecificFromSettings;
|
||||||
|
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||||||
|
ApplyTXSettingsToDSP;
|
||||||
|
|
||||||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||||||
if FWDSPReady then
|
if FWDSPReady then
|
||||||
@@ -4858,6 +4979,26 @@ begin
|
|||||||
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True);
|
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
||||||
|
// Любая правка в Settings→Transmit летит сюда: обновляем FTXSettings,
|
||||||
|
// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific.
|
||||||
|
var
|
||||||
|
OldMicByte, NewMicByte: Byte;
|
||||||
|
begin
|
||||||
|
OldMicByte := BuildMicLineSelectByte;
|
||||||
|
FTXSettings := T;
|
||||||
|
NewMicByte := BuildMicLineSelectByte;
|
||||||
|
ApplyTXSettingsToDSP;
|
||||||
|
if OldMicByte <> NewMicByte then
|
||||||
|
SendDUCSpecificFromSettings;
|
||||||
|
// Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json
|
||||||
|
if FDevConnected then
|
||||||
|
begin
|
||||||
|
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||||||
|
FSettings.Save;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.TrkVolumeChange(Sender: TObject);
|
procedure TMainForm.TrkVolumeChange(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
FVolume := TrkVolume.Position;
|
FVolume := TrkVolume.Position;
|
||||||
@@ -5063,9 +5204,26 @@ begin
|
|||||||
on E: Exception do
|
on E: Exception do
|
||||||
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
|
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
|
||||||
end;
|
end;
|
||||||
|
// Автопереключение mic-источника: явный выбор PortAudio-устройства
|
||||||
|
// → берём mic со звуковой карты, иначе HW mic-стрим бы перебивал его.
|
||||||
|
FTXSettings.MicSource := 1; // SoundCard
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
begin
|
||||||
StatusBar1.Panels[3].Text := 'Mic: none';
|
StatusBar1.Panels[3].Text := 'Mic: none';
|
||||||
|
// Нет выбранного soundcard mic → возвращаемся на HW mic-стрим.
|
||||||
|
FTXSettings.MicSource := 0; // Radio
|
||||||
|
end;
|
||||||
|
if FWDSPReady then
|
||||||
|
FDSPEngine.SetTXMicSource(TTXMicSource(FTXSettings.MicSource));
|
||||||
|
if FDevConnected then
|
||||||
|
begin
|
||||||
|
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||||||
|
FSettings.Save;
|
||||||
|
end;
|
||||||
|
// Если открыт диалог настроек — синхронизировать UI
|
||||||
|
if Assigned(FSettingsForm) then
|
||||||
|
TSettingsForm(FSettingsForm).LoadTXSettings(FTXSettings);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
|
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
|
||||||
@@ -5108,8 +5266,10 @@ begin
|
|||||||
SF.OnPAChange := OnPASettingsChange;
|
SF.OnPAChange := OnPASettingsChange;
|
||||||
SF.OnCATChange := OnCATSettingsChange;
|
SF.OnCATChange := OnCATSettingsChange;
|
||||||
SF.OnThemeChange := SetLightTheme;
|
SF.OnThemeChange := SetLightTheme;
|
||||||
|
SF.OnTXChange := OnTXSettingsChange;
|
||||||
end;
|
end;
|
||||||
SF := TSettingsForm(FSettingsForm);
|
SF := TSettingsForm(FSettingsForm);
|
||||||
|
SF.LoadTXSettings(FTXSettings);
|
||||||
SF.LoadPASettings(FPAMaxPower, FPABandCal);
|
SF.LoadPASettings(FPAMaxPower, FPABandCal);
|
||||||
SF.LoadCATSettings(
|
SF.LoadCATSettings(
|
||||||
FCATLastGlobal.CATSerialEnabled,
|
FCATLastGlobal.CATSerialEnabled,
|
||||||
|
|||||||
+249
@@ -30,6 +30,67 @@ type
|
|||||||
SpanHz: Double;
|
SpanHz: Double;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
||||||
|
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
|
||||||
|
// (Тип TTXMicSource живёт в WDSPEngine — здесь храним как Integer,
|
||||||
|
// чтобы не тащить циклическую зависимость с WDSPEngine.)
|
||||||
|
TTXSettings = record
|
||||||
|
// Mic source select
|
||||||
|
MicSource: Integer; // 0=Radio, 1=SoundCard
|
||||||
|
// Radio mic options (DUC Specific byte 50 bits)
|
||||||
|
MicBoost: Boolean; // bit0: +20 dB
|
||||||
|
MicBias: Boolean; // bit4: электретный bias
|
||||||
|
MicLineIn: Boolean; // bit1: 1=Line in, 0=Mic in
|
||||||
|
MicPTTEnabled: Boolean; // bit2: Mic PTT enable
|
||||||
|
// Mic gain (dB, применяется в WDSP PanelGain1)
|
||||||
|
MicGainDB: Double; // -10..+30
|
||||||
|
// TX bandpass filter
|
||||||
|
FilterLow: Integer; // Hz, для SSB обычно 100
|
||||||
|
FilterHigh: Integer; // Hz, для SSB обычно 2800
|
||||||
|
FilterNC: Integer; // FIR length (1024/2048/4096)
|
||||||
|
FilterMP: Integer; // 0=Linear Phase, 1=Low Latency
|
||||||
|
FilterWindow: Integer; // 0=Rect..6=BH7
|
||||||
|
// Compressor
|
||||||
|
CompressorOn: Boolean;
|
||||||
|
CompressorGain: Double; // 0..20 dB
|
||||||
|
// Leveler (TX AGC)
|
||||||
|
LevelerOn: Boolean;
|
||||||
|
LevelerTop: Double; // 0..10 dB
|
||||||
|
LevelerDecay: Integer; // ms
|
||||||
|
// ALC (Auto Level Control)
|
||||||
|
ALCOn: Boolean;
|
||||||
|
ALCMaxGain: Double; // -10..+10 dB
|
||||||
|
ALCDecay: Integer; // ms
|
||||||
|
// Phase Rotator
|
||||||
|
PhaseRotOn: Boolean;
|
||||||
|
PhaseRotStages: Integer; // 4/6/8
|
||||||
|
PhaseRotFreq: Double; // Hz, обычно 338
|
||||||
|
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
|
||||||
|
EQOn: Boolean;
|
||||||
|
EQNumBands: Integer; // 3 или 10
|
||||||
|
EQGains: array[0..10] of Double; // dB, ±15
|
||||||
|
EQFreqs: array[0..10] of Double; // Hz
|
||||||
|
// AM modulator
|
||||||
|
AMCarrierLevel: Double; // 0..1
|
||||||
|
// FM modulator
|
||||||
|
FMDeviation: Double; // Hz, 2500..5000
|
||||||
|
FMLowCut: Integer; // AF filter low
|
||||||
|
FMHighCut: Integer; // AF filter high
|
||||||
|
FMEmphPosition: Integer; // 0=PreEQ, 1=PostEQ
|
||||||
|
// CTCSS
|
||||||
|
CTCSSOn: Boolean;
|
||||||
|
CTCSSFreq: Double; // Hz
|
||||||
|
// TX Display (отдельный analyzer для TX-спектра/водопада)
|
||||||
|
TXFFTSize: Integer; // 4096..262144
|
||||||
|
TXWindowType: Integer; // 0..6
|
||||||
|
TXSpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
|
||||||
|
TXSpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
|
||||||
|
TXSpecAvgTimeMS: Double;
|
||||||
|
TXWfDetector: Integer;
|
||||||
|
TXWfAvgMode: Integer;
|
||||||
|
TXWfAvgTimeMS: Double;
|
||||||
|
end;
|
||||||
|
|
||||||
TGlobalSettings = record
|
TGlobalSettings = record
|
||||||
Volume: Integer;
|
Volume: Integer;
|
||||||
DriveLevel: Integer;
|
DriveLevel: Integer;
|
||||||
@@ -119,9 +180,15 @@ type
|
|||||||
const B: TBandSettings);
|
const B: TBandSettings);
|
||||||
function LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
function LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||||||
out B: TBandSettings): Boolean;
|
out B: TBandSettings): Boolean;
|
||||||
|
// TX settings per-device (JSON-секция "tx" под MAC).
|
||||||
|
// LoadTX возвращает True если устройство уже зарегистрировано в JSON;
|
||||||
|
// в любом случае T заполняется (дефолтами или сохранёнными значениями).
|
||||||
|
function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean;
|
||||||
|
procedure SaveTX(const MAC: array of Byte; const T: TTXSettings);
|
||||||
class function MacToStr(const MAC: array of Byte): string;
|
class function MacToStr(const MAC: array of Byte): string;
|
||||||
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
||||||
class procedure DefaultGlobal(out G: TGlobalSettings);
|
class procedure DefaultGlobal(out G: TGlobalSettings);
|
||||||
|
class procedure DefaultTX(out T: TTXSettings);
|
||||||
// Размер окна — не привязан к MAC, хранится в корне JSON
|
// Размер окна — не привязан к MAC, хранится в корне JSON
|
||||||
procedure SaveWindowBounds(L, T, W, H: Integer);
|
procedure SaveWindowBounds(L, T, W, H: Integer);
|
||||||
procedure LoadWindowBounds(out L, T, W, H: Integer);
|
procedure LoadWindowBounds(out L, T, W, H: Integer);
|
||||||
@@ -628,6 +695,188 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TX settings (per-device, JSON-секция "tx" под MAC)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
const
|
||||||
|
// 10-полосный голосовой EQ (Hz). [0]=preamp slot, [1..10]=bands.
|
||||||
|
// Подобрано под голос, а не под музыку: больше точек в 100..3500 Hz.
|
||||||
|
DefaultEQFreqs: array[0..10] of Double = (
|
||||||
|
0, // [0] preamp slot — частота не используется
|
||||||
|
100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500);
|
||||||
|
begin
|
||||||
|
FillChar(T, SizeOf(T), 0);
|
||||||
|
// Mic source — Radio (HW). 0=Radio, 1=SoundCard (синхронно с TTXMicSource в WDSPEngine).
|
||||||
|
T.MicSource := 0;
|
||||||
|
T.MicBoost := False;
|
||||||
|
T.MicBias := True;
|
||||||
|
T.MicLineIn := False;
|
||||||
|
T.MicPTTEnabled := False;
|
||||||
|
T.MicGainDB := 10.0;
|
||||||
|
// SSB-фильтр по умолчанию
|
||||||
|
T.FilterLow := 100;
|
||||||
|
T.FilterHigh := 2800;
|
||||||
|
T.FilterNC := 1024;
|
||||||
|
T.FilterMP := 1; // Low Latency (как в piHPSDR)
|
||||||
|
T.FilterWindow := 1; // BH4 / Blackman-Harris
|
||||||
|
// Compressor — выкл по умолчанию
|
||||||
|
T.CompressorOn := False;
|
||||||
|
T.CompressorGain:= 5.0;
|
||||||
|
// Leveler — вкл, типичные значения piHPSDR/Thetis
|
||||||
|
T.LevelerOn := True;
|
||||||
|
T.LevelerTop := 5.0;
|
||||||
|
T.LevelerDecay := 500;
|
||||||
|
// ALC — вкл всегда (защита от перегруза DAC)
|
||||||
|
T.ALCOn := True;
|
||||||
|
T.ALCMaxGain := 0.0;
|
||||||
|
T.ALCDecay := 10;
|
||||||
|
// Phase Rotator — выкл
|
||||||
|
T.PhaseRotOn := False;
|
||||||
|
T.PhaseRotStages:= 8;
|
||||||
|
T.PhaseRotFreq := 338.0;
|
||||||
|
// EQ — выкл, нулевые гейны
|
||||||
|
T.EQOn := False;
|
||||||
|
T.EQNumBands := 10;
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
T.EQGains[i] := 0.0;
|
||||||
|
T.EQFreqs[i] := DefaultEQFreqs[i];
|
||||||
|
end;
|
||||||
|
// AM
|
||||||
|
T.AMCarrierLevel := 0.5;
|
||||||
|
// FM
|
||||||
|
T.FMDeviation := 5000.0;
|
||||||
|
T.FMLowCut := 300;
|
||||||
|
T.FMHighCut := 3000;
|
||||||
|
T.FMEmphPosition := 1; // PostEQ
|
||||||
|
// CTCSS
|
||||||
|
T.CTCSSOn := False;
|
||||||
|
T.CTCSSFreq := 100.0;
|
||||||
|
// TX Display defaults — те же что у RX (см. DefaultGlobal)
|
||||||
|
T.TXFFTSize := 16384; // меньше чем RX — TX-полоса узкая, FFT не нужен большой
|
||||||
|
T.TXWindowType := 2; // Hann
|
||||||
|
T.TXSpecDetector := 0; // Peak
|
||||||
|
T.TXSpecAvgMode := 3; // Log Recursive
|
||||||
|
T.TXSpecAvgTimeMS := 30.0;
|
||||||
|
T.TXWfDetector := 0;
|
||||||
|
T.TXWfAvgMode := 3;
|
||||||
|
T.TXWfAvgTimeMS := 120.0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TSettingsManager.LoadTX(const MAC: array of Byte;
|
||||||
|
out T: TTXSettings): Boolean;
|
||||||
|
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
|
||||||
|
begin
|
||||||
|
DefaultTX(T);
|
||||||
|
MacStr := MacToStr(MAC);
|
||||||
|
Result := FRoot.Find(MacStr) <> nil;
|
||||||
|
if not Result then Exit;
|
||||||
|
DevObj := GetDevObj(MacStr);
|
||||||
|
// Если секции "tx" ещё нет — оставляем дефолты, не материализуем пустую секцию
|
||||||
|
if DevObj.Find('tx') = nil then Exit;
|
||||||
|
O := EnsureObj(DevObj, 'tx');
|
||||||
|
|
||||||
|
T.MicSource := JI(O,'mic_source', T.MicSource);
|
||||||
|
T.MicBoost := JB(O,'mic_boost', T.MicBoost);
|
||||||
|
T.MicBias := JB(O,'mic_bias', T.MicBias);
|
||||||
|
T.MicLineIn := JB(O,'mic_line_in', T.MicLineIn);
|
||||||
|
T.MicPTTEnabled := JB(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||||||
|
T.MicGainDB := JD(O,'mic_gain_db', T.MicGainDB);
|
||||||
|
T.FilterLow := JI(O,'filter_low', T.FilterLow);
|
||||||
|
T.FilterHigh := JI(O,'filter_high', T.FilterHigh);
|
||||||
|
T.FilterNC := JI(O,'filter_nc', T.FilterNC);
|
||||||
|
T.FilterMP := JI(O,'filter_mp', T.FilterMP);
|
||||||
|
T.FilterWindow := JI(O,'filter_window', T.FilterWindow);
|
||||||
|
T.CompressorOn := JB(O,'compressor_on', T.CompressorOn);
|
||||||
|
T.CompressorGain := JD(O,'compressor_gain', T.CompressorGain);
|
||||||
|
T.LevelerOn := JB(O,'leveler_on', T.LevelerOn);
|
||||||
|
T.LevelerTop := JD(O,'leveler_top', T.LevelerTop);
|
||||||
|
T.LevelerDecay := JI(O,'leveler_decay', T.LevelerDecay);
|
||||||
|
T.ALCOn := JB(O,'alc_on', T.ALCOn);
|
||||||
|
T.ALCMaxGain := JD(O,'alc_max_gain', T.ALCMaxGain);
|
||||||
|
T.ALCDecay := JI(O,'alc_decay', T.ALCDecay);
|
||||||
|
T.PhaseRotOn := JB(O,'phase_rot_on', T.PhaseRotOn);
|
||||||
|
T.PhaseRotStages := JI(O,'phase_rot_stages',T.PhaseRotStages);
|
||||||
|
T.PhaseRotFreq := JD(O,'phase_rot_freq', T.PhaseRotFreq);
|
||||||
|
T.EQOn := JB(O,'eq_on', T.EQOn);
|
||||||
|
T.EQNumBands := JI(O,'eq_num_bands', T.EQNumBands);
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
T.EQGains[i] := JD(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
|
||||||
|
T.EQFreqs[i] := JD(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
|
||||||
|
end;
|
||||||
|
T.AMCarrierLevel := JD(O,'am_carrier_level',T.AMCarrierLevel);
|
||||||
|
T.FMDeviation := JD(O,'fm_deviation', T.FMDeviation);
|
||||||
|
T.FMLowCut := JI(O,'fm_low_cut', T.FMLowCut);
|
||||||
|
T.FMHighCut := JI(O,'fm_high_cut', T.FMHighCut);
|
||||||
|
T.FMEmphPosition := JI(O,'fm_emph_position',T.FMEmphPosition);
|
||||||
|
T.CTCSSOn := JB(O,'ctcss_on', T.CTCSSOn);
|
||||||
|
T.CTCSSFreq := JD(O,'ctcss_freq', T.CTCSSFreq);
|
||||||
|
T.TXFFTSize := JI(O,'tx_fft_size', T.TXFFTSize);
|
||||||
|
T.TXWindowType := JI(O,'tx_win_type', T.TXWindowType);
|
||||||
|
T.TXSpecDetector := JI(O,'tx_spec_detector', T.TXSpecDetector);
|
||||||
|
T.TXSpecAvgMode := JI(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
|
||||||
|
T.TXSpecAvgTimeMS := JD(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
|
||||||
|
T.TXWfDetector := JI(O,'tx_wf_detector', T.TXWfDetector);
|
||||||
|
T.TXWfAvgMode := JI(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||||||
|
T.TXWfAvgTimeMS := JD(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
|
||||||
|
const T: TTXSettings);
|
||||||
|
var O: TJSONObject; i: Integer;
|
||||||
|
begin
|
||||||
|
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx');
|
||||||
|
JW(O,'mic_source', T.MicSource);
|
||||||
|
JW(O,'mic_boost', T.MicBoost);
|
||||||
|
JW(O,'mic_bias', T.MicBias);
|
||||||
|
JW(O,'mic_line_in', T.MicLineIn);
|
||||||
|
JW(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||||||
|
JW(O,'mic_gain_db', T.MicGainDB);
|
||||||
|
JW(O,'filter_low', T.FilterLow);
|
||||||
|
JW(O,'filter_high', T.FilterHigh);
|
||||||
|
JW(O,'filter_nc', T.FilterNC);
|
||||||
|
JW(O,'filter_mp', T.FilterMP);
|
||||||
|
JW(O,'filter_window', T.FilterWindow);
|
||||||
|
JW(O,'compressor_on', T.CompressorOn);
|
||||||
|
JW(O,'compressor_gain', T.CompressorGain);
|
||||||
|
JW(O,'leveler_on', T.LevelerOn);
|
||||||
|
JW(O,'leveler_top', T.LevelerTop);
|
||||||
|
JW(O,'leveler_decay', T.LevelerDecay);
|
||||||
|
JW(O,'alc_on', T.ALCOn);
|
||||||
|
JW(O,'alc_max_gain', T.ALCMaxGain);
|
||||||
|
JW(O,'alc_decay', T.ALCDecay);
|
||||||
|
JW(O,'phase_rot_on', T.PhaseRotOn);
|
||||||
|
JW(O,'phase_rot_stages',T.PhaseRotStages);
|
||||||
|
JW(O,'phase_rot_freq', T.PhaseRotFreq);
|
||||||
|
JW(O,'eq_on', T.EQOn);
|
||||||
|
JW(O,'eq_num_bands', T.EQNumBands);
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
JW(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
|
||||||
|
JW(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
|
||||||
|
end;
|
||||||
|
JW(O,'am_carrier_level',T.AMCarrierLevel);
|
||||||
|
JW(O,'fm_deviation', T.FMDeviation);
|
||||||
|
JW(O,'fm_low_cut', T.FMLowCut);
|
||||||
|
JW(O,'fm_high_cut', T.FMHighCut);
|
||||||
|
JW(O,'fm_emph_position',T.FMEmphPosition);
|
||||||
|
JW(O,'ctcss_on', T.CTCSSOn);
|
||||||
|
JW(O,'ctcss_freq', T.CTCSSFreq);
|
||||||
|
JW(O,'tx_fft_size', T.TXFFTSize);
|
||||||
|
JW(O,'tx_win_type', T.TXWindowType);
|
||||||
|
JW(O,'tx_spec_detector', T.TXSpecDetector);
|
||||||
|
JW(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
|
||||||
|
JW(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
|
||||||
|
JW(O,'tx_wf_detector', T.TXWfDetector);
|
||||||
|
JW(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||||||
|
JW(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
|
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
|
||||||
var O: TJSONObject;
|
var O: TJSONObject;
|
||||||
begin
|
begin
|
||||||
|
|||||||
+483
-4
@@ -27,12 +27,14 @@ uses
|
|||||||
Classes, SysUtils, Math,
|
Classes, SysUtils, Math,
|
||||||
Forms, Controls, Graphics, Dialogs,
|
Forms, Controls, Graphics, Dialogs,
|
||||||
StdCtrls, ExtCtrls, Spin,
|
StdCtrls, ExtCtrls, Spin,
|
||||||
FlatButton, AudioOutput, AudioInput, AppTheme;
|
FlatButton, AudioOutput, AudioInput, AppTheme, Settings;
|
||||||
|
|
||||||
type
|
type
|
||||||
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
|
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
|
||||||
TOnPASettingsChange = procedure(MaxPower: Double;
|
TOnPASettingsChange = procedure(MaxPower: Double;
|
||||||
const BandCal: array of Double) of object;
|
const BandCal: array of Double) of object;
|
||||||
|
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
|
||||||
|
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
|
||||||
|
|
||||||
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
|
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
|
||||||
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
|
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
|
||||||
@@ -66,6 +68,7 @@ type
|
|||||||
FPagePA: TScrollBox;
|
FPagePA: TScrollBox;
|
||||||
FPageAdvanced: TScrollBox;
|
FPageAdvanced: TScrollBox;
|
||||||
FPageCAT: TScrollBox;
|
FPageCAT: TScrollBox;
|
||||||
|
FPageTransmit: TScrollBox;
|
||||||
FNavAudio: TFlatButton;
|
FNavAudio: TFlatButton;
|
||||||
FNavDisplay: TFlatButton;
|
FNavDisplay: TFlatButton;
|
||||||
FNavSpectrum: TFlatButton;
|
FNavSpectrum: TFlatButton;
|
||||||
@@ -73,6 +76,7 @@ type
|
|||||||
FNavPA: TFlatButton;
|
FNavPA: TFlatButton;
|
||||||
FNavAdvanced: TFlatButton;
|
FNavAdvanced: TFlatButton;
|
||||||
FNavCAT: TFlatButton;
|
FNavCAT: TFlatButton;
|
||||||
|
FNavTransmit: TFlatButton;
|
||||||
|
|
||||||
// ---- Audio tab controls ----
|
// ---- Audio tab controls ----
|
||||||
FCmbRXDev: TComboBox;
|
FCmbRXDev: TComboBox;
|
||||||
@@ -108,6 +112,62 @@ type
|
|||||||
FEdMaxPower: TSpinEdit;
|
FEdMaxPower: TSpinEdit;
|
||||||
FEdBandCal: array[0..10] of TFloatSpinEdit;
|
FEdBandCal: array[0..10] of TFloatSpinEdit;
|
||||||
|
|
||||||
|
// ---- Transmit tab controls ----
|
||||||
|
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
|
||||||
|
FTX: TTXSettings;
|
||||||
|
// Mic source / options
|
||||||
|
FCmbTXMicSource: TComboBox;
|
||||||
|
FChkTXMicBoost: TCheckBox;
|
||||||
|
FChkTXMicBias: TCheckBox;
|
||||||
|
FChkTXMicLineIn: TCheckBox;
|
||||||
|
FChkTXMicPTT: TCheckBox;
|
||||||
|
FEdTXMicGain: TFloatSpinEdit;
|
||||||
|
// Filter
|
||||||
|
FEdTXFiltLow: TSpinEdit;
|
||||||
|
FEdTXFiltHigh: TSpinEdit;
|
||||||
|
FCmbTXFiltNC: TComboBox;
|
||||||
|
FCmbTXFiltMP: TComboBox;
|
||||||
|
FCmbTXFiltWindow: TComboBox;
|
||||||
|
// Compressor
|
||||||
|
FChkTXCompOn: TCheckBox;
|
||||||
|
FEdTXCompGain: TFloatSpinEdit;
|
||||||
|
// Leveler
|
||||||
|
FChkTXLevOn: TCheckBox;
|
||||||
|
FEdTXLevTop: TFloatSpinEdit;
|
||||||
|
FEdTXLevDecay: TSpinEdit;
|
||||||
|
// ALC
|
||||||
|
FChkTXALCOn: TCheckBox;
|
||||||
|
FEdTXALCMaxGain: TFloatSpinEdit;
|
||||||
|
FEdTXALCDecay: TSpinEdit;
|
||||||
|
// Phase Rotator
|
||||||
|
FChkTXPHROn: TCheckBox;
|
||||||
|
FEdTXPHRStages: TSpinEdit;
|
||||||
|
FEdTXPHRFreq: TFloatSpinEdit;
|
||||||
|
// EQ
|
||||||
|
FChkTXEQOn: TCheckBox;
|
||||||
|
FCmbTXEQBands: TComboBox;
|
||||||
|
FEdTXEQGain: array[0..10] of TFloatSpinEdit;
|
||||||
|
FEdTXEQFreq: array[0..10] of TFloatSpinEdit;
|
||||||
|
// AM
|
||||||
|
FEdTXAMCarrier: TFloatSpinEdit;
|
||||||
|
// FM
|
||||||
|
FEdTXFMDev: TFloatSpinEdit;
|
||||||
|
FEdTXFMLow: TSpinEdit;
|
||||||
|
FEdTXFMHigh: TSpinEdit;
|
||||||
|
FCmbTXFMEmph: TComboBox;
|
||||||
|
// CTCSS
|
||||||
|
FChkTXCTCSSOn: TCheckBox;
|
||||||
|
FEdTXCTCSSFreq: TFloatSpinEdit;
|
||||||
|
// TX Display (отдельный analyzer)
|
||||||
|
FCmbTXFFT: TComboBox;
|
||||||
|
FCmbTXWindow: TComboBox;
|
||||||
|
FCmbTXSpecDet: TComboBox;
|
||||||
|
FCmbTXSpecAvg: TComboBox;
|
||||||
|
FEdTXSpecAvgTime: TFloatSpinEdit;
|
||||||
|
FCmbTXWfDet: TComboBox;
|
||||||
|
FCmbTXWfAvg: TComboBox;
|
||||||
|
FEdTXWfAvgTime: TFloatSpinEdit;
|
||||||
|
|
||||||
// ---- CAT tab controls ----
|
// ---- CAT tab controls ----
|
||||||
FCATSerialEn: array[0..3] of TCheckBox;
|
FCATSerialEn: array[0..3] of TCheckBox;
|
||||||
FCATSerialPort: array[0..3] of TEdit;
|
FCATSerialPort: array[0..3] of TEdit;
|
||||||
@@ -132,6 +192,7 @@ type
|
|||||||
FOnVisibilityChange: TOnVisibilityChange;
|
FOnVisibilityChange: TOnVisibilityChange;
|
||||||
FOnFPSChange: TOnFPSChange;
|
FOnFPSChange: TOnFPSChange;
|
||||||
FOnThemeChange: TOnThemeChange;
|
FOnThemeChange: TOnThemeChange;
|
||||||
|
FOnTXChange: TOnTXSettingsChange;
|
||||||
|
|
||||||
FLoading: Boolean;
|
FLoading: Boolean;
|
||||||
|
|
||||||
@@ -141,6 +202,10 @@ type
|
|||||||
procedure BuildRX1Tab;
|
procedure BuildRX1Tab;
|
||||||
procedure BuildPATab;
|
procedure BuildPATab;
|
||||||
procedure BuildCATTab;
|
procedure BuildCATTab;
|
||||||
|
procedure BuildTransmitTab;
|
||||||
|
procedure CollectTXFromUI;
|
||||||
|
procedure FireTXChange;
|
||||||
|
procedure OnTXAnyChange(Sender: TObject);
|
||||||
procedure ApplyTheme(const T: TAppTheme);
|
procedure ApplyTheme(const T: TAppTheme);
|
||||||
procedure NavClick(Sender: TObject);
|
procedure NavClick(Sender: TObject);
|
||||||
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
|
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
|
||||||
@@ -214,6 +279,7 @@ type
|
|||||||
const SerPort: array of string;
|
const SerPort: array of string;
|
||||||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||||||
TcpEnabled: Boolean; TcpPort: Integer);
|
TcpEnabled: Boolean; TcpPort: Integer);
|
||||||
|
procedure LoadTXSettings(const T: TTXSettings);
|
||||||
|
|
||||||
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
|
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
|
||||||
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
|
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
|
||||||
@@ -225,6 +291,7 @@ type
|
|||||||
property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange;
|
property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange;
|
||||||
property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange;
|
property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange;
|
||||||
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
|
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
|
||||||
|
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -429,9 +496,10 @@ begin
|
|||||||
FNavDisplay := MakeNavButton('Display', 110);
|
FNavDisplay := MakeNavButton('Display', 110);
|
||||||
FNavSpectrum := MakeNavButton('Spectrum', 146);
|
FNavSpectrum := MakeNavButton('Spectrum', 146);
|
||||||
FNavWaterfall := MakeNavButton('Waterfall', 182);
|
FNavWaterfall := MakeNavButton('Waterfall', 182);
|
||||||
FNavPA := MakeNavButton('Power Amplifier', 218);
|
FNavTransmit := MakeNavButton('Transmit', 218);
|
||||||
FNavAdvanced := MakeNavButton('Advanced', 254);
|
FNavPA := MakeNavButton('Power Amplifier', 254);
|
||||||
FNavCAT := MakeNavButton('CAT', 290);
|
FNavAdvanced := MakeNavButton('Advanced', 290);
|
||||||
|
FNavCAT := MakeNavButton('CAT', 326);
|
||||||
|
|
||||||
FContentPanel := TPanel.Create(Self);
|
FContentPanel := TPanel.Create(Self);
|
||||||
FContentPanel.Parent := Self;
|
FContentPanel.Parent := Self;
|
||||||
@@ -443,6 +511,7 @@ begin
|
|||||||
FPageDisplay := MakeScrollPage;
|
FPageDisplay := MakeScrollPage;
|
||||||
FPageSpectrum := MakeScrollPage;
|
FPageSpectrum := MakeScrollPage;
|
||||||
FPageWaterfall := MakeScrollPage;
|
FPageWaterfall := MakeScrollPage;
|
||||||
|
FPageTransmit := MakeScrollPage;
|
||||||
FPagePA := MakeScrollPage;
|
FPagePA := MakeScrollPage;
|
||||||
FPageAdvanced := MakeScrollPage;
|
FPageAdvanced := MakeScrollPage;
|
||||||
FPageCAT := MakeScrollPage;
|
FPageCAT := MakeScrollPage;
|
||||||
@@ -450,6 +519,7 @@ begin
|
|||||||
BuildAudioTab;
|
BuildAudioTab;
|
||||||
BuildDisplayTab;
|
BuildDisplayTab;
|
||||||
BuildRX1Tab;
|
BuildRX1Tab;
|
||||||
|
BuildTransmitTab;
|
||||||
BuildPATab;
|
BuildPATab;
|
||||||
BuildCATTab;
|
BuildCATTab;
|
||||||
|
|
||||||
@@ -477,6 +547,7 @@ begin
|
|||||||
else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay)
|
else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay)
|
||||||
else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum)
|
else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum)
|
||||||
else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall)
|
else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall)
|
||||||
|
else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit)
|
||||||
else if Sender = FNavPA then SelectPage(FPagePA, FNavPA)
|
else if Sender = FNavPA then SelectPage(FPagePA, FNavPA)
|
||||||
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
|
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
|
||||||
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT);
|
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT);
|
||||||
@@ -488,6 +559,7 @@ begin
|
|||||||
FPageDisplay.Visible := APage = FPageDisplay;
|
FPageDisplay.Visible := APage = FPageDisplay;
|
||||||
FPageSpectrum.Visible := APage = FPageSpectrum;
|
FPageSpectrum.Visible := APage = FPageSpectrum;
|
||||||
FPageWaterfall.Visible := APage = FPageWaterfall;
|
FPageWaterfall.Visible := APage = FPageWaterfall;
|
||||||
|
FPageTransmit.Visible := APage = FPageTransmit;
|
||||||
FPagePA.Visible := APage = FPagePA;
|
FPagePA.Visible := APage = FPagePA;
|
||||||
FPageAdvanced.Visible := APage = FPageAdvanced;
|
FPageAdvanced.Visible := APage = FPageAdvanced;
|
||||||
FPageCAT.Visible := APage = FPageCAT;
|
FPageCAT.Visible := APage = FPageCAT;
|
||||||
@@ -496,6 +568,7 @@ begin
|
|||||||
FNavDisplay.Active := ANav = FNavDisplay;
|
FNavDisplay.Active := ANav = FNavDisplay;
|
||||||
FNavSpectrum.Active := ANav = FNavSpectrum;
|
FNavSpectrum.Active := ANav = FNavSpectrum;
|
||||||
FNavWaterfall.Active := ANav = FNavWaterfall;
|
FNavWaterfall.Active := ANav = FNavWaterfall;
|
||||||
|
FNavTransmit.Active := ANav = FNavTransmit;
|
||||||
FNavPA.Active := ANav = FNavPA;
|
FNavPA.Active := ANav = FNavPA;
|
||||||
FNavAdvanced.Active := ANav = FNavAdvanced;
|
FNavAdvanced.Active := ANav = FNavAdvanced;
|
||||||
FNavCAT.Active := ANav = FNavCAT;
|
FNavCAT.Active := ANav = FNavCAT;
|
||||||
@@ -767,6 +840,412 @@ begin
|
|||||||
FWfAGCOffset := MkFloat(CX, R1 + 2 * STEP, SPW, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange);
|
FWfAGCOffset := MkFloat(CX, R1 + 2 * STEP, SPW, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
procedure TSettingsForm.BuildTransmitTab;
|
||||||
|
const
|
||||||
|
MARGIN = 22;
|
||||||
|
PAD = 18;
|
||||||
|
LW = 150;
|
||||||
|
CX = PAD + LW + 12;
|
||||||
|
STEP = 32;
|
||||||
|
R1 = 42;
|
||||||
|
SPW = 96;
|
||||||
|
CMB_W = 200;
|
||||||
|
|
||||||
|
var
|
||||||
|
Grp: TPanel;
|
||||||
|
Y, i: Integer;
|
||||||
|
|
||||||
|
function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TSpinEdit;
|
||||||
|
begin
|
||||||
|
Result := TSpinEdit.Create(Self);
|
||||||
|
Result.Parent := AParent;
|
||||||
|
Result.SetBounds(AX, AY, SPW, BTN_H);
|
||||||
|
Result.MinValue := AMin;
|
||||||
|
Result.MaxValue := AMax;
|
||||||
|
Result.Value := AVal;
|
||||||
|
Result.Color := CLR_INPUT;
|
||||||
|
Result.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
Result.Font.Name := UI_FONT;
|
||||||
|
Result.Font.Size := 9;
|
||||||
|
Result.OnChange := OnTXAnyChange;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function MkFloat(AParent: TWinControl; AX, AY: Integer;
|
||||||
|
AVal, AMin, AMax, AInc: Double): TFloatSpinEdit;
|
||||||
|
begin
|
||||||
|
Result := TFloatSpinEdit.Create(Self);
|
||||||
|
Result.Parent := AParent;
|
||||||
|
Result.SetBounds(AX, AY, SPW, BTN_H);
|
||||||
|
Result.MinValue := AMin;
|
||||||
|
Result.MaxValue := AMax;
|
||||||
|
Result.Increment := AInc;
|
||||||
|
Result.Value := AVal;
|
||||||
|
Result.DecimalPlaces := 2;
|
||||||
|
Result.Color := CLR_INPUT;
|
||||||
|
Result.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
Result.Font.Name := UI_FONT;
|
||||||
|
Result.Font.Size := 9;
|
||||||
|
Result.OnChange := OnTXAnyChange;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function MkChk(AParent: TWinControl; AX, AY, AW: Integer; const ACap: string): TCheckBox;
|
||||||
|
begin
|
||||||
|
Result := TCheckBox.Create(Self);
|
||||||
|
Result.Parent := AParent;
|
||||||
|
Result.Caption := ACap;
|
||||||
|
Result.SetBounds(AX, AY, AW, 22);
|
||||||
|
Result.Font.Color := CLR_TEXT;
|
||||||
|
Result.Font.Name := UI_FONT;
|
||||||
|
Result.Font.Size := 9;
|
||||||
|
Result.OnChange := OnTXAnyChange;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function MkCmb(AParent: TWinControl; AX, AY, AW: Integer): TComboBox;
|
||||||
|
begin
|
||||||
|
Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange);
|
||||||
|
end;
|
||||||
|
|
||||||
|
begin
|
||||||
|
with TLabel.Create(Self) do
|
||||||
|
begin
|
||||||
|
Parent := FPageTransmit;
|
||||||
|
Caption := 'Transmit';
|
||||||
|
SetBounds(MARGIN, 18, 400, 28);
|
||||||
|
Font.Name := UI_FONT;
|
||||||
|
Font.Size := 16;
|
||||||
|
Font.Style := [fsBold];
|
||||||
|
Font.Color := CLR_TEXT;
|
||||||
|
end;
|
||||||
|
with TLabel.Create(Self) do
|
||||||
|
begin
|
||||||
|
Parent := FPageTransmit;
|
||||||
|
Caption := 'TX-цепь WDSP: микрофон, фильтр, динамическая обработка, модуляция.';
|
||||||
|
SetBounds(MARGIN, 48, 700, 20);
|
||||||
|
Font.Name := UI_FONT;
|
||||||
|
Font.Size := 9;
|
||||||
|
Font.Color := CLR_TEXTDIM;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// ---- Mic source / options ----
|
||||||
|
Y := 86;
|
||||||
|
Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, 700, 220);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Mic source', PAD, R1 + 5, LW);
|
||||||
|
FCmbTXMicSource := MkCmb(Grp, CX, R1, CMB_W);
|
||||||
|
FCmbTXMicSource.Items.Add('Radio (HW mic)');
|
||||||
|
FCmbTXMicSource.Items.Add('Sound card');
|
||||||
|
FCmbTXMicSource.ItemIndex := 0;
|
||||||
|
|
||||||
|
FChkTXMicBoost := MkChk(Grp, PAD, R1 + STEP, 200, 'Mic boost +20 dB');
|
||||||
|
FChkTXMicBias := MkChk(Grp, PAD + 200, R1 + STEP, 200, 'Mic bias');
|
||||||
|
FChkTXMicLineIn := MkChk(Grp, PAD + 400, R1 + STEP, 160, 'Line in');
|
||||||
|
FChkTXMicPTT := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'Mic-jack PTT');
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 3 * STEP + 5, LW);
|
||||||
|
FEdTXMicGain := MkFloat(Grp, CX, R1 + 3 * STEP, 10.0, -20.0, 60.0, 0.5);
|
||||||
|
|
||||||
|
// ---- TX bandpass filter ----
|
||||||
|
Inc(Y, 220 + 12);
|
||||||
|
Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, 700, 200);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
|
||||||
|
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 100);
|
||||||
|
MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90);
|
||||||
|
FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 2800);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'FIR length', PAD, R1 + STEP + 5, LW);
|
||||||
|
FCmbTXFiltNC := MkCmb(Grp, CX, R1 + STEP, 140);
|
||||||
|
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']);
|
||||||
|
FCmbTXFiltNC.ItemIndex := 2;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Phase mode', PAD, R1 + 2 * STEP + 5, LW);
|
||||||
|
FCmbTXFiltMP := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
|
||||||
|
FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']);
|
||||||
|
FCmbTXFiltMP.ItemIndex := 1;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Window', PAD, R1 + 3 * STEP + 5, LW);
|
||||||
|
FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 3 * STEP, 240);
|
||||||
|
for i := 0 to High(WINDOW_NAMES) do
|
||||||
|
FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]);
|
||||||
|
FCmbTXFiltWindow.ItemIndex := 1;
|
||||||
|
|
||||||
|
// ---- Compressor / Leveler / ALC / Phase Rotator ----
|
||||||
|
Inc(Y, 200 + 12);
|
||||||
|
Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, 700, 220);
|
||||||
|
|
||||||
|
FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor');
|
||||||
|
MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80);
|
||||||
|
FEdTXCompGain := MkFloat(Grp, PAD + 320, R1, 5.0, 0.0, 20.0, 0.5);
|
||||||
|
|
||||||
|
FChkTXLevOn := MkChk(Grp, PAD, R1 + STEP, 220, 'Leveler');
|
||||||
|
MakeLbl(Grp, 'Top (dB)', PAD + 230, R1 + STEP + 5, 80);
|
||||||
|
FEdTXLevTop := MkFloat(Grp, PAD + 320, R1 + STEP, 5.0, 0.0, 20.0, 0.5);
|
||||||
|
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + STEP + 5, 80);
|
||||||
|
FEdTXLevDecay := MkSpin(Grp, PAD + 520, R1 + STEP, 1, 5000, 500);
|
||||||
|
|
||||||
|
FChkTXALCOn := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'ALC');
|
||||||
|
MakeLbl(Grp, 'Max gain', PAD + 230, R1 + 2 * STEP + 5, 80);
|
||||||
|
FEdTXALCMaxGain := MkFloat(Grp, PAD + 320, R1 + 2 * STEP, 0.0, -20.0, 20.0, 0.5);
|
||||||
|
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + 2 * STEP + 5, 80);
|
||||||
|
FEdTXALCDecay := MkSpin(Grp, PAD + 520, R1 + 2 * STEP, 1, 5000, 10);
|
||||||
|
|
||||||
|
FChkTXPHROn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'Phase Rotator');
|
||||||
|
MakeLbl(Grp, 'Stages', PAD + 230, R1 + 3 * STEP + 5, 80);
|
||||||
|
FEdTXPHRStages := MkSpin(Grp, PAD + 320, R1 + 3 * STEP, 2, 16, 8);
|
||||||
|
MakeLbl(Grp, 'Corner (Hz)', PAD + 430, R1 + 3 * STEP + 5, 80);
|
||||||
|
FEdTXPHRFreq := MkFloat(Grp, PAD + 520, R1 + 3 * STEP, 338.0, 50.0, 3000.0, 1.0);
|
||||||
|
|
||||||
|
// ---- TX EQ ----
|
||||||
|
Inc(Y, 220 + 12);
|
||||||
|
Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, 700, 320);
|
||||||
|
|
||||||
|
FChkTXEQOn := MkChk(Grp, PAD, R1, 200, 'Enable EQ');
|
||||||
|
MakeLbl(Grp, 'Bands', PAD + 230, R1 + 5, 60);
|
||||||
|
FCmbTXEQBands := MkCmb(Grp, PAD + 290, R1, 80);
|
||||||
|
FCmbTXEQBands.Items.AddStrings(['3','10']);
|
||||||
|
FCmbTXEQBands.ItemIndex := 1;
|
||||||
|
|
||||||
|
// 11 строк: [0]=Preamp, [1..10]=полосы. Freq+Gain в одной строке.
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
if i = 0 then
|
||||||
|
MakeLbl(Grp, 'Preamp (dB)', PAD, R1 + STEP + i * 22 + 3, 100)
|
||||||
|
else
|
||||||
|
MakeLbl(Grp, Format('Band %d', [i]), PAD, R1 + STEP + i * 22 + 3, 80);
|
||||||
|
FEdTXEQFreq[i] := MkFloat(Grp, PAD + 90, R1 + STEP + i * 22, 0.0, 0.0, 20000.0, 10.0);
|
||||||
|
FEdTXEQFreq[i].Width := 100;
|
||||||
|
MakeLbl(Grp, 'gain', PAD + 200, R1 + STEP + i * 22 + 3, 30);
|
||||||
|
FEdTXEQGain[i] := MkFloat(Grp, PAD + 230, R1 + STEP + i * 22, 0.0, -15.0, 15.0, 0.5);
|
||||||
|
FEdTXEQGain[i].Width := 80;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// ---- AM / FM / CTCSS ----
|
||||||
|
Inc(Y, 320 + 12);
|
||||||
|
Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, 700, 220);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'AM carrier level', PAD, R1 + 5, LW);
|
||||||
|
FEdTXAMCarrier := MkFloat(Grp, CX, R1, 0.5, 0.0, 1.0, 0.05);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + STEP + 5, LW);
|
||||||
|
FEdTXFMDev := MkFloat(Grp, CX, R1 + STEP, 5000.0, 1000.0, 25000.0, 100.0);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'FM AF low cut', PAD, R1 + 2 * STEP + 5, LW);
|
||||||
|
FEdTXFMLow := MkSpin(Grp, CX, R1 + 2 * STEP, 0, 5000, 300);
|
||||||
|
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + 2 * STEP + 5, 100);
|
||||||
|
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + 2 * STEP, 100, 8000, 3000);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'FM emphasis', PAD, R1 + 3 * STEP + 5, LW);
|
||||||
|
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 3 * STEP, 180);
|
||||||
|
FCmbTXFMEmph.Items.AddStrings(['Pre-EQ','Post-EQ']);
|
||||||
|
FCmbTXFMEmph.ItemIndex := 1;
|
||||||
|
|
||||||
|
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 4 * STEP, 220, 'CTCSS tone');
|
||||||
|
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 4 * STEP + 5, 80);
|
||||||
|
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1);
|
||||||
|
|
||||||
|
// ---- TX Display (отдельный analyzer для TX-спектра/водопада) ----
|
||||||
|
Inc(Y, 220 + 12);
|
||||||
|
Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, 700, 290);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW);
|
||||||
|
FCmbTXFFT := MkCmb(Grp, CX, R1, 140);
|
||||||
|
for i := 0 to High(FFT_NAMES) do FCmbTXFFT.Items.Add(FFT_NAMES[i]);
|
||||||
|
FCmbTXFFT.ItemIndex := 2;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Window', PAD, R1 + STEP + 5, LW);
|
||||||
|
FCmbTXWindow := MkCmb(Grp, CX, R1 + STEP, 240);
|
||||||
|
for i := 0 to High(WINDOW_NAMES) do FCmbTXWindow.Items.Add(WINDOW_NAMES[i]);
|
||||||
|
FCmbTXWindow.ItemIndex := 2;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Spec detector',PAD, R1 + 2 * STEP + 5, LW);
|
||||||
|
FCmbTXSpecDet := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
|
||||||
|
for i := 0 to High(DETECTOR_NAMES) do FCmbTXSpecDet.Items.Add(DETECTOR_NAMES[i]);
|
||||||
|
FCmbTXSpecDet.ItemIndex := 0;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Spec average', PAD, R1 + 3 * STEP + 5, LW);
|
||||||
|
FCmbTXSpecAvg := MkCmb(Grp, CX, R1 + 3 * STEP, 200);
|
||||||
|
for i := 0 to High(AVGMODE_NAMES) do FCmbTXSpecAvg.Items.Add(AVGMODE_NAMES[i]);
|
||||||
|
FCmbTXSpecAvg.ItemIndex := 3;
|
||||||
|
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 3 * STEP + 5, 100);
|
||||||
|
FEdTXSpecAvgTime := MkFloat(Grp, PAD + 540, R1 + 3 * STEP, 30.0, 1.0, 5000.0, 1.0);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'WF detector', PAD, R1 + 4 * STEP + 5, LW);
|
||||||
|
FCmbTXWfDet := MkCmb(Grp, CX, R1 + 4 * STEP, 200);
|
||||||
|
for i := 0 to High(DETECTOR_NAMES) do FCmbTXWfDet.Items.Add(DETECTOR_NAMES[i]);
|
||||||
|
FCmbTXWfDet.ItemIndex := 0;
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'WF average', PAD, R1 + 5 * STEP + 5, LW);
|
||||||
|
FCmbTXWfAvg := MkCmb(Grp, CX, R1 + 5 * STEP, 200);
|
||||||
|
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbTXWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
|
||||||
|
FCmbTXWfAvg.ItemIndex := 3;
|
||||||
|
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 5 * STEP + 5, 100);
|
||||||
|
FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.CollectTXFromUI;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
if FCmbTXMicSource <> nil then
|
||||||
|
FTX.MicSource := Max(0, FCmbTXMicSource.ItemIndex);
|
||||||
|
if FChkTXMicBoost <> nil then FTX.MicBoost := FChkTXMicBoost.Checked;
|
||||||
|
if FChkTXMicBias <> nil then FTX.MicBias := FChkTXMicBias.Checked;
|
||||||
|
if FChkTXMicLineIn <> nil then FTX.MicLineIn := FChkTXMicLineIn.Checked;
|
||||||
|
if FChkTXMicPTT <> nil then FTX.MicPTTEnabled := FChkTXMicPTT.Checked;
|
||||||
|
if FEdTXMicGain <> nil then FTX.MicGainDB := FEdTXMicGain.Value;
|
||||||
|
if FEdTXFiltLow <> nil then FTX.FilterLow := FEdTXFiltLow.Value;
|
||||||
|
if FEdTXFiltHigh <> nil then FTX.FilterHigh := FEdTXFiltHigh.Value;
|
||||||
|
if FCmbTXFiltNC <> nil then
|
||||||
|
FTX.FilterNC := StrToIntDef(FCmbTXFiltNC.Text, 1024);
|
||||||
|
if FCmbTXFiltMP <> nil then FTX.FilterMP := Max(0, FCmbTXFiltMP.ItemIndex);
|
||||||
|
if FCmbTXFiltWindow<> nil then FTX.FilterWindow := Max(0, FCmbTXFiltWindow.ItemIndex);
|
||||||
|
if FChkTXCompOn <> nil then FTX.CompressorOn := FChkTXCompOn.Checked;
|
||||||
|
if FEdTXCompGain <> nil then FTX.CompressorGain := FEdTXCompGain.Value;
|
||||||
|
if FChkTXLevOn <> nil then FTX.LevelerOn := FChkTXLevOn.Checked;
|
||||||
|
if FEdTXLevTop <> nil then FTX.LevelerTop := FEdTXLevTop.Value;
|
||||||
|
if FEdTXLevDecay <> nil then FTX.LevelerDecay := FEdTXLevDecay.Value;
|
||||||
|
if FChkTXALCOn <> nil then FTX.ALCOn := FChkTXALCOn.Checked;
|
||||||
|
if FEdTXALCMaxGain <> nil then FTX.ALCMaxGain := FEdTXALCMaxGain.Value;
|
||||||
|
if FEdTXALCDecay <> nil then FTX.ALCDecay := FEdTXALCDecay.Value;
|
||||||
|
if FChkTXPHROn <> nil then FTX.PhaseRotOn := FChkTXPHROn.Checked;
|
||||||
|
if FEdTXPHRStages <> nil then FTX.PhaseRotStages := FEdTXPHRStages.Value;
|
||||||
|
if FEdTXPHRFreq <> nil then FTX.PhaseRotFreq := FEdTXPHRFreq.Value;
|
||||||
|
if FChkTXEQOn <> nil then FTX.EQOn := FChkTXEQOn.Checked;
|
||||||
|
if FCmbTXEQBands <> nil then
|
||||||
|
if FCmbTXEQBands.ItemIndex = 0 then FTX.EQNumBands := 3 else FTX.EQNumBands := 10;
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
if FEdTXEQGain[i] <> nil then FTX.EQGains[i] := FEdTXEQGain[i].Value;
|
||||||
|
if FEdTXEQFreq[i] <> nil then FTX.EQFreqs[i] := FEdTXEQFreq[i].Value;
|
||||||
|
end;
|
||||||
|
if FEdTXAMCarrier <> nil then FTX.AMCarrierLevel := FEdTXAMCarrier.Value;
|
||||||
|
if FEdTXFMDev <> nil then FTX.FMDeviation := FEdTXFMDev.Value;
|
||||||
|
if FEdTXFMLow <> nil then FTX.FMLowCut := FEdTXFMLow.Value;
|
||||||
|
if FEdTXFMHigh <> nil then FTX.FMHighCut := FEdTXFMHigh.Value;
|
||||||
|
if FCmbTXFMEmph <> nil then FTX.FMEmphPosition := Max(0, FCmbTXFMEmph.ItemIndex);
|
||||||
|
if FChkTXCTCSSOn <> nil then FTX.CTCSSOn := FChkTXCTCSSOn.Checked;
|
||||||
|
if FEdTXCTCSSFreq <> nil then FTX.CTCSSFreq := FEdTXCTCSSFreq.Value;
|
||||||
|
if (FCmbTXFFT <> nil) and (FCmbTXFFT.ItemIndex >= 0) then
|
||||||
|
FTX.TXFFTSize := FFT_SIZES[FCmbTXFFT.ItemIndex];
|
||||||
|
if FCmbTXWindow <> nil then FTX.TXWindowType := Max(0, FCmbTXWindow.ItemIndex);
|
||||||
|
if FCmbTXSpecDet <> nil then FTX.TXSpecDetector := Max(0, FCmbTXSpecDet.ItemIndex);
|
||||||
|
if (FCmbTXSpecAvg <> nil) and (FCmbTXSpecAvg.ItemIndex >= 0) then
|
||||||
|
FTX.TXSpecAvgMode := AVGMODE_VALUES[FCmbTXSpecAvg.ItemIndex];
|
||||||
|
if FEdTXSpecAvgTime <> nil then FTX.TXSpecAvgTimeMS := FEdTXSpecAvgTime.Value;
|
||||||
|
if FCmbTXWfDet <> nil then FTX.TXWfDetector := Max(0, FCmbTXWfDet.ItemIndex);
|
||||||
|
if (FCmbTXWfAvg <> nil) and (FCmbTXWfAvg.ItemIndex >= 0) then
|
||||||
|
FTX.TXWfAvgMode := WF_AVGMODE_VALUES[FCmbTXWfAvg.ItemIndex];
|
||||||
|
if FEdTXWfAvgTime <> nil then FTX.TXWfAvgTimeMS := FEdTXWfAvgTime.Value;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.FireTXChange;
|
||||||
|
begin
|
||||||
|
if FLoading then Exit;
|
||||||
|
CollectTXFromUI;
|
||||||
|
if Assigned(FOnTXChange) then
|
||||||
|
FOnTXChange(FTX);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.OnTXAnyChange(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FireTXChange;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.LoadTXSettings(const T: TTXSettings);
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
FLoading := True;
|
||||||
|
try
|
||||||
|
FTX := T;
|
||||||
|
if FCmbTXMicSource <> nil then
|
||||||
|
FCmbTXMicSource.ItemIndex := EnsureRange(T.MicSource, 0, FCmbTXMicSource.Items.Count - 1);
|
||||||
|
if FChkTXMicBoost <> nil then FChkTXMicBoost.Checked := T.MicBoost;
|
||||||
|
if FChkTXMicBias <> nil then FChkTXMicBias.Checked := T.MicBias;
|
||||||
|
if FChkTXMicLineIn <> nil then FChkTXMicLineIn.Checked := T.MicLineIn;
|
||||||
|
if FChkTXMicPTT <> nil then FChkTXMicPTT.Checked := T.MicPTTEnabled;
|
||||||
|
if FEdTXMicGain <> nil then FEdTXMicGain.Value := T.MicGainDB;
|
||||||
|
if FEdTXFiltLow <> nil then FEdTXFiltLow.Value := T.FilterLow;
|
||||||
|
if FEdTXFiltHigh <> nil then FEdTXFiltHigh.Value := T.FilterHigh;
|
||||||
|
if FCmbTXFiltNC <> nil then FCmbTXFiltNC.Text := IntToStr(T.FilterNC);
|
||||||
|
if FCmbTXFiltMP <> nil then
|
||||||
|
FCmbTXFiltMP.ItemIndex := EnsureRange(T.FilterMP, 0, FCmbTXFiltMP.Items.Count - 1);
|
||||||
|
if FCmbTXFiltWindow<> nil then
|
||||||
|
FCmbTXFiltWindow.ItemIndex := EnsureRange(T.FilterWindow, 0, FCmbTXFiltWindow.Items.Count - 1);
|
||||||
|
if FChkTXCompOn <> nil then FChkTXCompOn.Checked := T.CompressorOn;
|
||||||
|
if FEdTXCompGain <> nil then FEdTXCompGain.Value := T.CompressorGain;
|
||||||
|
if FChkTXLevOn <> nil then FChkTXLevOn.Checked := T.LevelerOn;
|
||||||
|
if FEdTXLevTop <> nil then FEdTXLevTop.Value := T.LevelerTop;
|
||||||
|
if FEdTXLevDecay <> nil then FEdTXLevDecay.Value := T.LevelerDecay;
|
||||||
|
if FChkTXALCOn <> nil then FChkTXALCOn.Checked := T.ALCOn;
|
||||||
|
if FEdTXALCMaxGain <> nil then FEdTXALCMaxGain.Value := T.ALCMaxGain;
|
||||||
|
if FEdTXALCDecay <> nil then FEdTXALCDecay.Value := T.ALCDecay;
|
||||||
|
if FChkTXPHROn <> nil then FChkTXPHROn.Checked := T.PhaseRotOn;
|
||||||
|
if FEdTXPHRStages <> nil then FEdTXPHRStages.Value := T.PhaseRotStages;
|
||||||
|
if FEdTXPHRFreq <> nil then FEdTXPHRFreq.Value := T.PhaseRotFreq;
|
||||||
|
if FChkTXEQOn <> nil then FChkTXEQOn.Checked := T.EQOn;
|
||||||
|
if FCmbTXEQBands <> nil then
|
||||||
|
if T.EQNumBands <= 3 then FCmbTXEQBands.ItemIndex := 0
|
||||||
|
else FCmbTXEQBands.ItemIndex := 1;
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
if FEdTXEQGain[i] <> nil then FEdTXEQGain[i].Value := T.EQGains[i];
|
||||||
|
if FEdTXEQFreq[i] <> nil then FEdTXEQFreq[i].Value := T.EQFreqs[i];
|
||||||
|
end;
|
||||||
|
if FEdTXAMCarrier <> nil then FEdTXAMCarrier.Value := T.AMCarrierLevel;
|
||||||
|
if FEdTXFMDev <> nil then FEdTXFMDev.Value := T.FMDeviation;
|
||||||
|
if FEdTXFMLow <> nil then FEdTXFMLow.Value := T.FMLowCut;
|
||||||
|
if FEdTXFMHigh <> nil then FEdTXFMHigh.Value := T.FMHighCut;
|
||||||
|
if FCmbTXFMEmph <> nil then
|
||||||
|
FCmbTXFMEmph.ItemIndex := EnsureRange(T.FMEmphPosition, 0, FCmbTXFMEmph.Items.Count - 1);
|
||||||
|
if FChkTXCTCSSOn <> nil then FChkTXCTCSSOn.Checked := T.CTCSSOn;
|
||||||
|
if FEdTXCTCSSFreq <> nil then FEdTXCTCSSFreq.Value := T.CTCSSFreq;
|
||||||
|
|
||||||
|
if FCmbTXFFT <> nil then
|
||||||
|
begin
|
||||||
|
FCmbTXFFT.ItemIndex := 2;
|
||||||
|
for i := 0 to High(FFT_SIZES) do
|
||||||
|
if FFT_SIZES[i] = T.TXFFTSize then
|
||||||
|
begin
|
||||||
|
FCmbTXFFT.ItemIndex := i;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if FCmbTXWindow <> nil then
|
||||||
|
FCmbTXWindow.ItemIndex := EnsureRange(T.TXWindowType, 0, FCmbTXWindow.Items.Count - 1);
|
||||||
|
if FCmbTXSpecDet <> nil then
|
||||||
|
FCmbTXSpecDet.ItemIndex := EnsureRange(T.TXSpecDetector, 0, FCmbTXSpecDet.Items.Count - 1);
|
||||||
|
if FCmbTXSpecAvg <> nil then
|
||||||
|
begin
|
||||||
|
FCmbTXSpecAvg.ItemIndex := 3;
|
||||||
|
for i := 0 to High(AVGMODE_VALUES) do
|
||||||
|
if AVGMODE_VALUES[i] = T.TXSpecAvgMode then
|
||||||
|
begin
|
||||||
|
FCmbTXSpecAvg.ItemIndex := i;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if FEdTXSpecAvgTime <> nil then FEdTXSpecAvgTime.Value := T.TXSpecAvgTimeMS;
|
||||||
|
if FCmbTXWfDet <> nil then
|
||||||
|
FCmbTXWfDet.ItemIndex := EnsureRange(T.TXWfDetector, 0, FCmbTXWfDet.Items.Count - 1);
|
||||||
|
if FCmbTXWfAvg <> nil then
|
||||||
|
begin
|
||||||
|
FCmbTXWfAvg.ItemIndex := 3;
|
||||||
|
for i := 0 to High(WF_AVGMODE_VALUES) do
|
||||||
|
if WF_AVGMODE_VALUES[i] = T.TXWfAvgMode then
|
||||||
|
begin
|
||||||
|
FCmbTXWfAvg.ItemIndex := i;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if FEdTXWfAvgTime <> nil then FEdTXWfAvgTime.Value := T.TXWfAvgTimeMS;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// BuildPATab
|
// BuildPATab
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+6
-5
@@ -274,19 +274,20 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// Цветовая схема водопада для светлой темы.
|
// Цветовая схема водопада для светлой темы.
|
||||||
// Шум исчезает в фоне (кремовый), сигналы: зелень → бирюза → синева → индиго.
|
// Шум растворяется в фоне панели; сигналы: синий → cyan → зелёный → жёлтый → оранжевый → красный.
|
||||||
function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord;
|
function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord;
|
||||||
const
|
const
|
||||||
NSTOPS = 8;
|
NSTOPS = 8;
|
||||||
SR: array[0..NSTOPS-1] of Integer = (240, 210, 148, 60, 14, 8, 24, 64);
|
// panel steel blue cyan green yell orange red
|
||||||
SG: array[0..NSTOPS-1] of Integer = (238, 228, 200, 164, 132, 96, 52, 6);
|
SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255);
|
||||||
SB: array[0..NSTOPS-1] of Integer = (230, 190, 118, 68, 96, 148, 166, 100);
|
SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20);
|
||||||
|
SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20);
|
||||||
var
|
var
|
||||||
T: Double;
|
T: Double;
|
||||||
Seg: Integer;
|
Seg: Integer;
|
||||||
R, G, B: Integer;
|
R, G, B: Integer;
|
||||||
begin
|
begin
|
||||||
if ValueDB <= LowDB then begin Result := $FFF0EEE6; Exit; end;
|
if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end;
|
||||||
if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end;
|
if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end;
|
||||||
T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1);
|
T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1);
|
||||||
Seg := Min(NSTOPS - 2, Trunc(T));
|
Seg := Min(NSTOPS - 2, Trunc(T));
|
||||||
|
|||||||
+533
-73
@@ -95,7 +95,8 @@ const
|
|||||||
RXA_CHAN = 0;
|
RXA_CHAN = 0;
|
||||||
TXA_CHAN = 1; // используем разные каналы для RX и TX
|
TXA_CHAN = 1; // используем разные каналы для RX и TX
|
||||||
|
|
||||||
DISP_ID = 0; // ID для XCreateAnalyzer
|
RX_DISP_ID = 0; // analyzer для RX-спектра/водопада
|
||||||
|
TX_DISP_ID = 1; // analyzer для TX-спектра/водопада
|
||||||
DSP_BUFSIZE = 1024;
|
DSP_BUFSIZE = 1024;
|
||||||
SPECTRUM_PIXELS = 1024;
|
SPECTRUM_PIXELS = 1024;
|
||||||
DISPLAY_BLOCK_SIZE = 64;
|
DISPLAY_BLOCK_SIZE = 64;
|
||||||
@@ -127,6 +128,13 @@ type
|
|||||||
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
||||||
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
||||||
Count: Integer) of object;
|
Count: Integer) of object;
|
||||||
|
// Sound-card mic source: TTXDSPThread дёргает этот колбэк перед каждым тиком,
|
||||||
|
// чтобы вытянуть накопленные сэмплы из внешнего буфера (например PortAudio).
|
||||||
|
// Реализация должна вернуть кол-во отправленных в WDSP сэмплов.
|
||||||
|
TPullMicSamplesFunc = function(Max: Integer): Integer of object;
|
||||||
|
|
||||||
|
// TX mic source: откуда получаем сэмплы для модулятора.
|
||||||
|
TTXMicSource = (txmsRadio, txmsSoundCard);
|
||||||
|
|
||||||
{ TWDSPEngine }
|
{ TWDSPEngine }
|
||||||
TWDSPEngine = class
|
TWDSPEngine = class
|
||||||
@@ -154,6 +162,10 @@ type
|
|||||||
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
|
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
|
||||||
FDispBuf: array of Double;
|
FDispBuf: array of Double;
|
||||||
FDispPos: Integer;
|
FDispPos: Integer;
|
||||||
|
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
|
||||||
|
// по DISPLAY_BLOCK_SIZE из FTXOut в ProcessTXBlock.
|
||||||
|
FTXDispBuf: array of Double;
|
||||||
|
FTXDispPos: Integer;
|
||||||
// Аудио выходные буферы — аллоцируем один раз
|
// Аудио выходные буферы — аллоцируем один раз
|
||||||
FOutL: array of Single;
|
FOutL: array of Single;
|
||||||
FOutR: array of Single;
|
FOutR: array of Single;
|
||||||
@@ -196,6 +208,50 @@ type
|
|||||||
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
|
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
|
||||||
FTXThread: TThread;
|
FTXThread: TThread;
|
||||||
FTXActive: Boolean;
|
FTXActive: Boolean;
|
||||||
|
FActiveDisplayID: Integer; // RX_DISP_ID или TX_DISP_ID
|
||||||
|
FTXMicSource: TTXMicSource;
|
||||||
|
FOnPullMic: TPullMicSamplesFunc;
|
||||||
|
|
||||||
|
// Кэшированные TX-параметры — применяются после OpenChannel(TXA) и при изменении.
|
||||||
|
// Для Filter/Compressor/Leveler/ALC/PhaseRot/EQ/AM/FM/CTCSS.
|
||||||
|
FTXFilterLow: Integer;
|
||||||
|
FTXFilterHigh: Integer;
|
||||||
|
FTXFilterNC: Integer;
|
||||||
|
FTXFilterMP: Integer;
|
||||||
|
FTXFilterWindow: Integer;
|
||||||
|
FTXMicGainDB: Double;
|
||||||
|
FTXCompressorOn: Boolean;
|
||||||
|
FTXCompressorGain:Double;
|
||||||
|
FTXLevelerOn: Boolean;
|
||||||
|
FTXLevelerTop: Double;
|
||||||
|
FTXLevelerDecay: Integer;
|
||||||
|
FTXALCOn: Boolean;
|
||||||
|
FTXALCMaxGain: Double;
|
||||||
|
FTXALCDecay: Integer;
|
||||||
|
FTXPhaseRotOn: Boolean;
|
||||||
|
FTXPhaseRotStages:Integer;
|
||||||
|
FTXPhaseRotFreq: Double;
|
||||||
|
FTXEQOn: Boolean;
|
||||||
|
FTXEQNumBands: Integer;
|
||||||
|
FTXEQGains: array[0..10] of Double;
|
||||||
|
FTXEQFreqs: array[0..10] of Double;
|
||||||
|
FTXAMCarrier: Double;
|
||||||
|
FTXFMDeviation: Double;
|
||||||
|
FTXFMLowCut: Integer;
|
||||||
|
FTXFMHighCut: Integer;
|
||||||
|
FTXFMEmphPos: Integer;
|
||||||
|
FTXCTCSSOn: Boolean;
|
||||||
|
FTXCTCSSFreq: Double;
|
||||||
|
|
||||||
|
// TX-display параметры (отдельные от RX) — применяются к TX_DISP_ID.
|
||||||
|
FTXFFTSize: Integer;
|
||||||
|
FTXWindowType: Integer;
|
||||||
|
FTXSpecDetector: Integer;
|
||||||
|
FTXSpecAvgMode: Integer;
|
||||||
|
FTXSpecAvgTimeMS: Double;
|
||||||
|
FTXWfDetector: Integer;
|
||||||
|
FTXWfAvgMode: Integer;
|
||||||
|
FTXWfAvgTimeMS: Double;
|
||||||
|
|
||||||
FMode: Integer;
|
FMode: Integer;
|
||||||
FSMeter: Double;
|
FSMeter: Double;
|
||||||
@@ -239,6 +295,10 @@ type
|
|||||||
procedure ApplyANFState;
|
procedure ApplyANFState;
|
||||||
procedure ApplyNoiseFilterState;
|
procedure ApplyNoiseFilterState;
|
||||||
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
||||||
|
procedure FeedTXDisplaySample(const I, Q: Double);
|
||||||
|
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
|
||||||
|
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
||||||
|
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
|
||||||
|
|
||||||
public
|
public
|
||||||
constructor Create(SampleRate: Integer = 192000;
|
constructor Create(SampleRate: Integer = 192000;
|
||||||
@@ -283,9 +343,33 @@ type
|
|||||||
procedure SetMicGain(GainDB: Double);
|
procedure SetMicGain(GainDB: Double);
|
||||||
procedure SetTXRun(Run: Boolean);
|
procedure SetTXRun(Run: Boolean);
|
||||||
|
|
||||||
// TX mic вход — вызывается из receive thread (thread-safe lock-free)
|
// Расширенное TX управление (применяются мгновенно если канал открыт)
|
||||||
// Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов
|
procedure SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||||||
|
procedure SetTXCompressor(Enable: Boolean; GainDB: Double);
|
||||||
|
procedure SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||||||
|
procedure SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||||||
|
procedure SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
|
||||||
|
procedure SetTXEQ(Enable: Boolean; NumBands: Integer;
|
||||||
|
const Gains, Freqs: array of Double);
|
||||||
|
procedure SetTXAMCarrierLevel(Level: Double);
|
||||||
|
procedure SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
|
||||||
|
procedure SetTXCTCSS(Enable: Boolean; FreqHz: Double);
|
||||||
|
// Источник микрофона: Radio (HW mic-stream) или SoundCard (PortAudio).
|
||||||
|
procedure SetTXMicSource(Source: TTXMicSource);
|
||||||
|
|
||||||
|
// TX display: отдельные FFT/window/detector/avg для TX-analyzer (TX_DISP_ID).
|
||||||
|
// Аналог RX-сеттеров, но применяются только к TX-anaylzer.
|
||||||
|
procedure SetTXFFTParams(FFTSize, WinType: Integer);
|
||||||
|
procedure SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||||
|
procedure SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||||
|
|
||||||
|
// TX mic вход — вызывается из receive thread (thread-safe lock-free).
|
||||||
|
// Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов.
|
||||||
|
// Если MicSource=SoundCard, эти сэмплы игнорируются.
|
||||||
procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
procedure PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
||||||
|
// Прямой push double-сэмпла (-1..+1), для sound-card пути.
|
||||||
|
// Должен вызываться из ОДНОГО потока (PortAudio callback).
|
||||||
|
procedure PushTXMicSampleD(const S: Double);
|
||||||
|
|
||||||
// Настройки анализатора (применяются немедленно если открыт)
|
// Настройки анализатора (применяются немедленно если открыт)
|
||||||
procedure SetFFTParams(FFTSize, WinType: Integer);
|
procedure SetFFTParams(FFTSize, WinType: Integer);
|
||||||
@@ -329,6 +413,24 @@ type
|
|||||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||||
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
||||||
property TXActive: Boolean read FTXActive;
|
property TXActive: Boolean read FTXActive;
|
||||||
|
// Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive).
|
||||||
|
// SpectrumView/Waterfall должны читать пиксели именно из этого ID.
|
||||||
|
property ActiveDisplayID: Integer read FActiveDisplayID;
|
||||||
|
property TXMicSource: TTXMicSource read FTXMicSource;
|
||||||
|
property OnPullMicSamples: TPullMicSamplesFunc
|
||||||
|
read FOnPullMic write FOnPullMic;
|
||||||
|
|
||||||
|
// Диагностика mic ring buffer: сколько сэмплов накоплено TX-цепью.
|
||||||
|
function TXMicRingAvail: Integer;
|
||||||
|
|
||||||
|
property TXFFTSize: Integer read FTXFFTSize;
|
||||||
|
property TXWindowType: Integer read FTXWindowType;
|
||||||
|
property TXSpecDetector: Integer read FTXSpecDetector;
|
||||||
|
property TXSpecAvgMode: Integer read FTXSpecAvgMode;
|
||||||
|
property TXSpecAvgTimeMS: Double read FTXSpecAvgTimeMS;
|
||||||
|
property TXWfDetector: Integer read FTXWfDetector;
|
||||||
|
property TXWfAvgMode: Integer read FTXWfAvgMode;
|
||||||
|
property TXWfAvgTimeMS: Double read FTXWfAvgTimeMS;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -426,6 +528,13 @@ begin
|
|||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
if not FEngine.FTXActive then Continue;
|
if not FEngine.FTXActive then Continue;
|
||||||
|
|
||||||
|
// Если выбран sound-card mic — тянем сэмплы из внешнего источника
|
||||||
|
// (PortAudio через FOnPullMic в MainForm) ПЕРЕД проверкой ring buffer.
|
||||||
|
// Callback сам пушит сэмплы в FTXMicRing через PushTXMicSampleD.
|
||||||
|
if (FEngine.FTXMicSource = txmsSoundCard) and
|
||||||
|
Assigned(FEngine.FOnPullMic) then
|
||||||
|
FEngine.FOnPullMic(FEngine.FAudioBufSize * 2);
|
||||||
|
|
||||||
// Обрабатываем накопившиеся блоки с реальными mic-данными
|
// Обрабатываем накопившиеся блоки с реальными mic-данными
|
||||||
repeat
|
repeat
|
||||||
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
||||||
@@ -571,7 +680,9 @@ begin
|
|||||||
SetLength(FRXAccQ, FBufSize);
|
SetLength(FRXAccQ, FBufSize);
|
||||||
SetLength(FSpecBuf, FBufSize * 2);
|
SetLength(FSpecBuf, FBufSize * 2);
|
||||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||||
|
SetLength(FTXDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||||
FDispPos := 0;
|
FDispPos := 0;
|
||||||
|
FTXDispPos := 0;
|
||||||
SetLength(FOutL, FAudioBufSize);
|
SetLength(FOutL, FAudioBufSize);
|
||||||
SetLength(FOutR, FAudioBufSize);
|
SetLength(FOutR, FAudioBufSize);
|
||||||
|
|
||||||
@@ -584,12 +695,68 @@ begin
|
|||||||
FDisplayThread := nil;
|
FDisplayThread := nil;
|
||||||
|
|
||||||
// TX инициализация
|
// TX инициализация
|
||||||
FTXMicHead := 0;
|
FTXMicHead := 0;
|
||||||
FTXMicTail := 0;
|
FTXMicTail := 0;
|
||||||
FTXActive := False;
|
FTXActive := False;
|
||||||
FTXThread := nil;
|
FActiveDisplayID := RX_DISP_ID;
|
||||||
FTXMicSem := RTLEventCreate;
|
FTXMicSource := txmsRadio;
|
||||||
|
FOnPullMic := nil;
|
||||||
|
FTXThread := nil;
|
||||||
|
FTXMicSem := RTLEventCreate;
|
||||||
FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
|
FillChar(FTXMicRing, SizeOf(FTXMicRing), 0);
|
||||||
|
|
||||||
|
// TX-цепь — дефолты, согласуются с Settings.DefaultTX
|
||||||
|
FTXFilterLow := 100;
|
||||||
|
FTXFilterHigh := 2800;
|
||||||
|
FTXFilterNC := 1024;
|
||||||
|
FTXFilterMP := 1;
|
||||||
|
FTXFilterWindow := 1;
|
||||||
|
FTXMicGainDB := 10.0;
|
||||||
|
FTXCompressorOn := False;
|
||||||
|
FTXCompressorGain:= 5.0;
|
||||||
|
FTXLevelerOn := True;
|
||||||
|
FTXLevelerTop := 5.0;
|
||||||
|
FTXLevelerDecay := 500;
|
||||||
|
FTXALCOn := True;
|
||||||
|
FTXALCMaxGain := 0.0;
|
||||||
|
FTXALCDecay := 10;
|
||||||
|
FTXPhaseRotOn := False;
|
||||||
|
FTXPhaseRotStages:= 8;
|
||||||
|
FTXPhaseRotFreq := 338.0;
|
||||||
|
FTXEQOn := False;
|
||||||
|
FTXEQNumBands := 10;
|
||||||
|
FillChar(FTXEQGains, SizeOf(FTXEQGains), 0);
|
||||||
|
// Дефолтные центры полос EQ (Hz). Должны быть валидными ДАЖЕ при EQOn=False —
|
||||||
|
// SetTXAEQProfile вызывается в ApplyTXChainSettings всегда, и нулевые freqs
|
||||||
|
// могут сломать TX-фильтры внутри WDSP.
|
||||||
|
FTXEQFreqs[0] := 0;
|
||||||
|
FTXEQFreqs[1] := 100;
|
||||||
|
FTXEQFreqs[2] := 200;
|
||||||
|
FTXEQFreqs[3] := 400;
|
||||||
|
FTXEQFreqs[4] := 700;
|
||||||
|
FTXEQFreqs[5] := 1100;
|
||||||
|
FTXEQFreqs[6] := 1500;
|
||||||
|
FTXEQFreqs[7] := 2000;
|
||||||
|
FTXEQFreqs[8] := 2500;
|
||||||
|
FTXEQFreqs[9] := 3000;
|
||||||
|
FTXEQFreqs[10] := 3500;
|
||||||
|
FTXAMCarrier := 0.5;
|
||||||
|
FTXFMDeviation := 5000.0;
|
||||||
|
FTXFMLowCut := 300;
|
||||||
|
FTXFMHighCut := 3000;
|
||||||
|
FTXFMEmphPos := 1;
|
||||||
|
FTXCTCSSOn := False;
|
||||||
|
FTXCTCSSFreq := 100.0;
|
||||||
|
|
||||||
|
// TX-display defaults
|
||||||
|
FTXFFTSize := 16384;
|
||||||
|
FTXWindowType := 2;
|
||||||
|
FTXSpecDetector := 0;
|
||||||
|
FTXSpecAvgMode := 3;
|
||||||
|
FTXSpecAvgTimeMS := 30.0;
|
||||||
|
FTXWfDetector := 0;
|
||||||
|
FTXWfAvgMode := 3;
|
||||||
|
FTXWfAvgTimeMS := 120.0;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TWDSPEngine.Destroy;
|
destructor TWDSPEngine.Destroy;
|
||||||
@@ -612,8 +779,19 @@ begin
|
|||||||
if FAnalyzerOpen then Exit;
|
if FAnalyzerOpen then Exit;
|
||||||
|
|
||||||
Success := -1;
|
Success := -1;
|
||||||
XCreateAnalyzer(DISP_ID, @Success, 262144, 1, 1, nil);
|
XCreateAnalyzer(RX_DISP_ID, @Success, 262144, 1, 1, nil);
|
||||||
if Success <> 0 then Exit;
|
if Success <> 0 then
|
||||||
|
begin
|
||||||
|
FLastError := Format('XCreateAnalyzer(RX) failed: %d', [Success]);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Отдельный analyzer для TX-спектра/водопада. Ошибку логируем в LastError,
|
||||||
|
// но не считаем фатальной — RX без TX-display продолжает работать.
|
||||||
|
Success := -1;
|
||||||
|
XCreateAnalyzer(TX_DISP_ID, @Success, 262144, 1, 1, nil);
|
||||||
|
if Success <> 0 then
|
||||||
|
FLastError := Format('XCreateAnalyzer(TX) failed: %d', [Success]);
|
||||||
|
|
||||||
FFlp[0] := 0;
|
FFlp[0] := 0;
|
||||||
ApplyAnalyzerSettings;
|
ApplyAnalyzerSettings;
|
||||||
@@ -623,53 +801,81 @@ begin
|
|||||||
FDisplayThread := TDisplayThread.Create(Self);
|
FDisplayThread := TDisplayThread.Create(Self);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
procedure TWDSPEngine.ApplyRXAnalyzerSettings;
|
||||||
// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
|
// Применяет FFFTSize / FWindowType / FSpec* / FWf* к RX_DISP_ID.
|
||||||
var
|
var
|
||||||
AnalyzerFFT: Integer;
|
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
|
||||||
AvgCount: Integer;
|
Backmult, WfBackmult: Double;
|
||||||
Backmult: Double;
|
const KEEP_TIME_SEC = 0.10;
|
||||||
WaterfallAvgCount: Integer;
|
|
||||||
WaterfallBackmult: Double;
|
|
||||||
MaxW: Integer;
|
|
||||||
Overlap: Integer;
|
|
||||||
const
|
|
||||||
KEEP_TIME_SEC = 0.10;
|
|
||||||
var
|
|
||||||
DisplaySampleRate: Integer;
|
|
||||||
DisplayPixels: Integer;
|
|
||||||
begin
|
begin
|
||||||
// FFT Size пользователя управляет именно display-analyzer, как в Thetis,
|
|
||||||
// но визуальная нормализация должна идти к ширине видимого окна, как в piHPSDR.
|
|
||||||
AnalyzerFFT := CalcDisplayFFTSize;
|
AnalyzerFFT := CalcDisplayFFTSize;
|
||||||
FDispPos := 0;
|
FDispPos := 0;
|
||||||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
|
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
|
||||||
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||||||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
|
CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
|
||||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||||||
FDisplayPixelCount := DisplayPixels;
|
FDisplayPixelCount := DisplayPixels;
|
||||||
DisplaySampleRate := FSampleRate;
|
|
||||||
|
|
||||||
// Раздельные выходы display: pixout=0 панорама, pixout=1 водопад.
|
|
||||||
SetAnalyzer(DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE, FWindowType, 14.0,
|
|
||||||
Overlap, 0, 0.0, 0.0, DisplayPixels, 1, 0, 0.0, 0.0, MaxW);
|
|
||||||
|
|
||||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WaterfallAvgCount, WaterfallBackmult);
|
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||||
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
|
|
||||||
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
|
|
||||||
SetDisplayNumAverage(DISP_ID, 0, AvgCount);
|
|
||||||
SetDisplayAvBackmult(DISP_ID, 0, Backmult);
|
|
||||||
SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
|
||||||
|
|
||||||
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
|
SetAnalyzer(RX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
||||||
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
|
FWindowType, 14.0, Overlap, 0, 0.0, 0.0, DisplayPixels,
|
||||||
SetDisplayNumAverage(DISP_ID, 1, WaterfallAvgCount);
|
1, 0, 0.0, 0.0, MaxW);
|
||||||
SetDisplayAvBackmult(DISP_ID, 1, WaterfallBackmult);
|
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
|
||||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
SetDisplayAverageMode (RX_DISP_ID, 0, FSpecAvgMode);
|
||||||
|
SetDisplayNumAverage (RX_DISP_ID, 0, AvgCount);
|
||||||
|
SetDisplayAvBackmult (RX_DISP_ID, 0, Backmult);
|
||||||
|
SetDisplayNormOneHz (RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||||
|
SetDisplayDetectorMode(RX_DISP_ID, 1, FWfDetector);
|
||||||
|
SetDisplayAverageMode (RX_DISP_ID, 1, FWfAvgMode);
|
||||||
|
SetDisplayNumAverage (RX_DISP_ID, 1, WfAvgCount);
|
||||||
|
SetDisplayAvBackmult (RX_DISP_ID, 1, WfBackmult);
|
||||||
|
SetDisplayNormOneHz (RX_DISP_ID, 1, 0);
|
||||||
|
SetDisplaySampleRate (RX_DISP_ID, FSampleRate);
|
||||||
|
end;
|
||||||
|
|
||||||
// В Thetis сюда идёт реальный sample rate приёмника.
|
procedure TWDSPEngine.ApplyTXAnalyzerSettings;
|
||||||
SetDisplaySampleRate(DISP_ID, DisplaySampleRate);
|
// Применяет FTXFFTSize / FTXWindowType / FTXSpec* / FTXWf* к TX_DISP_ID.
|
||||||
|
// FFT/Window/Det/Avg отдельные от RX — TX-спектр обычно нужен мельче по FFT
|
||||||
|
// (TX-полоса узкая) и с другим усреднением (быстрее реагировать на голос).
|
||||||
|
var
|
||||||
|
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
|
||||||
|
Backmult, WfBackmult: Double;
|
||||||
|
EffectiveFPS: Double;
|
||||||
|
const KEEP_TIME_SEC = 0.10;
|
||||||
|
begin
|
||||||
|
AnalyzerFFT := NormalizeFFTSize(FTXFFTSize);
|
||||||
|
FTXDispPos := 0;
|
||||||
|
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
|
||||||
|
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
|
||||||
|
CalcAnalyzerTiming(AnalyzerFFT, Overlap, EffectiveFPS);
|
||||||
|
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||||||
|
|
||||||
|
AverageTimeToParams(FTXSpecAvgMode, FTXSpecAvgTimeMS, AvgCount, Backmult);
|
||||||
|
AverageTimeToParams(FTXWfAvgMode, FTXWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||||
|
|
||||||
|
SetAnalyzer(TX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
||||||
|
FTXWindowType, 14.0, Overlap, 0, 0.0, 0.0, DisplayPixels,
|
||||||
|
1, 0, 0.0, 0.0, MaxW);
|
||||||
|
SetDisplayDetectorMode(TX_DISP_ID, 0, FTXSpecDetector);
|
||||||
|
SetDisplayAverageMode (TX_DISP_ID, 0, FTXSpecAvgMode);
|
||||||
|
SetDisplayNumAverage (TX_DISP_ID, 0, AvgCount);
|
||||||
|
SetDisplayAvBackmult (TX_DISP_ID, 0, Backmult);
|
||||||
|
SetDisplayNormOneHz (TX_DISP_ID, 0, UsePanNormOneHz(FTXSpecDetector));
|
||||||
|
SetDisplayDetectorMode(TX_DISP_ID, 1, FTXWfDetector);
|
||||||
|
SetDisplayAverageMode (TX_DISP_ID, 1, FTXWfAvgMode);
|
||||||
|
SetDisplayNumAverage (TX_DISP_ID, 1, WfAvgCount);
|
||||||
|
SetDisplayAvBackmult (TX_DISP_ID, 1, WfBackmult);
|
||||||
|
SetDisplayNormOneHz (TX_DISP_ID, 1, 0);
|
||||||
|
SetDisplaySampleRate (TX_DISP_ID, FSampleRate);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||||
|
// Совместимый вход: применяет настройки и к RX, и к TX analyzer'у.
|
||||||
|
begin
|
||||||
|
ApplyRXAnalyzerSettings;
|
||||||
|
ApplyTXAnalyzerSettings;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.CloseAnalyzer;
|
procedure TWDSPEngine.CloseAnalyzer;
|
||||||
@@ -681,7 +887,8 @@ begin
|
|||||||
FDisplayThread.WaitFor;
|
FDisplayThread.WaitFor;
|
||||||
FreeAndNil(FDisplayThread);
|
FreeAndNil(FDisplayThread);
|
||||||
end;
|
end;
|
||||||
DestroyAnalyzer(DISP_ID);
|
DestroyAnalyzer(RX_DISP_ID);
|
||||||
|
DestroyAnalyzer(TX_DISP_ID);
|
||||||
FAnalyzerOpen := False;
|
FAnalyzerOpen := False;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -761,18 +968,13 @@ begin
|
|||||||
);
|
);
|
||||||
|
|
||||||
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
|
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
|
||||||
SetTXABandpassFreqs(TXA_CHAN, 100, 2800);
|
|
||||||
SetTXABandpassWindow(TXA_CHAN, 1);
|
|
||||||
SetTXALevelerSt(TXA_CHAN, 1);
|
|
||||||
SetTXALevelerTop(TXA_CHAN, 5.0);
|
|
||||||
SetTXAALCSt(TXA_CHAN, 1);
|
|
||||||
SetTXAALCDecay(TXA_CHAN, 10);
|
|
||||||
SetTXACompressorRun(TXA_CHAN, 0);
|
|
||||||
SetTXAPanelGain1(TXA_CHAN, 1.0);
|
|
||||||
SetTXAPanelRun(TXA_CHAN, 1);
|
SetTXAPanelRun(TXA_CHAN, 1);
|
||||||
SetTXAPanelSelect(TXA_CHAN, 1);
|
SetTXAPanelSelect(TXA_CHAN, 1);
|
||||||
|
|
||||||
FInitialized := True;
|
FInitialized := True;
|
||||||
|
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
|
||||||
|
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
||||||
|
ApplyTXChainSettings;
|
||||||
ApplyNoiseFilterState;
|
ApplyNoiseFilterState;
|
||||||
Result := True;
|
Result := True;
|
||||||
|
|
||||||
@@ -904,7 +1106,6 @@ end;
|
|||||||
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
|
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
|
||||||
begin
|
begin
|
||||||
if not FAnalyzerOpen then Exit;
|
if not FAnalyzerOpen then Exit;
|
||||||
if FTXActive then Exit;
|
|
||||||
|
|
||||||
FDispBuf[FDispPos * 2] := I;
|
FDispBuf[FDispPos * 2] := I;
|
||||||
FDispBuf[FDispPos * 2 + 1] := Q;
|
FDispBuf[FDispPos * 2 + 1] := Q;
|
||||||
@@ -912,11 +1113,26 @@ begin
|
|||||||
|
|
||||||
if FDispPos >= DISPLAY_BLOCK_SIZE then
|
if FDispPos >= DISPLAY_BLOCK_SIZE then
|
||||||
begin
|
begin
|
||||||
Spectrum0(1, DISP_ID, 0, 0, @FDispBuf[0]);
|
Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]);
|
||||||
FDispPos := 0;
|
FDispPos := 0;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.FeedTXDisplaySample(const I, Q: Double);
|
||||||
|
begin
|
||||||
|
if not FAnalyzerOpen then Exit;
|
||||||
|
|
||||||
|
FTXDispBuf[FTXDispPos * 2] := I;
|
||||||
|
FTXDispBuf[FTXDispPos * 2 + 1] := Q;
|
||||||
|
Inc(FTXDispPos);
|
||||||
|
|
||||||
|
if FTXDispPos >= DISPLAY_BLOCK_SIZE then
|
||||||
|
begin
|
||||||
|
Spectrum0(1, TX_DISP_ID, 0, 0, @FTXDispBuf[0]);
|
||||||
|
FTXDispPos := 0;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.ProcessRXBlock;
|
procedure TWDSPEngine.ProcessRXBlock;
|
||||||
var
|
var
|
||||||
i: Integer;
|
i: Integer;
|
||||||
@@ -943,8 +1159,10 @@ begin
|
|||||||
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
|
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
|
||||||
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
||||||
|
|
||||||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1)
|
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
||||||
if Assigned(FOnAudio) and not FMuted then
|
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
||||||
|
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
||||||
|
if Assigned(FOnAudio) and not FMuted and not FTXActive then
|
||||||
begin
|
begin
|
||||||
for i := 0 to FAudioBufSize - 1 do
|
for i := 0 to FAudioBufSize - 1 do
|
||||||
begin
|
begin
|
||||||
@@ -1227,6 +1445,7 @@ end;
|
|||||||
|
|
||||||
procedure TWDSPEngine.SetMicGain(GainDB: Double);
|
procedure TWDSPEngine.SetMicGain(GainDB: Double);
|
||||||
begin
|
begin
|
||||||
|
FTXMicGainDB := GainDB;
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
SetTXAPanelGain1(TXA_CHAN, Power(10.0, GainDB / 20.0));
|
SetTXAPanelGain1(TXA_CHAN, Power(10.0, GainDB / 20.0));
|
||||||
end;
|
end;
|
||||||
@@ -1235,6 +1454,11 @@ procedure TWDSPEngine.SetTXRun(Run: Boolean);
|
|||||||
begin
|
begin
|
||||||
FTXActive := Run;
|
FTXActive := Run;
|
||||||
FDispPos := 0;
|
FDispPos := 0;
|
||||||
|
FTXDispPos := 0;
|
||||||
|
if Run then
|
||||||
|
FActiveDisplayID := TX_DISP_ID
|
||||||
|
else
|
||||||
|
FActiveDisplayID := RX_DISP_ID;
|
||||||
|
|
||||||
if Run then
|
if Run then
|
||||||
begin
|
begin
|
||||||
@@ -1299,10 +1523,14 @@ begin
|
|||||||
Err := 0;
|
Err := 0;
|
||||||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||||||
|
|
||||||
// TX сигнал → анализатор: спектр/водопад показывают переданный сигнал
|
// TX сигнал → отдельный TX-analyzer (TX_DISP_ID).
|
||||||
// FTXOut: FBufSize IQ пар @ FSampleRate — тот же формат что и FSpecBuf
|
// Подаём чанками по DISPLAY_BLOCK_SIZE — analyzer ожидает именно такой
|
||||||
|
// размер блока (см. SetAnalyzer). Если подавать FTXOut целиком
|
||||||
|
// (FBufSize пар), analyzer прочитает только первые DISPLAY_BLOCK_SIZE
|
||||||
|
// и проигнорирует остальное → спектр будет пустой.
|
||||||
if FAnalyzerOpen then
|
if FAnalyzerOpen then
|
||||||
Spectrum0(1, DISP_ID, 0, 0, @FTXOut[0]);
|
for i := 0 to FBufSize - 1 do
|
||||||
|
FeedTXDisplaySample(FTXOut[i * 2], FTXOut[i * 2 + 1]);
|
||||||
|
|
||||||
// FTXOut содержит FBufSize IQ пар @ FSampleRate (interleaved double)
|
// FTXOut содержит FBufSize IQ пар @ FSampleRate (interleaved double)
|
||||||
if Assigned(FOnTXIQ) then
|
if Assigned(FOnTXIQ) then
|
||||||
@@ -1335,6 +1563,236 @@ begin
|
|||||||
RTLEventSetEvent(FTXMicSem); // будим TX поток
|
RTLEventSetEvent(FTXMicSem); // будим TX поток
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.PushTXMicSampleD(const S: Double);
|
||||||
|
var newHead: Integer;
|
||||||
|
begin
|
||||||
|
newHead := (FTXMicHead + 1) and (TX_MIC_RING - 1);
|
||||||
|
if newHead <> FTXMicTail then
|
||||||
|
begin
|
||||||
|
FTXMicRing[FTXMicHead] := S;
|
||||||
|
FTXMicHead := newHead;
|
||||||
|
end;
|
||||||
|
// overrun → дроп, как в HW path
|
||||||
|
RTLEventSetEvent(FTXMicSem);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TX chain: применение всех TX-параметров к WDSP-каналу
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
procedure TWDSPEngine.ApplyTXChainSettings;
|
||||||
|
// Вызывается после OpenChannel(TXA) и при старте TX. Прокидывает все
|
||||||
|
// FTX*-поля в WDSP. Идемпотентна, ничего не меняет если канал не открыт.
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
Gains, Freqs, Qs: array[0..10] of Double;
|
||||||
|
begin
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
|
||||||
|
// Bandpass
|
||||||
|
if (FTXFilterLow > 0) and (FTXFilterHigh > FTXFilterLow) then
|
||||||
|
SetTXABandpassFreqs(TXA_CHAN, FTXFilterLow, FTXFilterHigh);
|
||||||
|
if FTXFilterWindow >= 0 then
|
||||||
|
SetTXABandpassWindow(TXA_CHAN, FTXFilterWindow);
|
||||||
|
if FTXFilterNC > 0 then
|
||||||
|
TXASetNC(TXA_CHAN, FTXFilterNC);
|
||||||
|
if FTXFilterMP >= 0 then
|
||||||
|
TXASetMP(TXA_CHAN, FTXFilterMP);
|
||||||
|
|
||||||
|
// Mic gain (panel gain1)
|
||||||
|
SetTXAPanelGain1(TXA_CHAN, Power(10.0, FTXMicGainDB / 20.0));
|
||||||
|
|
||||||
|
// Compressor
|
||||||
|
SetTXACompressorRun(TXA_CHAN, Ord(FTXCompressorOn));
|
||||||
|
SetTXACompressorGain(TXA_CHAN, FTXCompressorGain);
|
||||||
|
|
||||||
|
// Leveler
|
||||||
|
SetTXALevelerSt(TXA_CHAN, Ord(FTXLevelerOn));
|
||||||
|
SetTXALevelerTop(TXA_CHAN, FTXLevelerTop);
|
||||||
|
SetTXALevelerDecay(TXA_CHAN, FTXLevelerDecay);
|
||||||
|
|
||||||
|
// ALC
|
||||||
|
SetTXAALCSt(TXA_CHAN, Ord(FTXALCOn));
|
||||||
|
SetTXAALCMaxGain(TXA_CHAN, FTXALCMaxGain);
|
||||||
|
SetTXAALCDecay(TXA_CHAN, FTXALCDecay);
|
||||||
|
|
||||||
|
// Phase Rotator
|
||||||
|
SetTXAPHROTRun(TXA_CHAN, Ord(FTXPhaseRotOn));
|
||||||
|
SetTXAPHROTNstages(TXA_CHAN, FTXPhaseRotStages);
|
||||||
|
SetTXAPHROTCorner(TXA_CHAN, FTXPhaseRotFreq);
|
||||||
|
|
||||||
|
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
Gains[i] := FTXEQGains[i];
|
||||||
|
Freqs[i] := FTXEQFreqs[i];
|
||||||
|
Qs[i] := 0.707; // умолчание Q
|
||||||
|
end;
|
||||||
|
SetTXAEQProfile(TXA_CHAN, FTXEQNumBands, @Freqs[0], @Gains[0]);
|
||||||
|
SetTXAEQRun(TXA_CHAN, Ord(FTXEQOn));
|
||||||
|
|
||||||
|
// AM
|
||||||
|
SetTXAAMCarrierLevel(TXA_CHAN, FTXAMCarrier);
|
||||||
|
|
||||||
|
// FM
|
||||||
|
SetTXAFMDeviation(TXA_CHAN, FTXFMDeviation);
|
||||||
|
if (FTXFMLowCut < FTXFMHighCut) then
|
||||||
|
SetTXAFMAFFilter(TXA_CHAN, FTXFMLowCut, FTXFMHighCut);
|
||||||
|
SetTXAFMEmphPosition(TXA_CHAN, FTXFMEmphPos);
|
||||||
|
|
||||||
|
// CTCSS
|
||||||
|
SetTXACTCSSFreq(TXA_CHAN, FTXCTCSSFreq);
|
||||||
|
SetTXACTCSSRun(TXA_CHAN, Ord(FTXCTCSSOn));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXFilterFull(Low, High, NC, MP, Window: Integer);
|
||||||
|
begin
|
||||||
|
FTXFilterLow := Low;
|
||||||
|
FTXFilterHigh := High;
|
||||||
|
FTXFilterNC := NC;
|
||||||
|
FTXFilterMP := MP;
|
||||||
|
FTXFilterWindow := Window;
|
||||||
|
FFilterLow := Low;
|
||||||
|
FFilterHigh := High;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXABandpassFreqs(TXA_CHAN, Low, High);
|
||||||
|
SetTXABandpassWindow(TXA_CHAN, Window);
|
||||||
|
if NC > 0 then TXASetNC(TXA_CHAN, NC);
|
||||||
|
if MP >= 0 then TXASetMP(TXA_CHAN, MP);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXCompressor(Enable: Boolean; GainDB: Double);
|
||||||
|
begin
|
||||||
|
FTXCompressorOn := Enable;
|
||||||
|
FTXCompressorGain := GainDB;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXACompressorGain(TXA_CHAN, GainDB);
|
||||||
|
SetTXACompressorRun(TXA_CHAN, Ord(Enable));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXLeveler(Enable: Boolean; TopDB: Double; DecayMS: Integer);
|
||||||
|
begin
|
||||||
|
FTXLevelerOn := Enable;
|
||||||
|
FTXLevelerTop := TopDB;
|
||||||
|
FTXLevelerDecay := DecayMS;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXALevelerTop(TXA_CHAN, TopDB);
|
||||||
|
SetTXALevelerDecay(TXA_CHAN, DecayMS);
|
||||||
|
SetTXALevelerSt(TXA_CHAN, Ord(Enable));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXALC(Enable: Boolean; MaxGainDB: Double; DecayMS: Integer);
|
||||||
|
begin
|
||||||
|
FTXALCOn := Enable;
|
||||||
|
FTXALCMaxGain := MaxGainDB;
|
||||||
|
FTXALCDecay := DecayMS;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXAALCMaxGain(TXA_CHAN, MaxGainDB);
|
||||||
|
SetTXAALCDecay(TXA_CHAN, DecayMS);
|
||||||
|
SetTXAALCSt(TXA_CHAN, Ord(Enable));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXPhaseRot(Enable: Boolean; Stages: Integer; FreqHz: Double);
|
||||||
|
begin
|
||||||
|
FTXPhaseRotOn := Enable;
|
||||||
|
FTXPhaseRotStages := Stages;
|
||||||
|
FTXPhaseRotFreq := FreqHz;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXAPHROTNstages(TXA_CHAN, Stages);
|
||||||
|
SetTXAPHROTCorner(TXA_CHAN, FreqHz);
|
||||||
|
SetTXAPHROTRun(TXA_CHAN, Ord(Enable));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXEQ(Enable: Boolean; NumBands: Integer;
|
||||||
|
const Gains, Freqs: array of Double);
|
||||||
|
var
|
||||||
|
i, n: Integer;
|
||||||
|
G, F: array[0..10] of Double;
|
||||||
|
begin
|
||||||
|
FTXEQOn := Enable;
|
||||||
|
FTXEQNumBands := NumBands;
|
||||||
|
n := Min(11, Min(Length(Gains), Length(Freqs)));
|
||||||
|
for i := 0 to n - 1 do
|
||||||
|
begin
|
||||||
|
FTXEQGains[i] := Gains[i];
|
||||||
|
FTXEQFreqs[i] := Freqs[i];
|
||||||
|
G[i] := Gains[i];
|
||||||
|
F[i] := Freqs[i];
|
||||||
|
end;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXAEQProfile(TXA_CHAN, NumBands, @F[0], @G[0]);
|
||||||
|
SetTXAEQRun(TXA_CHAN, Ord(Enable));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXAMCarrierLevel(Level: Double);
|
||||||
|
begin
|
||||||
|
FTXAMCarrier := Level;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
WDSP.SetTXAAMCarrierLevel(TXA_CHAN, Level);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXFMParams(Deviation: Double; LowCut, HighCut, EmphPos: Integer);
|
||||||
|
begin
|
||||||
|
FTXFMDeviation := Deviation;
|
||||||
|
FTXFMLowCut := LowCut;
|
||||||
|
FTXFMHighCut := HighCut;
|
||||||
|
FTXFMEmphPos := EmphPos;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXAFMDeviation(TXA_CHAN, Deviation);
|
||||||
|
if LowCut < HighCut then
|
||||||
|
SetTXAFMAFFilter(TXA_CHAN, LowCut, HighCut);
|
||||||
|
SetTXAFMEmphPosition(TXA_CHAN, EmphPos);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXCTCSS(Enable: Boolean; FreqHz: Double);
|
||||||
|
begin
|
||||||
|
FTXCTCSSOn := Enable;
|
||||||
|
FTXCTCSSFreq := FreqHz;
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
SetTXACTCSSFreq(TXA_CHAN, FreqHz);
|
||||||
|
SetTXACTCSSRun(TXA_CHAN, Ord(Enable));
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TWDSPEngine.TXMicRingAvail: Integer;
|
||||||
|
begin
|
||||||
|
Result := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXMicSource(Source: TTXMicSource);
|
||||||
|
begin
|
||||||
|
if FTXMicSource = Source then Exit;
|
||||||
|
FTXMicSource := Source;
|
||||||
|
// При смене источника сбрасываем ring, чтобы не подмешивать остатки
|
||||||
|
// от предыдущего producer'а (race window игнорируется — TX обычно неактивен).
|
||||||
|
FTXMicHead := 0;
|
||||||
|
FTXMicTail := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXFFTParams(FFTSize, WinType: Integer);
|
||||||
|
begin
|
||||||
|
FTXFFTSize := NormalizeFFTSize(FFTSize);
|
||||||
|
FTXWindowType := EnsureRange(WinType, 0, 6);
|
||||||
|
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXSpectrumDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||||
|
begin
|
||||||
|
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, False);
|
||||||
|
FTXSpecDetector := Detector;
|
||||||
|
FTXSpecAvgMode := AvgMode;
|
||||||
|
FTXSpecAvgTimeMS := AvgTimeMS;
|
||||||
|
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetTXWaterfallDisplay(Detector, AvgMode: Integer; AvgTimeMS: Double);
|
||||||
|
begin
|
||||||
|
NormalizeDisplayParams(Detector, AvgMode, AvgTimeMS, True);
|
||||||
|
FTXWfDetector := Detector;
|
||||||
|
FTXWfAvgMode := AvgMode;
|
||||||
|
FTXWfAvgTimeMS := AvgTimeMS;
|
||||||
|
if FAnalyzerOpen then ApplyTXAnalyzerSettings;
|
||||||
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Spectrum
|
// Spectrum
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1350,12 +1808,14 @@ var
|
|||||||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||||
Flag: Integer;
|
Flag: Integer;
|
||||||
i, N: Integer;
|
i, N: Integer;
|
||||||
|
DispID: Integer;
|
||||||
begin
|
begin
|
||||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||||||
|
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
|
||||||
|
|
||||||
Flag := 0;
|
Flag := 0;
|
||||||
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
|
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||||||
if Flag <> 0 then
|
if Flag <> 0 then
|
||||||
begin
|
begin
|
||||||
for i := 0 to N - 1 do
|
for i := 0 to N - 1 do
|
||||||
@@ -1365,7 +1825,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
Flag := 0;
|
Flag := 0;
|
||||||
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
|
GetPixels(DispID, 1, @PixBuf[0], @Flag);
|
||||||
if Flag <> 0 then
|
if Flag <> 0 then
|
||||||
begin
|
begin
|
||||||
for i := 0 to N - 1 do
|
for i := 0 to N - 1 do
|
||||||
@@ -1512,11 +1972,11 @@ begin
|
|||||||
FSpecAvgTimeMS := AvgTimeMS;
|
FSpecAvgTimeMS := AvgTimeMS;
|
||||||
if not FAnalyzerOpen then Exit;
|
if not FAnalyzerOpen then Exit;
|
||||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||||
SetDisplayDetectorMode(DISP_ID, 0, FSpecDetector);
|
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
|
||||||
SetDisplayAverageMode(DISP_ID, 0, FSpecAvgMode);
|
SetDisplayAverageMode(RX_DISP_ID, 0, FSpecAvgMode);
|
||||||
SetDisplayNumAverage(DISP_ID, 0, AvgCount);
|
SetDisplayNumAverage(RX_DISP_ID, 0, AvgCount);
|
||||||
SetDisplayAvBackmult(DISP_ID, 0, Backmult);
|
SetDisplayAvBackmult(RX_DISP_ID, 0, Backmult);
|
||||||
SetDisplayNormOneHz(DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
SetDisplayNormOneHz(RX_DISP_ID, 0, UsePanNormOneHz(FSpecDetector));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
|
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
|
||||||
@@ -1536,11 +1996,11 @@ begin
|
|||||||
FWfAvgTimeMS := AvgTimeMS;
|
FWfAvgTimeMS := AvgTimeMS;
|
||||||
if not FAnalyzerOpen then Exit;
|
if not FAnalyzerOpen then Exit;
|
||||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
|
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
|
||||||
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
|
SetDisplayDetectorMode(RX_DISP_ID, 1, FWfDetector);
|
||||||
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
|
SetDisplayAverageMode(RX_DISP_ID, 1, FWfAvgMode);
|
||||||
SetDisplayNumAverage(DISP_ID, 1, AvgCount);
|
SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount);
|
||||||
SetDisplayAvBackmult(DISP_ID, 1, Backmult);
|
SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult);
|
||||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
SetDisplayNormOneHz(RX_DISP_ID, 1, 0);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
Reference in New Issue
Block a user