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
+533 -73
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);
@@ -584,12 +695,68 @@ begin
FDisplayThread := nil;
// TX инициализация
FTXMicHead := 0;
FTXMicTail := 0;
FTXActive := False;
FTXThread := nil;
FTXMicSem := RTLEventCreate;
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,53 +801,81 @@ 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,
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
KEEP_TIME_SEC * AnalyzerFFT * EnsureRange(FDisplayFPS, 5, 100)));
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.