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:
2026-05-05 08:06:58 +03:00
co-authored by Claude Opus 4.7
parent fa38e433e1
commit ce97ee486c
6 changed files with 1461 additions and 100 deletions
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.