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