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
+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