mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +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 если буфер пуст.
|
||||
function ReadSample: Double;
|
||||
// Сколько сэмплов реально лежит в ring буфере (0..MY_MIC_RING_SIZE-1).
|
||||
// Использовать для чтения только доступных данных без пуш-тишины.
|
||||
function Available: Integer;
|
||||
|
||||
property IsOpen: Boolean read FOpen;
|
||||
property LastError: string read GetLastError;
|
||||
@@ -534,4 +537,13 @@ begin
|
||||
FOutPt := newpt;
|
||||
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.
|
||||
|
||||
+178
-18
@@ -158,6 +158,7 @@ type
|
||||
FAudioIn: TAudioInput;
|
||||
FAudioOutDevName: string; // текущее имя выходного устройства
|
||||
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
|
||||
FTXSettings: TTXSettings; // полный TX-конфиг (загружается per-MAC)
|
||||
FWDSPReady: Boolean; // True когда WDSP открыт и работает
|
||||
|
||||
// ---- State ----
|
||||
@@ -536,6 +537,7 @@ type
|
||||
procedure ApplyMOX(Active: Boolean);
|
||||
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
|
||||
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
|
||||
procedure OnTXSettingsChange(const T: TTXSettings);
|
||||
procedure BtnAGCModeClick(Sender: TObject);
|
||||
procedure TrkAGCChange(Sender: TObject);
|
||||
procedure MeterTimerTick(Sender: TObject);
|
||||
@@ -559,6 +561,12 @@ type
|
||||
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
|
||||
procedure ApplyAudioDevice(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 ApplyFPS(FPS: Integer);
|
||||
|
||||
@@ -942,6 +950,8 @@ begin
|
||||
begin
|
||||
SaveCurrentBand;
|
||||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||||
if FDevConnected then
|
||||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||||
FSettings.Save;
|
||||
end;
|
||||
end;
|
||||
@@ -1105,6 +1115,8 @@ begin
|
||||
FNetwork.OnHPStatus := OnHPStatusCB;
|
||||
FNetwork.OnDDCIQ := OnDDCIQCB;
|
||||
FNetwork.OnMicPacket := OnMicPacketCB;
|
||||
// Дефолты TX (на случай если устройство ещё не выбрано).
|
||||
TSettingsManager.DefaultTX(FTXSettings);
|
||||
|
||||
BuildUI;
|
||||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||||
@@ -1129,6 +1141,9 @@ begin
|
||||
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
||||
FDSPEngine.OnWaterfall := OnWaterfallReady;
|
||||
FDSPEngine.OnTXIQ := OnTXIQReady;
|
||||
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
|
||||
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
|
||||
FDSPEngine.OnPullMicSamples := PullSoundCardMic;
|
||||
|
||||
// Audio output/input — создаём объекты сейчас, открываем после показа формы
|
||||
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
|
||||
@@ -1182,6 +1197,8 @@ begin
|
||||
begin
|
||||
SaveCurrentBand;
|
||||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||||
if FDevConnected then
|
||||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||||
end;
|
||||
// Размер окна сохраняем всегда (не зависит от подключения)
|
||||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||||
@@ -1298,6 +1315,7 @@ begin
|
||||
Add; Items[2].Width := 320; Items[2].Text := 'Supply: -- | FWD: -- | SWR: --';
|
||||
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
|
||||
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
|
||||
Add; Items[5].Width := 280; Items[5].Text := 'TX: --';
|
||||
end;
|
||||
|
||||
// ---- Left panel ----
|
||||
@@ -2520,6 +2538,24 @@ begin
|
||||
FSpecView.LastSWR := FLastSWR;
|
||||
|
||||
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;
|
||||
|
||||
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||||
@@ -2807,10 +2843,101 @@ end;
|
||||
|
||||
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
|
||||
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);
|
||||
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
|
||||
// ===========================================================================
|
||||
@@ -3015,7 +3142,6 @@ var
|
||||
Idx, i: Integer;
|
||||
Dev: THPSDRDevice;
|
||||
GenPkt: TGeneralPacket;
|
||||
DUCPkt: TDUCSpecificPacket;
|
||||
G_Settings: TGlobalSettings;
|
||||
PreG: TGlobalSettings;
|
||||
PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
||||
@@ -3144,7 +3270,6 @@ end;
|
||||
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
|
||||
var
|
||||
GenPkt: TGeneralPacket;
|
||||
DUCPkt: TDUCSpecificPacket;
|
||||
G_Settings: TGlobalSettings;
|
||||
i: Integer;
|
||||
begin
|
||||
@@ -3163,6 +3288,8 @@ begin
|
||||
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
|
||||
FCATLastGlobal := G_Settings;
|
||||
CATApplySettings(G_Settings);
|
||||
// TX-настройки per-device — отдельная JSON-секция "tx"
|
||||
FSettings.LoadTX(FDevMAC, FTXSettings);
|
||||
FVolume := G_Settings.Volume;
|
||||
FActiveVfo := G_Settings.ActiveVfo;
|
||||
// PA settings
|
||||
@@ -3231,12 +3358,15 @@ begin
|
||||
FAudioOutDevName := '';
|
||||
end;
|
||||
FAudioOut.Open;
|
||||
// Аудио устройство ввода (TX mic)
|
||||
// Аудио устройство ввода (TX mic). Если в настройках выбрана конкретная
|
||||
// soundcard — открываем её и принудительно переключаем MicSource на SoundCard,
|
||||
// иначе пользователь нажмёт MOX и не поймёт почему его микрофон молчит.
|
||||
if G_Settings.AudioInDevice <> '' then
|
||||
begin
|
||||
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
|
||||
FAudioInDevName := G_Settings.AudioInDevice;
|
||||
FAudioIn.Open;
|
||||
FTXSettings.MicSource := 1; // SoundCard
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -3286,18 +3416,6 @@ begin
|
||||
end;
|
||||
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
|
||||
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
|
||||
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
|
||||
@@ -3307,9 +3425,12 @@ begin
|
||||
FRXStartTime := GetTickCount64;
|
||||
FRXLastPktTime := 0;
|
||||
|
||||
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах
|
||||
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
|
||||
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
|
||||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0);
|
||||
FNetwork.SendDUCSpecific(DUCPkt);
|
||||
SendDUCSpecificFromSettings;
|
||||
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||||
ApplyTXSettingsToDSP;
|
||||
|
||||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||||
if FWDSPReady then
|
||||
@@ -4858,6 +4979,26 @@ begin
|
||||
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True);
|
||||
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);
|
||||
begin
|
||||
FVolume := TrkVolume.Position;
|
||||
@@ -5063,9 +5204,26 @@ begin
|
||||
on E: Exception do
|
||||
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
|
||||
end;
|
||||
// Автопереключение mic-источника: явный выбор PortAudio-устройства
|
||||
// → берём mic со звуковой карты, иначе HW mic-стрим бы перебивал его.
|
||||
FTXSettings.MicSource := 1; // SoundCard
|
||||
end
|
||||
else
|
||||
begin
|
||||
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;
|
||||
|
||||
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
|
||||
@@ -5108,8 +5266,10 @@ begin
|
||||
SF.OnPAChange := OnPASettingsChange;
|
||||
SF.OnCATChange := OnCATSettingsChange;
|
||||
SF.OnThemeChange := SetLightTheme;
|
||||
SF.OnTXChange := OnTXSettingsChange;
|
||||
end;
|
||||
SF := TSettingsForm(FSettingsForm);
|
||||
SF.LoadTXSettings(FTXSettings);
|
||||
SF.LoadPASettings(FPAMaxPower, FPABandCal);
|
||||
SF.LoadCATSettings(
|
||||
FCATLastGlobal.CATSerialEnabled,
|
||||
|
||||
+249
@@ -30,6 +30,67 @@ type
|
||||
SpanHz: Double;
|
||||
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
|
||||
Volume: Integer;
|
||||
DriveLevel: Integer;
|
||||
@@ -119,9 +180,15 @@ type
|
||||
const B: TBandSettings);
|
||||
function LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||||
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 procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
||||
class procedure DefaultGlobal(out G: TGlobalSettings);
|
||||
class procedure DefaultTX(out T: TTXSettings);
|
||||
// Размер окна — не привязан к MAC, хранится в корне JSON
|
||||
procedure SaveWindowBounds(L, T, W, H: Integer);
|
||||
procedure LoadWindowBounds(out L, T, W, H: Integer);
|
||||
@@ -628,6 +695,188 @@ begin
|
||||
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);
|
||||
var O: TJSONObject;
|
||||
begin
|
||||
|
||||
+483
-4
@@ -27,12 +27,14 @@ uses
|
||||
Classes, SysUtils, Math,
|
||||
Forms, Controls, Graphics, Dialogs,
|
||||
StdCtrls, ExtCtrls, Spin,
|
||||
FlatButton, AudioOutput, AudioInput, AppTheme;
|
||||
FlatButton, AudioOutput, AudioInput, AppTheme, Settings;
|
||||
|
||||
type
|
||||
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
|
||||
TOnPASettingsChange = procedure(MaxPower: Double;
|
||||
const BandCal: array of Double) of object;
|
||||
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
|
||||
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
|
||||
|
||||
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
|
||||
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
|
||||
@@ -66,6 +68,7 @@ type
|
||||
FPagePA: TScrollBox;
|
||||
FPageAdvanced: TScrollBox;
|
||||
FPageCAT: TScrollBox;
|
||||
FPageTransmit: TScrollBox;
|
||||
FNavAudio: TFlatButton;
|
||||
FNavDisplay: TFlatButton;
|
||||
FNavSpectrum: TFlatButton;
|
||||
@@ -73,6 +76,7 @@ type
|
||||
FNavPA: TFlatButton;
|
||||
FNavAdvanced: TFlatButton;
|
||||
FNavCAT: TFlatButton;
|
||||
FNavTransmit: TFlatButton;
|
||||
|
||||
// ---- Audio tab controls ----
|
||||
FCmbRXDev: TComboBox;
|
||||
@@ -108,6 +112,62 @@ type
|
||||
FEdMaxPower: TSpinEdit;
|
||||
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 ----
|
||||
FCATSerialEn: array[0..3] of TCheckBox;
|
||||
FCATSerialPort: array[0..3] of TEdit;
|
||||
@@ -132,6 +192,7 @@ type
|
||||
FOnVisibilityChange: TOnVisibilityChange;
|
||||
FOnFPSChange: TOnFPSChange;
|
||||
FOnThemeChange: TOnThemeChange;
|
||||
FOnTXChange: TOnTXSettingsChange;
|
||||
|
||||
FLoading: Boolean;
|
||||
|
||||
@@ -141,6 +202,10 @@ type
|
||||
procedure BuildRX1Tab;
|
||||
procedure BuildPATab;
|
||||
procedure BuildCATTab;
|
||||
procedure BuildTransmitTab;
|
||||
procedure CollectTXFromUI;
|
||||
procedure FireTXChange;
|
||||
procedure OnTXAnyChange(Sender: TObject);
|
||||
procedure ApplyTheme(const T: TAppTheme);
|
||||
procedure NavClick(Sender: TObject);
|
||||
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
|
||||
@@ -214,6 +279,7 @@ type
|
||||
const SerPort: array of string;
|
||||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||||
TcpEnabled: Boolean; TcpPort: Integer);
|
||||
procedure LoadTXSettings(const T: TTXSettings);
|
||||
|
||||
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
|
||||
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
|
||||
@@ -225,6 +291,7 @@ type
|
||||
property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange;
|
||||
property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange;
|
||||
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
|
||||
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
|
||||
end;
|
||||
|
||||
implementation
|
||||
@@ -429,9 +496,10 @@ begin
|
||||
FNavDisplay := MakeNavButton('Display', 110);
|
||||
FNavSpectrum := MakeNavButton('Spectrum', 146);
|
||||
FNavWaterfall := MakeNavButton('Waterfall', 182);
|
||||
FNavPA := MakeNavButton('Power Amplifier', 218);
|
||||
FNavAdvanced := MakeNavButton('Advanced', 254);
|
||||
FNavCAT := MakeNavButton('CAT', 290);
|
||||
FNavTransmit := MakeNavButton('Transmit', 218);
|
||||
FNavPA := MakeNavButton('Power Amplifier', 254);
|
||||
FNavAdvanced := MakeNavButton('Advanced', 290);
|
||||
FNavCAT := MakeNavButton('CAT', 326);
|
||||
|
||||
FContentPanel := TPanel.Create(Self);
|
||||
FContentPanel.Parent := Self;
|
||||
@@ -443,6 +511,7 @@ begin
|
||||
FPageDisplay := MakeScrollPage;
|
||||
FPageSpectrum := MakeScrollPage;
|
||||
FPageWaterfall := MakeScrollPage;
|
||||
FPageTransmit := MakeScrollPage;
|
||||
FPagePA := MakeScrollPage;
|
||||
FPageAdvanced := MakeScrollPage;
|
||||
FPageCAT := MakeScrollPage;
|
||||
@@ -450,6 +519,7 @@ begin
|
||||
BuildAudioTab;
|
||||
BuildDisplayTab;
|
||||
BuildRX1Tab;
|
||||
BuildTransmitTab;
|
||||
BuildPATab;
|
||||
BuildCATTab;
|
||||
|
||||
@@ -477,6 +547,7 @@ begin
|
||||
else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay)
|
||||
else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum)
|
||||
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 = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
|
||||
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT);
|
||||
@@ -488,6 +559,7 @@ begin
|
||||
FPageDisplay.Visible := APage = FPageDisplay;
|
||||
FPageSpectrum.Visible := APage = FPageSpectrum;
|
||||
FPageWaterfall.Visible := APage = FPageWaterfall;
|
||||
FPageTransmit.Visible := APage = FPageTransmit;
|
||||
FPagePA.Visible := APage = FPagePA;
|
||||
FPageAdvanced.Visible := APage = FPageAdvanced;
|
||||
FPageCAT.Visible := APage = FPageCAT;
|
||||
@@ -496,6 +568,7 @@ begin
|
||||
FNavDisplay.Active := ANav = FNavDisplay;
|
||||
FNavSpectrum.Active := ANav = FNavSpectrum;
|
||||
FNavWaterfall.Active := ANav = FNavWaterfall;
|
||||
FNavTransmit.Active := ANav = FNavTransmit;
|
||||
FNavPA.Active := ANav = FNavPA;
|
||||
FNavAdvanced.Active := ANav = FNavAdvanced;
|
||||
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);
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+6
-5
@@ -274,19 +274,20 @@ begin
|
||||
end;
|
||||
|
||||
// Цветовая схема водопада для светлой темы.
|
||||
// Шум исчезает в фоне (кремовый), сигналы: зелень → бирюза → синева → индиго.
|
||||
// Шум растворяется в фоне панели; сигналы: синий → cyan → зелёный → жёлтый → оранжевый → красный.
|
||||
function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord;
|
||||
const
|
||||
NSTOPS = 8;
|
||||
SR: array[0..NSTOPS-1] of Integer = (240, 210, 148, 60, 14, 8, 24, 64);
|
||||
SG: array[0..NSTOPS-1] of Integer = (238, 228, 200, 164, 132, 96, 52, 6);
|
||||
SB: array[0..NSTOPS-1] of Integer = (230, 190, 118, 68, 96, 148, 166, 100);
|
||||
// panel steel blue cyan green yell orange red
|
||||
SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255);
|
||||
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
|
||||
T: Double;
|
||||
Seg: Integer;
|
||||
R, G, B: Integer;
|
||||
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;
|
||||
T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1);
|
||||
Seg := Min(NSTOPS - 2, Trunc(T));
|
||||
|
||||
+527
-67
@@ -95,7 +95,8 @@ const
|
||||
RXA_CHAN = 0;
|
||||
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;
|
||||
SPECTRUM_PIXELS = 1024;
|
||||
DISPLAY_BLOCK_SIZE = 64;
|
||||
@@ -127,6 +128,13 @@ type
|
||||
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
||||
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
||||
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 = class
|
||||
@@ -154,6 +162,10 @@ type
|
||||
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
|
||||
FDispBuf: array of Double;
|
||||
FDispPos: Integer;
|
||||
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
|
||||
// по DISPLAY_BLOCK_SIZE из FTXOut в ProcessTXBlock.
|
||||
FTXDispBuf: array of Double;
|
||||
FTXDispPos: Integer;
|
||||
// Аудио выходные буферы — аллоцируем один раз
|
||||
FOutL: array of Single;
|
||||
FOutR: array of Single;
|
||||
@@ -196,6 +208,50 @@ type
|
||||
FTXMicSem: PRTLEvent; // сигнал: есть новые mic сэмплы
|
||||
FTXThread: TThread;
|
||||
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;
|
||||
FSMeter: Double;
|
||||
@@ -239,6 +295,10 @@ type
|
||||
procedure ApplyANFState;
|
||||
procedure ApplyNoiseFilterState;
|
||||
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
|
||||
constructor Create(SampleRate: Integer = 192000;
|
||||
@@ -283,9 +343,33 @@ type
|
||||
procedure SetMicGain(GainDB: Double);
|
||||
procedure SetTXRun(Run: Boolean);
|
||||
|
||||
// TX mic вход — вызывается из receive thread (thread-safe lock-free)
|
||||
// Src: 16-bit big-endian hardware mic samples, N — кол-во сэмплов
|
||||
// Расширенное TX управление (применяются мгновенно если канал открыт)
|
||||
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);
|
||||
// Прямой push double-сэмпла (-1..+1), для sound-card пути.
|
||||
// Должен вызываться из ОДНОГО потока (PortAudio callback).
|
||||
procedure PushTXMicSampleD(const S: Double);
|
||||
|
||||
// Настройки анализатора (применяются немедленно если открыт)
|
||||
procedure SetFFTParams(FFTSize, WinType: Integer);
|
||||
@@ -329,6 +413,24 @@ type
|
||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||
property OnTXIQ: TOnTXIQReady read FOnTXIQ write FOnTXIQ;
|
||||
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;
|
||||
|
||||
implementation
|
||||
@@ -426,6 +528,13 @@ begin
|
||||
if Terminated then Break;
|
||||
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-данными
|
||||
repeat
|
||||
Avail := (FEngine.FTXMicHead - FEngine.FTXMicTail + TX_MIC_RING)
|
||||
@@ -571,7 +680,9 @@ begin
|
||||
SetLength(FRXAccQ, FBufSize);
|
||||
SetLength(FSpecBuf, FBufSize * 2);
|
||||
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
SetLength(FTXDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
FDispPos := 0;
|
||||
FTXDispPos := 0;
|
||||
SetLength(FOutL, FAudioBufSize);
|
||||
SetLength(FOutR, FAudioBufSize);
|
||||
|
||||
@@ -587,9 +698,65 @@ begin
|
||||
FTXMicHead := 0;
|
||||
FTXMicTail := 0;
|
||||
FTXActive := False;
|
||||
FActiveDisplayID := RX_DISP_ID;
|
||||
FTXMicSource := txmsRadio;
|
||||
FOnPullMic := nil;
|
||||
FTXThread := nil;
|
||||
FTXMicSem := RTLEventCreate;
|
||||
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;
|
||||
|
||||
destructor TWDSPEngine.Destroy;
|
||||
@@ -612,8 +779,19 @@ begin
|
||||
if FAnalyzerOpen then Exit;
|
||||
|
||||
Success := -1;
|
||||
XCreateAnalyzer(DISP_ID, @Success, 262144, 1, 1, nil);
|
||||
if Success <> 0 then Exit;
|
||||
XCreateAnalyzer(RX_DISP_ID, @Success, 262144, 1, 1, nil);
|
||||
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;
|
||||
ApplyAnalyzerSettings;
|
||||
@@ -623,24 +801,13 @@ begin
|
||||
FDisplayThread := TDisplayThread.Create(Self);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyAnalyzerSettings;
|
||||
// Вызывается из OpenAnalyzer и при изменении FFT/window/detector/avg параметров
|
||||
procedure TWDSPEngine.ApplyRXAnalyzerSettings;
|
||||
// Применяет FFFTSize / FWindowType / FSpec* / FWf* к RX_DISP_ID.
|
||||
var
|
||||
AnalyzerFFT: Integer;
|
||||
AvgCount: Integer;
|
||||
Backmult: Double;
|
||||
WaterfallAvgCount: Integer;
|
||||
WaterfallBackmult: Double;
|
||||
MaxW: Integer;
|
||||
Overlap: Integer;
|
||||
const
|
||||
KEEP_TIME_SEC = 0.10;
|
||||
var
|
||||
DisplaySampleRate: Integer;
|
||||
DisplayPixels: Integer;
|
||||
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
|
||||
Backmult, WfBackmult: Double;
|
||||
const KEEP_TIME_SEC = 0.10;
|
||||
begin
|
||||
// FFT Size пользователя управляет именно display-analyzer, как в Thetis,
|
||||
// но визуальная нормализация должна идти к ширине видимого окна, как в piHPSDR.
|
||||
AnalyzerFFT := CalcDisplayFFTSize;
|
||||
FDispPos := 0;
|
||||
MaxW := AnalyzerFFT + Round(Min(KEEP_TIME_SEC * FSampleRate,
|
||||
@@ -648,28 +815,67 @@ begin
|
||||
CalcAnalyzerTiming(AnalyzerFFT, Overlap, FEffectiveDisplayFPS);
|
||||
DisplayPixels := EnsureRange(FLastSpectrumW, 64, SPECTRUM_PIXELS);
|
||||
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(FWfAvgMode, FWfAvgTimeMS, WaterfallAvgCount, WaterfallBackmult);
|
||||
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));
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||
|
||||
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage(DISP_ID, 1, WaterfallAvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, WaterfallBackmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
||||
SetAnalyzer(RX_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);
|
||||
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
|
||||
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 приёмника.
|
||||
SetDisplaySampleRate(DISP_ID, DisplaySampleRate);
|
||||
procedure TWDSPEngine.ApplyTXAnalyzerSettings;
|
||||
// Применяет 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;
|
||||
|
||||
procedure TWDSPEngine.CloseAnalyzer;
|
||||
@@ -681,7 +887,8 @@ begin
|
||||
FDisplayThread.WaitFor;
|
||||
FreeAndNil(FDisplayThread);
|
||||
end;
|
||||
DestroyAnalyzer(DISP_ID);
|
||||
DestroyAnalyzer(RX_DISP_ID);
|
||||
DestroyAnalyzer(TX_DISP_ID);
|
||||
FAnalyzerOpen := False;
|
||||
end;
|
||||
|
||||
@@ -761,18 +968,13 @@ begin
|
||||
);
|
||||
|
||||
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);
|
||||
SetTXAPanelSelect(TXA_CHAN, 1);
|
||||
|
||||
FInitialized := True;
|
||||
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
|
||||
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
||||
ApplyTXChainSettings;
|
||||
ApplyNoiseFilterState;
|
||||
Result := True;
|
||||
|
||||
@@ -904,7 +1106,6 @@ end;
|
||||
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
|
||||
begin
|
||||
if not FAnalyzerOpen then Exit;
|
||||
if FTXActive then Exit;
|
||||
|
||||
FDispBuf[FDispPos * 2] := I;
|
||||
FDispBuf[FDispPos * 2 + 1] := Q;
|
||||
@@ -912,11 +1113,26 @@ begin
|
||||
|
||||
if FDispPos >= DISPLAY_BLOCK_SIZE then
|
||||
begin
|
||||
Spectrum0(1, DISP_ID, 0, 0, @FDispBuf[0]);
|
||||
Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]);
|
||||
FDispPos := 0;
|
||||
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;
|
||||
var
|
||||
i: Integer;
|
||||
@@ -943,8 +1159,10 @@ begin
|
||||
// вызов вернёт 0/-∞ — отсюда "падение до нуля". Только здесь!
|
||||
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
||||
|
||||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1)
|
||||
if Assigned(FOnAudio) and not FMuted then
|
||||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
||||
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
||||
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
||||
if Assigned(FOnAudio) and not FMuted and not FTXActive then
|
||||
begin
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
@@ -1227,6 +1445,7 @@ end;
|
||||
|
||||
procedure TWDSPEngine.SetMicGain(GainDB: Double);
|
||||
begin
|
||||
FTXMicGainDB := GainDB;
|
||||
if not FInitialized then Exit;
|
||||
SetTXAPanelGain1(TXA_CHAN, Power(10.0, GainDB / 20.0));
|
||||
end;
|
||||
@@ -1235,6 +1454,11 @@ procedure TWDSPEngine.SetTXRun(Run: Boolean);
|
||||
begin
|
||||
FTXActive := Run;
|
||||
FDispPos := 0;
|
||||
FTXDispPos := 0;
|
||||
if Run then
|
||||
FActiveDisplayID := TX_DISP_ID
|
||||
else
|
||||
FActiveDisplayID := RX_DISP_ID;
|
||||
|
||||
if Run then
|
||||
begin
|
||||
@@ -1299,10 +1523,14 @@ begin
|
||||
Err := 0;
|
||||
fexchange0(TXA_CHAN, @FTXIn[0], @FTXOut[0], @Err);
|
||||
|
||||
// TX сигнал → анализатор: спектр/водопад показывают переданный сигнал
|
||||
// FTXOut: FBufSize IQ пар @ FSampleRate — тот же формат что и FSpecBuf
|
||||
// TX сигнал → отдельный TX-analyzer (TX_DISP_ID).
|
||||
// Подаём чанками по DISPLAY_BLOCK_SIZE — analyzer ожидает именно такой
|
||||
// размер блока (см. SetAnalyzer). Если подавать FTXOut целиком
|
||||
// (FBufSize пар), analyzer прочитает только первые DISPLAY_BLOCK_SIZE
|
||||
// и проигнорирует остальное → спектр будет пустой.
|
||||
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)
|
||||
if Assigned(FOnTXIQ) then
|
||||
@@ -1335,6 +1563,236 @@ begin
|
||||
RTLEventSetEvent(FTXMicSem); // будим TX поток
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1350,12 +1808,14 @@ var
|
||||
PixBuf: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
Flag: Integer;
|
||||
i, N: Integer;
|
||||
DispID: Integer;
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||||
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
|
||||
|
||||
Flag := 0;
|
||||
GetPixels(DISP_ID, 0, @PixBuf[0], @Flag);
|
||||
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
@@ -1365,7 +1825,7 @@ begin
|
||||
end;
|
||||
|
||||
Flag := 0;
|
||||
GetPixels(DISP_ID, 1, @PixBuf[0], @Flag);
|
||||
GetPixels(DispID, 1, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
@@ -1512,11 +1972,11 @@ begin
|
||||
FSpecAvgTimeMS := AvgTimeMS;
|
||||
if not FAnalyzerOpen then Exit;
|
||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||
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(RX_DISP_ID, 0, FSpecDetector);
|
||||
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));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetWaterfallDisplay(Detector, AvgMode: Integer;
|
||||
@@ -1536,11 +1996,11 @@ begin
|
||||
FWfAvgTimeMS := AvgTimeMS;
|
||||
if not FAnalyzerOpen then Exit;
|
||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, AvgCount, Backmult);
|
||||
SetDisplayDetectorMode(DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode(DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage(DISP_ID, 1, AvgCount);
|
||||
SetDisplayAvBackmult(DISP_ID, 1, Backmult);
|
||||
SetDisplayNormOneHz(DISP_ID, 1, 0);
|
||||
SetDisplayDetectorMode(RX_DISP_ID, 1, FWfDetector);
|
||||
SetDisplayAverageMode(RX_DISP_ID, 1, FWfAvgMode);
|
||||
SetDisplayNumAverage(RX_DISP_ID, 1, AvgCount);
|
||||
SetDisplayAvBackmult(RX_DISP_ID, 1, Backmult);
|
||||
SetDisplayNormOneHz(RX_DISP_ID, 1, 0);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
Reference in New Issue
Block a user