mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
WIP: TX implementation — per-device settings, separate analyzer, sound-card mic
- Settings: TTXSettings record + LoadTX/SaveTX, JSON section "tx" per MAC - WDSPEngine: separate TX_DISP_ID analyzer with own FFT/Window/Det/Avg parameters, ApplyTXAnalyzerSettings + setters - WDSPEngine: full TX chain setters (Filter/Compressor/Leveler/ALC/ PhaseRot/EQ/AMCarrier/FM/CTCSS) + ApplyTXChainSettings - WDSPEngine: ProcessTXBlock now feeds TX analyzer in DISPLAY_BLOCK_SIZE chunks (was passing whole FTXOut → analyzer ignored most of it) - WDSPEngine: ActiveDisplayID switches RX/TX in UpdateSpectrum - WDSPEngine: RX audio muted while TX active to avoid local feedback - WDSPEngine: default EQ frequencies in Create (zero freqs broke TX chain) - WDSPEngine: PushTXMicSampleD + OnPullMicSamples for sound-card path - AudioInput: Available counter for non-blocking pull - MainForm: PullSoundCardMic, BuildMicLineSelectByte, SendDUCSpecific with mic bias/boost/line/PTT bits, OnTXSettingsChange - MainForm: auto-switch MicSource on TX device select, TX diagnostic in StatusBar panel [5] - SettingsForm: new Transmit nav page with Microphone, Filter, Dynamic processing, EQ, AM/FM/CTCSS, TX Display sections NOT YET WORKING: sound-card mic produces ring activity but signal still not visible on TX spectrum/waterfall — needs further investigation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+249
@@ -30,6 +30,67 @@ type
|
||||
SpanHz: Double;
|
||||
end;
|
||||
|
||||
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
||||
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
|
||||
// (Тип TTXMicSource живёт в WDSPEngine — здесь храним как Integer,
|
||||
// чтобы не тащить циклическую зависимость с WDSPEngine.)
|
||||
TTXSettings = record
|
||||
// Mic source select
|
||||
MicSource: Integer; // 0=Radio, 1=SoundCard
|
||||
// Radio mic options (DUC Specific byte 50 bits)
|
||||
MicBoost: Boolean; // bit0: +20 dB
|
||||
MicBias: Boolean; // bit4: электретный bias
|
||||
MicLineIn: Boolean; // bit1: 1=Line in, 0=Mic in
|
||||
MicPTTEnabled: Boolean; // bit2: Mic PTT enable
|
||||
// Mic gain (dB, применяется в WDSP PanelGain1)
|
||||
MicGainDB: Double; // -10..+30
|
||||
// TX bandpass filter
|
||||
FilterLow: Integer; // Hz, для SSB обычно 100
|
||||
FilterHigh: Integer; // Hz, для SSB обычно 2800
|
||||
FilterNC: Integer; // FIR length (1024/2048/4096)
|
||||
FilterMP: Integer; // 0=Linear Phase, 1=Low Latency
|
||||
FilterWindow: Integer; // 0=Rect..6=BH7
|
||||
// Compressor
|
||||
CompressorOn: Boolean;
|
||||
CompressorGain: Double; // 0..20 dB
|
||||
// Leveler (TX AGC)
|
||||
LevelerOn: Boolean;
|
||||
LevelerTop: Double; // 0..10 dB
|
||||
LevelerDecay: Integer; // ms
|
||||
// ALC (Auto Level Control)
|
||||
ALCOn: Boolean;
|
||||
ALCMaxGain: Double; // -10..+10 dB
|
||||
ALCDecay: Integer; // ms
|
||||
// Phase Rotator
|
||||
PhaseRotOn: Boolean;
|
||||
PhaseRotStages: Integer; // 4/6/8
|
||||
PhaseRotFreq: Double; // Hz, обычно 338
|
||||
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
|
||||
EQOn: Boolean;
|
||||
EQNumBands: Integer; // 3 или 10
|
||||
EQGains: array[0..10] of Double; // dB, ±15
|
||||
EQFreqs: array[0..10] of Double; // Hz
|
||||
// AM modulator
|
||||
AMCarrierLevel: Double; // 0..1
|
||||
// FM modulator
|
||||
FMDeviation: Double; // Hz, 2500..5000
|
||||
FMLowCut: Integer; // AF filter low
|
||||
FMHighCut: Integer; // AF filter high
|
||||
FMEmphPosition: Integer; // 0=PreEQ, 1=PostEQ
|
||||
// CTCSS
|
||||
CTCSSOn: Boolean;
|
||||
CTCSSFreq: Double; // Hz
|
||||
// TX Display (отдельный analyzer для TX-спектра/водопада)
|
||||
TXFFTSize: Integer; // 4096..262144
|
||||
TXWindowType: Integer; // 0..6
|
||||
TXSpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
|
||||
TXSpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
|
||||
TXSpecAvgTimeMS: Double;
|
||||
TXWfDetector: Integer;
|
||||
TXWfAvgMode: Integer;
|
||||
TXWfAvgTimeMS: Double;
|
||||
end;
|
||||
|
||||
TGlobalSettings = record
|
||||
Volume: Integer;
|
||||
DriveLevel: Integer;
|
||||
@@ -119,9 +180,15 @@ type
|
||||
const B: TBandSettings);
|
||||
function LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||||
out B: TBandSettings): Boolean;
|
||||
// TX settings per-device (JSON-секция "tx" под MAC).
|
||||
// LoadTX возвращает True если устройство уже зарегистрировано в JSON;
|
||||
// в любом случае T заполняется (дефолтами или сохранёнными значениями).
|
||||
function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean;
|
||||
procedure SaveTX(const MAC: array of Byte; const T: TTXSettings);
|
||||
class function MacToStr(const MAC: array of Byte): string;
|
||||
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
||||
class procedure DefaultGlobal(out G: TGlobalSettings);
|
||||
class procedure DefaultTX(out T: TTXSettings);
|
||||
// Размер окна — не привязан к MAC, хранится в корне JSON
|
||||
procedure SaveWindowBounds(L, T, W, H: Integer);
|
||||
procedure LoadWindowBounds(out L, T, W, H: Integer);
|
||||
@@ -628,6 +695,188 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TX settings (per-device, JSON-секция "tx" под MAC)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
|
||||
var
|
||||
i: Integer;
|
||||
const
|
||||
// 10-полосный голосовой EQ (Hz). [0]=preamp slot, [1..10]=bands.
|
||||
// Подобрано под голос, а не под музыку: больше точек в 100..3500 Hz.
|
||||
DefaultEQFreqs: array[0..10] of Double = (
|
||||
0, // [0] preamp slot — частота не используется
|
||||
100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500);
|
||||
begin
|
||||
FillChar(T, SizeOf(T), 0);
|
||||
// Mic source — Radio (HW). 0=Radio, 1=SoundCard (синхронно с TTXMicSource в WDSPEngine).
|
||||
T.MicSource := 0;
|
||||
T.MicBoost := False;
|
||||
T.MicBias := True;
|
||||
T.MicLineIn := False;
|
||||
T.MicPTTEnabled := False;
|
||||
T.MicGainDB := 10.0;
|
||||
// SSB-фильтр по умолчанию
|
||||
T.FilterLow := 100;
|
||||
T.FilterHigh := 2800;
|
||||
T.FilterNC := 1024;
|
||||
T.FilterMP := 1; // Low Latency (как в piHPSDR)
|
||||
T.FilterWindow := 1; // BH4 / Blackman-Harris
|
||||
// Compressor — выкл по умолчанию
|
||||
T.CompressorOn := False;
|
||||
T.CompressorGain:= 5.0;
|
||||
// Leveler — вкл, типичные значения piHPSDR/Thetis
|
||||
T.LevelerOn := True;
|
||||
T.LevelerTop := 5.0;
|
||||
T.LevelerDecay := 500;
|
||||
// ALC — вкл всегда (защита от перегруза DAC)
|
||||
T.ALCOn := True;
|
||||
T.ALCMaxGain := 0.0;
|
||||
T.ALCDecay := 10;
|
||||
// Phase Rotator — выкл
|
||||
T.PhaseRotOn := False;
|
||||
T.PhaseRotStages:= 8;
|
||||
T.PhaseRotFreq := 338.0;
|
||||
// EQ — выкл, нулевые гейны
|
||||
T.EQOn := False;
|
||||
T.EQNumBands := 10;
|
||||
for i := 0 to 10 do
|
||||
begin
|
||||
T.EQGains[i] := 0.0;
|
||||
T.EQFreqs[i] := DefaultEQFreqs[i];
|
||||
end;
|
||||
// AM
|
||||
T.AMCarrierLevel := 0.5;
|
||||
// FM
|
||||
T.FMDeviation := 5000.0;
|
||||
T.FMLowCut := 300;
|
||||
T.FMHighCut := 3000;
|
||||
T.FMEmphPosition := 1; // PostEQ
|
||||
// CTCSS
|
||||
T.CTCSSOn := False;
|
||||
T.CTCSSFreq := 100.0;
|
||||
// TX Display defaults — те же что у RX (см. DefaultGlobal)
|
||||
T.TXFFTSize := 16384; // меньше чем RX — TX-полоса узкая, FFT не нужен большой
|
||||
T.TXWindowType := 2; // Hann
|
||||
T.TXSpecDetector := 0; // Peak
|
||||
T.TXSpecAvgMode := 3; // Log Recursive
|
||||
T.TXSpecAvgTimeMS := 30.0;
|
||||
T.TXWfDetector := 0;
|
||||
T.TXWfAvgMode := 3;
|
||||
T.TXWfAvgTimeMS := 120.0;
|
||||
end;
|
||||
|
||||
function TSettingsManager.LoadTX(const MAC: array of Byte;
|
||||
out T: TTXSettings): Boolean;
|
||||
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
|
||||
begin
|
||||
DefaultTX(T);
|
||||
MacStr := MacToStr(MAC);
|
||||
Result := FRoot.Find(MacStr) <> nil;
|
||||
if not Result then Exit;
|
||||
DevObj := GetDevObj(MacStr);
|
||||
// Если секции "tx" ещё нет — оставляем дефолты, не материализуем пустую секцию
|
||||
if DevObj.Find('tx') = nil then Exit;
|
||||
O := EnsureObj(DevObj, 'tx');
|
||||
|
||||
T.MicSource := JI(O,'mic_source', T.MicSource);
|
||||
T.MicBoost := JB(O,'mic_boost', T.MicBoost);
|
||||
T.MicBias := JB(O,'mic_bias', T.MicBias);
|
||||
T.MicLineIn := JB(O,'mic_line_in', T.MicLineIn);
|
||||
T.MicPTTEnabled := JB(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||||
T.MicGainDB := JD(O,'mic_gain_db', T.MicGainDB);
|
||||
T.FilterLow := JI(O,'filter_low', T.FilterLow);
|
||||
T.FilterHigh := JI(O,'filter_high', T.FilterHigh);
|
||||
T.FilterNC := JI(O,'filter_nc', T.FilterNC);
|
||||
T.FilterMP := JI(O,'filter_mp', T.FilterMP);
|
||||
T.FilterWindow := JI(O,'filter_window', T.FilterWindow);
|
||||
T.CompressorOn := JB(O,'compressor_on', T.CompressorOn);
|
||||
T.CompressorGain := JD(O,'compressor_gain', T.CompressorGain);
|
||||
T.LevelerOn := JB(O,'leveler_on', T.LevelerOn);
|
||||
T.LevelerTop := JD(O,'leveler_top', T.LevelerTop);
|
||||
T.LevelerDecay := JI(O,'leveler_decay', T.LevelerDecay);
|
||||
T.ALCOn := JB(O,'alc_on', T.ALCOn);
|
||||
T.ALCMaxGain := JD(O,'alc_max_gain', T.ALCMaxGain);
|
||||
T.ALCDecay := JI(O,'alc_decay', T.ALCDecay);
|
||||
T.PhaseRotOn := JB(O,'phase_rot_on', T.PhaseRotOn);
|
||||
T.PhaseRotStages := JI(O,'phase_rot_stages',T.PhaseRotStages);
|
||||
T.PhaseRotFreq := JD(O,'phase_rot_freq', T.PhaseRotFreq);
|
||||
T.EQOn := JB(O,'eq_on', T.EQOn);
|
||||
T.EQNumBands := JI(O,'eq_num_bands', T.EQNumBands);
|
||||
for i := 0 to 10 do
|
||||
begin
|
||||
T.EQGains[i] := JD(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
|
||||
T.EQFreqs[i] := JD(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
|
||||
end;
|
||||
T.AMCarrierLevel := JD(O,'am_carrier_level',T.AMCarrierLevel);
|
||||
T.FMDeviation := JD(O,'fm_deviation', T.FMDeviation);
|
||||
T.FMLowCut := JI(O,'fm_low_cut', T.FMLowCut);
|
||||
T.FMHighCut := JI(O,'fm_high_cut', T.FMHighCut);
|
||||
T.FMEmphPosition := JI(O,'fm_emph_position',T.FMEmphPosition);
|
||||
T.CTCSSOn := JB(O,'ctcss_on', T.CTCSSOn);
|
||||
T.CTCSSFreq := JD(O,'ctcss_freq', T.CTCSSFreq);
|
||||
T.TXFFTSize := JI(O,'tx_fft_size', T.TXFFTSize);
|
||||
T.TXWindowType := JI(O,'tx_win_type', T.TXWindowType);
|
||||
T.TXSpecDetector := JI(O,'tx_spec_detector', T.TXSpecDetector);
|
||||
T.TXSpecAvgMode := JI(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
|
||||
T.TXSpecAvgTimeMS := JD(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
|
||||
T.TXWfDetector := JI(O,'tx_wf_detector', T.TXWfDetector);
|
||||
T.TXWfAvgMode := JI(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||||
T.TXWfAvgTimeMS := JD(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||||
end;
|
||||
|
||||
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
|
||||
const T: TTXSettings);
|
||||
var O: TJSONObject; i: Integer;
|
||||
begin
|
||||
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx');
|
||||
JW(O,'mic_source', T.MicSource);
|
||||
JW(O,'mic_boost', T.MicBoost);
|
||||
JW(O,'mic_bias', T.MicBias);
|
||||
JW(O,'mic_line_in', T.MicLineIn);
|
||||
JW(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||||
JW(O,'mic_gain_db', T.MicGainDB);
|
||||
JW(O,'filter_low', T.FilterLow);
|
||||
JW(O,'filter_high', T.FilterHigh);
|
||||
JW(O,'filter_nc', T.FilterNC);
|
||||
JW(O,'filter_mp', T.FilterMP);
|
||||
JW(O,'filter_window', T.FilterWindow);
|
||||
JW(O,'compressor_on', T.CompressorOn);
|
||||
JW(O,'compressor_gain', T.CompressorGain);
|
||||
JW(O,'leveler_on', T.LevelerOn);
|
||||
JW(O,'leveler_top', T.LevelerTop);
|
||||
JW(O,'leveler_decay', T.LevelerDecay);
|
||||
JW(O,'alc_on', T.ALCOn);
|
||||
JW(O,'alc_max_gain', T.ALCMaxGain);
|
||||
JW(O,'alc_decay', T.ALCDecay);
|
||||
JW(O,'phase_rot_on', T.PhaseRotOn);
|
||||
JW(O,'phase_rot_stages',T.PhaseRotStages);
|
||||
JW(O,'phase_rot_freq', T.PhaseRotFreq);
|
||||
JW(O,'eq_on', T.EQOn);
|
||||
JW(O,'eq_num_bands', T.EQNumBands);
|
||||
for i := 0 to 10 do
|
||||
begin
|
||||
JW(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
|
||||
JW(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
|
||||
end;
|
||||
JW(O,'am_carrier_level',T.AMCarrierLevel);
|
||||
JW(O,'fm_deviation', T.FMDeviation);
|
||||
JW(O,'fm_low_cut', T.FMLowCut);
|
||||
JW(O,'fm_high_cut', T.FMHighCut);
|
||||
JW(O,'fm_emph_position',T.FMEmphPosition);
|
||||
JW(O,'ctcss_on', T.CTCSSOn);
|
||||
JW(O,'ctcss_freq', T.CTCSSFreq);
|
||||
JW(O,'tx_fft_size', T.TXFFTSize);
|
||||
JW(O,'tx_win_type', T.TXWindowType);
|
||||
JW(O,'tx_spec_detector', T.TXSpecDetector);
|
||||
JW(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
|
||||
JW(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
|
||||
JW(O,'tx_wf_detector', T.TXWfDetector);
|
||||
JW(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||||
JW(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||||
end;
|
||||
|
||||
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
|
||||
var O: TJSONObject;
|
||||
begin
|
||||
|
||||
Reference in New Issue
Block a user