mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 04:47:35 +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:
+178
-18
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user