diff --git a/AudioInput.pas b/AudioInput.pas index 89489a6..6efc125 100644 --- a/AudioInput.pas +++ b/AudioInput.pas @@ -132,6 +132,9 @@ type // Читает следующий моно-сэмпл из кольцевого буфера. // Возвращает 0.0 если буфер пуст. function ReadSample: Double; + // Сколько сэмплов реально лежит в ring буфере (0..MY_MIC_RING_SIZE-1). + // Использовать для чтения только доступных данных без пуш-тишины. + function Available: Integer; property IsOpen: Boolean read FOpen; property LastError: string read GetLastError; @@ -534,4 +537,13 @@ begin FOutPt := newpt; end; +function TAudioInput.Available: Integer; +var d: Integer; +begin + // Считаем сколько непрочитанных сэмплов между OutPt (читатель) и InPt (писатель). + d := FInPt - FOutPt; + if d < 0 then d := d + MY_MIC_RING_SIZE; + Result := d; +end; + end. diff --git a/MainForm.pas b/MainForm.pas index 1150174..6eb528a 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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, diff --git a/Settings.pas b/Settings.pas index 8868d55..d01dafe 100644 --- a/Settings.pas +++ b/Settings.pas @@ -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 diff --git a/SettingsForm.pas b/SettingsForm.pas index 05ade3c..40b794c 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -27,12 +27,14 @@ uses Classes, SysUtils, Math, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, Spin, - FlatButton, AudioOutput, AudioInput, AppTheme; + FlatButton, AudioOutput, AudioInput, AppTheme, Settings; type // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон TOnPASettingsChange = procedure(MaxPower: Double; const BandCal: array of Double) of object; + // TX-настройки целиком (любая правка → один callback с актуальной структурой) + TOnTXSettingsChange = procedure(const T: TTXSettings) of object; TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector, SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object; @@ -66,6 +68,7 @@ type FPagePA: TScrollBox; FPageAdvanced: TScrollBox; FPageCAT: TScrollBox; + FPageTransmit: TScrollBox; FNavAudio: TFlatButton; FNavDisplay: TFlatButton; FNavSpectrum: TFlatButton; @@ -73,6 +76,7 @@ type FNavPA: TFlatButton; FNavAdvanced: TFlatButton; FNavCAT: TFlatButton; + FNavTransmit: TFlatButton; // ---- Audio tab controls ---- FCmbRXDev: TComboBox; @@ -108,6 +112,62 @@ type FEdMaxPower: TSpinEdit; FEdBandCal: array[0..10] of TFloatSpinEdit; + // ---- Transmit tab controls ---- + // Текущее состояние, отдаётся в OnTXChange целиком при любой правке. + FTX: TTXSettings; + // Mic source / options + FCmbTXMicSource: TComboBox; + FChkTXMicBoost: TCheckBox; + FChkTXMicBias: TCheckBox; + FChkTXMicLineIn: TCheckBox; + FChkTXMicPTT: TCheckBox; + FEdTXMicGain: TFloatSpinEdit; + // Filter + FEdTXFiltLow: TSpinEdit; + FEdTXFiltHigh: TSpinEdit; + FCmbTXFiltNC: TComboBox; + FCmbTXFiltMP: TComboBox; + FCmbTXFiltWindow: TComboBox; + // Compressor + FChkTXCompOn: TCheckBox; + FEdTXCompGain: TFloatSpinEdit; + // Leveler + FChkTXLevOn: TCheckBox; + FEdTXLevTop: TFloatSpinEdit; + FEdTXLevDecay: TSpinEdit; + // ALC + FChkTXALCOn: TCheckBox; + FEdTXALCMaxGain: TFloatSpinEdit; + FEdTXALCDecay: TSpinEdit; + // Phase Rotator + FChkTXPHROn: TCheckBox; + FEdTXPHRStages: TSpinEdit; + FEdTXPHRFreq: TFloatSpinEdit; + // EQ + FChkTXEQOn: TCheckBox; + FCmbTXEQBands: TComboBox; + FEdTXEQGain: array[0..10] of TFloatSpinEdit; + FEdTXEQFreq: array[0..10] of TFloatSpinEdit; + // AM + FEdTXAMCarrier: TFloatSpinEdit; + // FM + FEdTXFMDev: TFloatSpinEdit; + FEdTXFMLow: TSpinEdit; + FEdTXFMHigh: TSpinEdit; + FCmbTXFMEmph: TComboBox; + // CTCSS + FChkTXCTCSSOn: TCheckBox; + FEdTXCTCSSFreq: TFloatSpinEdit; + // TX Display (отдельный analyzer) + FCmbTXFFT: TComboBox; + FCmbTXWindow: TComboBox; + FCmbTXSpecDet: TComboBox; + FCmbTXSpecAvg: TComboBox; + FEdTXSpecAvgTime: TFloatSpinEdit; + FCmbTXWfDet: TComboBox; + FCmbTXWfAvg: TComboBox; + FEdTXWfAvgTime: TFloatSpinEdit; + // ---- CAT tab controls ---- FCATSerialEn: array[0..3] of TCheckBox; FCATSerialPort: array[0..3] of TEdit; @@ -132,6 +192,7 @@ type FOnVisibilityChange: TOnVisibilityChange; FOnFPSChange: TOnFPSChange; FOnThemeChange: TOnThemeChange; + FOnTXChange: TOnTXSettingsChange; FLoading: Boolean; @@ -141,6 +202,10 @@ type procedure BuildRX1Tab; procedure BuildPATab; procedure BuildCATTab; + procedure BuildTransmitTab; + procedure CollectTXFromUI; + procedure FireTXChange; + procedure OnTXAnyChange(Sender: TObject); procedure ApplyTheme(const T: TAppTheme); procedure NavClick(Sender: TObject); procedure SelectPage(APage: TWinControl; ANav: TFlatButton); @@ -214,6 +279,7 @@ type const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer); + procedure LoadTXSettings(const T: TTXSettings); property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange; property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; @@ -225,6 +291,7 @@ type property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange; property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange; property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange; + property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange; end; implementation @@ -429,9 +496,10 @@ begin FNavDisplay := MakeNavButton('Display', 110); FNavSpectrum := MakeNavButton('Spectrum', 146); FNavWaterfall := MakeNavButton('Waterfall', 182); - FNavPA := MakeNavButton('Power Amplifier', 218); - FNavAdvanced := MakeNavButton('Advanced', 254); - FNavCAT := MakeNavButton('CAT', 290); + FNavTransmit := MakeNavButton('Transmit', 218); + FNavPA := MakeNavButton('Power Amplifier', 254); + FNavAdvanced := MakeNavButton('Advanced', 290); + FNavCAT := MakeNavButton('CAT', 326); FContentPanel := TPanel.Create(Self); FContentPanel.Parent := Self; @@ -443,6 +511,7 @@ begin FPageDisplay := MakeScrollPage; FPageSpectrum := MakeScrollPage; FPageWaterfall := MakeScrollPage; + FPageTransmit := MakeScrollPage; FPagePA := MakeScrollPage; FPageAdvanced := MakeScrollPage; FPageCAT := MakeScrollPage; @@ -450,6 +519,7 @@ begin BuildAudioTab; BuildDisplayTab; BuildRX1Tab; + BuildTransmitTab; BuildPATab; BuildCATTab; @@ -477,6 +547,7 @@ begin else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay) else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum) else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall) + else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit) else if Sender = FNavPA then SelectPage(FPagePA, FNavPA) else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced) else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT); @@ -488,6 +559,7 @@ begin FPageDisplay.Visible := APage = FPageDisplay; FPageSpectrum.Visible := APage = FPageSpectrum; FPageWaterfall.Visible := APage = FPageWaterfall; + FPageTransmit.Visible := APage = FPageTransmit; FPagePA.Visible := APage = FPagePA; FPageAdvanced.Visible := APage = FPageAdvanced; FPageCAT.Visible := APage = FPageCAT; @@ -496,6 +568,7 @@ begin FNavDisplay.Active := ANav = FNavDisplay; FNavSpectrum.Active := ANav = FNavSpectrum; FNavWaterfall.Active := ANav = FNavWaterfall; + FNavTransmit.Active := ANav = FNavTransmit; FNavPA.Active := ANav = FNavPA; FNavAdvanced.Active := ANav = FNavAdvanced; FNavCAT.Active := ANav = FNavCAT; @@ -767,6 +840,412 @@ begin FWfAGCOffset := MkFloat(CX, R1 + 2 * STEP, SPW, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange); end; +// --------------------------------------------------------------------------- +// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit) +// --------------------------------------------------------------------------- + +procedure TSettingsForm.BuildTransmitTab; +const + MARGIN = 22; + PAD = 18; + LW = 150; + CX = PAD + LW + 12; + STEP = 32; + R1 = 42; + SPW = 96; + CMB_W = 200; + +var + Grp: TPanel; + Y, i: Integer; + + function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TSpinEdit; + begin + Result := TSpinEdit.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, SPW, BTN_H); + Result.MinValue := AMin; + Result.MaxValue := AMax; + Result.Value := AVal; + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnTXAnyChange; + end; + + function MkFloat(AParent: TWinControl; AX, AY: Integer; + AVal, AMin, AMax, AInc: Double): TFloatSpinEdit; + begin + Result := TFloatSpinEdit.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, SPW, BTN_H); + Result.MinValue := AMin; + Result.MaxValue := AMax; + Result.Increment := AInc; + Result.Value := AVal; + Result.DecimalPlaces := 2; + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnTXAnyChange; + end; + + function MkChk(AParent: TWinControl; AX, AY, AW: Integer; const ACap: string): TCheckBox; + begin + Result := TCheckBox.Create(Self); + Result.Parent := AParent; + Result.Caption := ACap; + Result.SetBounds(AX, AY, AW, 22); + Result.Font.Color := CLR_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnTXAnyChange; + end; + + function MkCmb(AParent: TWinControl; AX, AY, AW: Integer): TComboBox; + begin + Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange); + end; + +begin + with TLabel.Create(Self) do + begin + Parent := FPageTransmit; + Caption := 'Transmit'; + SetBounds(MARGIN, 18, 400, 28); + Font.Name := UI_FONT; + Font.Size := 16; + Font.Style := [fsBold]; + Font.Color := CLR_TEXT; + end; + with TLabel.Create(Self) do + begin + Parent := FPageTransmit; + Caption := 'TX-цепь WDSP: микрофон, фильтр, динамическая обработка, модуляция.'; + SetBounds(MARGIN, 48, 700, 20); + Font.Name := UI_FONT; + Font.Size := 9; + Font.Color := CLR_TEXTDIM; + end; + + // ---- Mic source / options ---- + Y := 86; + Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, 700, 220); + + MakeLbl(Grp, 'Mic source', PAD, R1 + 5, LW); + FCmbTXMicSource := MkCmb(Grp, CX, R1, CMB_W); + FCmbTXMicSource.Items.Add('Radio (HW mic)'); + FCmbTXMicSource.Items.Add('Sound card'); + FCmbTXMicSource.ItemIndex := 0; + + FChkTXMicBoost := MkChk(Grp, PAD, R1 + STEP, 200, 'Mic boost +20 dB'); + FChkTXMicBias := MkChk(Grp, PAD + 200, R1 + STEP, 200, 'Mic bias'); + FChkTXMicLineIn := MkChk(Grp, PAD + 400, R1 + STEP, 160, 'Line in'); + FChkTXMicPTT := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'Mic-jack PTT'); + + MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 3 * STEP + 5, LW); + FEdTXMicGain := MkFloat(Grp, CX, R1 + 3 * STEP, 10.0, -20.0, 60.0, 0.5); + + // ---- TX bandpass filter ---- + Inc(Y, 220 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, 700, 200); + + MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW); + FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 100); + MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90); + FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 2800); + + MakeLbl(Grp, 'FIR length', PAD, R1 + STEP + 5, LW); + FCmbTXFiltNC := MkCmb(Grp, CX, R1 + STEP, 140); + FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']); + FCmbTXFiltNC.ItemIndex := 2; + + MakeLbl(Grp, 'Phase mode', PAD, R1 + 2 * STEP + 5, LW); + FCmbTXFiltMP := MkCmb(Grp, CX, R1 + 2 * STEP, 200); + FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']); + FCmbTXFiltMP.ItemIndex := 1; + + MakeLbl(Grp, 'Window', PAD, R1 + 3 * STEP + 5, LW); + FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 3 * STEP, 240); + for i := 0 to High(WINDOW_NAMES) do + FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]); + FCmbTXFiltWindow.ItemIndex := 1; + + // ---- Compressor / Leveler / ALC / Phase Rotator ---- + Inc(Y, 200 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, 700, 220); + + FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor'); + MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80); + FEdTXCompGain := MkFloat(Grp, PAD + 320, R1, 5.0, 0.0, 20.0, 0.5); + + FChkTXLevOn := MkChk(Grp, PAD, R1 + STEP, 220, 'Leveler'); + MakeLbl(Grp, 'Top (dB)', PAD + 230, R1 + STEP + 5, 80); + FEdTXLevTop := MkFloat(Grp, PAD + 320, R1 + STEP, 5.0, 0.0, 20.0, 0.5); + MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + STEP + 5, 80); + FEdTXLevDecay := MkSpin(Grp, PAD + 520, R1 + STEP, 1, 5000, 500); + + FChkTXALCOn := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'ALC'); + MakeLbl(Grp, 'Max gain', PAD + 230, R1 + 2 * STEP + 5, 80); + FEdTXALCMaxGain := MkFloat(Grp, PAD + 320, R1 + 2 * STEP, 0.0, -20.0, 20.0, 0.5); + MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + 2 * STEP + 5, 80); + FEdTXALCDecay := MkSpin(Grp, PAD + 520, R1 + 2 * STEP, 1, 5000, 10); + + FChkTXPHROn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'Phase Rotator'); + MakeLbl(Grp, 'Stages', PAD + 230, R1 + 3 * STEP + 5, 80); + FEdTXPHRStages := MkSpin(Grp, PAD + 320, R1 + 3 * STEP, 2, 16, 8); + MakeLbl(Grp, 'Corner (Hz)', PAD + 430, R1 + 3 * STEP + 5, 80); + FEdTXPHRFreq := MkFloat(Grp, PAD + 520, R1 + 3 * STEP, 338.0, 50.0, 3000.0, 1.0); + + // ---- TX EQ ---- + Inc(Y, 220 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, 700, 320); + + FChkTXEQOn := MkChk(Grp, PAD, R1, 200, 'Enable EQ'); + MakeLbl(Grp, 'Bands', PAD + 230, R1 + 5, 60); + FCmbTXEQBands := MkCmb(Grp, PAD + 290, R1, 80); + FCmbTXEQBands.Items.AddStrings(['3','10']); + FCmbTXEQBands.ItemIndex := 1; + + // 11 строк: [0]=Preamp, [1..10]=полосы. Freq+Gain в одной строке. + for i := 0 to 10 do + begin + if i = 0 then + MakeLbl(Grp, 'Preamp (dB)', PAD, R1 + STEP + i * 22 + 3, 100) + else + MakeLbl(Grp, Format('Band %d', [i]), PAD, R1 + STEP + i * 22 + 3, 80); + FEdTXEQFreq[i] := MkFloat(Grp, PAD + 90, R1 + STEP + i * 22, 0.0, 0.0, 20000.0, 10.0); + FEdTXEQFreq[i].Width := 100; + MakeLbl(Grp, 'gain', PAD + 200, R1 + STEP + i * 22 + 3, 30); + FEdTXEQGain[i] := MkFloat(Grp, PAD + 230, R1 + STEP + i * 22, 0.0, -15.0, 15.0, 0.5); + FEdTXEQGain[i].Width := 80; + end; + + // ---- AM / FM / CTCSS ---- + Inc(Y, 320 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, 700, 220); + + MakeLbl(Grp, 'AM carrier level', PAD, R1 + 5, LW); + FEdTXAMCarrier := MkFloat(Grp, CX, R1, 0.5, 0.0, 1.0, 0.05); + + MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + STEP + 5, LW); + FEdTXFMDev := MkFloat(Grp, CX, R1 + STEP, 5000.0, 1000.0, 25000.0, 100.0); + + MakeLbl(Grp, 'FM AF low cut', PAD, R1 + 2 * STEP + 5, LW); + FEdTXFMLow := MkSpin(Grp, CX, R1 + 2 * STEP, 0, 5000, 300); + MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + 2 * STEP + 5, 100); + FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + 2 * STEP, 100, 8000, 3000); + + MakeLbl(Grp, 'FM emphasis', PAD, R1 + 3 * STEP + 5, LW); + FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 3 * STEP, 180); + FCmbTXFMEmph.Items.AddStrings(['Pre-EQ','Post-EQ']); + FCmbTXFMEmph.ItemIndex := 1; + + FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 4 * STEP, 220, 'CTCSS tone'); + MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 4 * STEP + 5, 80); + FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1); + + // ---- TX Display (отдельный analyzer для TX-спектра/водопада) ---- + Inc(Y, 220 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, 700, 290); + + MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW); + FCmbTXFFT := MkCmb(Grp, CX, R1, 140); + for i := 0 to High(FFT_NAMES) do FCmbTXFFT.Items.Add(FFT_NAMES[i]); + FCmbTXFFT.ItemIndex := 2; + + MakeLbl(Grp, 'Window', PAD, R1 + STEP + 5, LW); + FCmbTXWindow := MkCmb(Grp, CX, R1 + STEP, 240); + for i := 0 to High(WINDOW_NAMES) do FCmbTXWindow.Items.Add(WINDOW_NAMES[i]); + FCmbTXWindow.ItemIndex := 2; + + MakeLbl(Grp, 'Spec detector',PAD, R1 + 2 * STEP + 5, LW); + FCmbTXSpecDet := MkCmb(Grp, CX, R1 + 2 * STEP, 200); + for i := 0 to High(DETECTOR_NAMES) do FCmbTXSpecDet.Items.Add(DETECTOR_NAMES[i]); + FCmbTXSpecDet.ItemIndex := 0; + + MakeLbl(Grp, 'Spec average', PAD, R1 + 3 * STEP + 5, LW); + FCmbTXSpecAvg := MkCmb(Grp, CX, R1 + 3 * STEP, 200); + for i := 0 to High(AVGMODE_NAMES) do FCmbTXSpecAvg.Items.Add(AVGMODE_NAMES[i]); + FCmbTXSpecAvg.ItemIndex := 3; + MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 3 * STEP + 5, 100); + FEdTXSpecAvgTime := MkFloat(Grp, PAD + 540, R1 + 3 * STEP, 30.0, 1.0, 5000.0, 1.0); + + MakeLbl(Grp, 'WF detector', PAD, R1 + 4 * STEP + 5, LW); + FCmbTXWfDet := MkCmb(Grp, CX, R1 + 4 * STEP, 200); + for i := 0 to High(DETECTOR_NAMES) do FCmbTXWfDet.Items.Add(DETECTOR_NAMES[i]); + FCmbTXWfDet.ItemIndex := 0; + + MakeLbl(Grp, 'WF average', PAD, R1 + 5 * STEP + 5, LW); + FCmbTXWfAvg := MkCmb(Grp, CX, R1 + 5 * STEP, 200); + for i := 0 to High(WF_AVGMODE_NAMES) do FCmbTXWfAvg.Items.Add(WF_AVGMODE_NAMES[i]); + FCmbTXWfAvg.ItemIndex := 3; + MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 5 * STEP + 5, 100); + FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0); +end; + +procedure TSettingsForm.CollectTXFromUI; +var i: Integer; +begin + if FCmbTXMicSource <> nil then + FTX.MicSource := Max(0, FCmbTXMicSource.ItemIndex); + if FChkTXMicBoost <> nil then FTX.MicBoost := FChkTXMicBoost.Checked; + if FChkTXMicBias <> nil then FTX.MicBias := FChkTXMicBias.Checked; + if FChkTXMicLineIn <> nil then FTX.MicLineIn := FChkTXMicLineIn.Checked; + if FChkTXMicPTT <> nil then FTX.MicPTTEnabled := FChkTXMicPTT.Checked; + if FEdTXMicGain <> nil then FTX.MicGainDB := FEdTXMicGain.Value; + if FEdTXFiltLow <> nil then FTX.FilterLow := FEdTXFiltLow.Value; + if FEdTXFiltHigh <> nil then FTX.FilterHigh := FEdTXFiltHigh.Value; + if FCmbTXFiltNC <> nil then + FTX.FilterNC := StrToIntDef(FCmbTXFiltNC.Text, 1024); + if FCmbTXFiltMP <> nil then FTX.FilterMP := Max(0, FCmbTXFiltMP.ItemIndex); + if FCmbTXFiltWindow<> nil then FTX.FilterWindow := Max(0, FCmbTXFiltWindow.ItemIndex); + if FChkTXCompOn <> nil then FTX.CompressorOn := FChkTXCompOn.Checked; + if FEdTXCompGain <> nil then FTX.CompressorGain := FEdTXCompGain.Value; + if FChkTXLevOn <> nil then FTX.LevelerOn := FChkTXLevOn.Checked; + if FEdTXLevTop <> nil then FTX.LevelerTop := FEdTXLevTop.Value; + if FEdTXLevDecay <> nil then FTX.LevelerDecay := FEdTXLevDecay.Value; + if FChkTXALCOn <> nil then FTX.ALCOn := FChkTXALCOn.Checked; + if FEdTXALCMaxGain <> nil then FTX.ALCMaxGain := FEdTXALCMaxGain.Value; + if FEdTXALCDecay <> nil then FTX.ALCDecay := FEdTXALCDecay.Value; + if FChkTXPHROn <> nil then FTX.PhaseRotOn := FChkTXPHROn.Checked; + if FEdTXPHRStages <> nil then FTX.PhaseRotStages := FEdTXPHRStages.Value; + if FEdTXPHRFreq <> nil then FTX.PhaseRotFreq := FEdTXPHRFreq.Value; + if FChkTXEQOn <> nil then FTX.EQOn := FChkTXEQOn.Checked; + if FCmbTXEQBands <> nil then + if FCmbTXEQBands.ItemIndex = 0 then FTX.EQNumBands := 3 else FTX.EQNumBands := 10; + for i := 0 to 10 do + begin + if FEdTXEQGain[i] <> nil then FTX.EQGains[i] := FEdTXEQGain[i].Value; + if FEdTXEQFreq[i] <> nil then FTX.EQFreqs[i] := FEdTXEQFreq[i].Value; + end; + if FEdTXAMCarrier <> nil then FTX.AMCarrierLevel := FEdTXAMCarrier.Value; + if FEdTXFMDev <> nil then FTX.FMDeviation := FEdTXFMDev.Value; + if FEdTXFMLow <> nil then FTX.FMLowCut := FEdTXFMLow.Value; + if FEdTXFMHigh <> nil then FTX.FMHighCut := FEdTXFMHigh.Value; + if FCmbTXFMEmph <> nil then FTX.FMEmphPosition := Max(0, FCmbTXFMEmph.ItemIndex); + if FChkTXCTCSSOn <> nil then FTX.CTCSSOn := FChkTXCTCSSOn.Checked; + if FEdTXCTCSSFreq <> nil then FTX.CTCSSFreq := FEdTXCTCSSFreq.Value; + if (FCmbTXFFT <> nil) and (FCmbTXFFT.ItemIndex >= 0) then + FTX.TXFFTSize := FFT_SIZES[FCmbTXFFT.ItemIndex]; + if FCmbTXWindow <> nil then FTX.TXWindowType := Max(0, FCmbTXWindow.ItemIndex); + if FCmbTXSpecDet <> nil then FTX.TXSpecDetector := Max(0, FCmbTXSpecDet.ItemIndex); + if (FCmbTXSpecAvg <> nil) and (FCmbTXSpecAvg.ItemIndex >= 0) then + FTX.TXSpecAvgMode := AVGMODE_VALUES[FCmbTXSpecAvg.ItemIndex]; + if FEdTXSpecAvgTime <> nil then FTX.TXSpecAvgTimeMS := FEdTXSpecAvgTime.Value; + if FCmbTXWfDet <> nil then FTX.TXWfDetector := Max(0, FCmbTXWfDet.ItemIndex); + if (FCmbTXWfAvg <> nil) and (FCmbTXWfAvg.ItemIndex >= 0) then + FTX.TXWfAvgMode := WF_AVGMODE_VALUES[FCmbTXWfAvg.ItemIndex]; + if FEdTXWfAvgTime <> nil then FTX.TXWfAvgTimeMS := FEdTXWfAvgTime.Value; +end; + +procedure TSettingsForm.FireTXChange; +begin + if FLoading then Exit; + CollectTXFromUI; + if Assigned(FOnTXChange) then + FOnTXChange(FTX); +end; + +procedure TSettingsForm.OnTXAnyChange(Sender: TObject); +begin + FireTXChange; +end; + +procedure TSettingsForm.LoadTXSettings(const T: TTXSettings); +var i: Integer; +begin + FLoading := True; + try + FTX := T; + if FCmbTXMicSource <> nil then + FCmbTXMicSource.ItemIndex := EnsureRange(T.MicSource, 0, FCmbTXMicSource.Items.Count - 1); + if FChkTXMicBoost <> nil then FChkTXMicBoost.Checked := T.MicBoost; + if FChkTXMicBias <> nil then FChkTXMicBias.Checked := T.MicBias; + if FChkTXMicLineIn <> nil then FChkTXMicLineIn.Checked := T.MicLineIn; + if FChkTXMicPTT <> nil then FChkTXMicPTT.Checked := T.MicPTTEnabled; + if FEdTXMicGain <> nil then FEdTXMicGain.Value := T.MicGainDB; + if FEdTXFiltLow <> nil then FEdTXFiltLow.Value := T.FilterLow; + if FEdTXFiltHigh <> nil then FEdTXFiltHigh.Value := T.FilterHigh; + if FCmbTXFiltNC <> nil then FCmbTXFiltNC.Text := IntToStr(T.FilterNC); + if FCmbTXFiltMP <> nil then + FCmbTXFiltMP.ItemIndex := EnsureRange(T.FilterMP, 0, FCmbTXFiltMP.Items.Count - 1); + if FCmbTXFiltWindow<> nil then + FCmbTXFiltWindow.ItemIndex := EnsureRange(T.FilterWindow, 0, FCmbTXFiltWindow.Items.Count - 1); + if FChkTXCompOn <> nil then FChkTXCompOn.Checked := T.CompressorOn; + if FEdTXCompGain <> nil then FEdTXCompGain.Value := T.CompressorGain; + if FChkTXLevOn <> nil then FChkTXLevOn.Checked := T.LevelerOn; + if FEdTXLevTop <> nil then FEdTXLevTop.Value := T.LevelerTop; + if FEdTXLevDecay <> nil then FEdTXLevDecay.Value := T.LevelerDecay; + if FChkTXALCOn <> nil then FChkTXALCOn.Checked := T.ALCOn; + if FEdTXALCMaxGain <> nil then FEdTXALCMaxGain.Value := T.ALCMaxGain; + if FEdTXALCDecay <> nil then FEdTXALCDecay.Value := T.ALCDecay; + if FChkTXPHROn <> nil then FChkTXPHROn.Checked := T.PhaseRotOn; + if FEdTXPHRStages <> nil then FEdTXPHRStages.Value := T.PhaseRotStages; + if FEdTXPHRFreq <> nil then FEdTXPHRFreq.Value := T.PhaseRotFreq; + if FChkTXEQOn <> nil then FChkTXEQOn.Checked := T.EQOn; + if FCmbTXEQBands <> nil then + if T.EQNumBands <= 3 then FCmbTXEQBands.ItemIndex := 0 + else FCmbTXEQBands.ItemIndex := 1; + for i := 0 to 10 do + begin + if FEdTXEQGain[i] <> nil then FEdTXEQGain[i].Value := T.EQGains[i]; + if FEdTXEQFreq[i] <> nil then FEdTXEQFreq[i].Value := T.EQFreqs[i]; + end; + if FEdTXAMCarrier <> nil then FEdTXAMCarrier.Value := T.AMCarrierLevel; + if FEdTXFMDev <> nil then FEdTXFMDev.Value := T.FMDeviation; + if FEdTXFMLow <> nil then FEdTXFMLow.Value := T.FMLowCut; + if FEdTXFMHigh <> nil then FEdTXFMHigh.Value := T.FMHighCut; + if FCmbTXFMEmph <> nil then + FCmbTXFMEmph.ItemIndex := EnsureRange(T.FMEmphPosition, 0, FCmbTXFMEmph.Items.Count - 1); + if FChkTXCTCSSOn <> nil then FChkTXCTCSSOn.Checked := T.CTCSSOn; + if FEdTXCTCSSFreq <> nil then FEdTXCTCSSFreq.Value := T.CTCSSFreq; + + if FCmbTXFFT <> nil then + begin + FCmbTXFFT.ItemIndex := 2; + for i := 0 to High(FFT_SIZES) do + if FFT_SIZES[i] = T.TXFFTSize then + begin + FCmbTXFFT.ItemIndex := i; + Break; + end; + end; + if FCmbTXWindow <> nil then + FCmbTXWindow.ItemIndex := EnsureRange(T.TXWindowType, 0, FCmbTXWindow.Items.Count - 1); + if FCmbTXSpecDet <> nil then + FCmbTXSpecDet.ItemIndex := EnsureRange(T.TXSpecDetector, 0, FCmbTXSpecDet.Items.Count - 1); + if FCmbTXSpecAvg <> nil then + begin + FCmbTXSpecAvg.ItemIndex := 3; + for i := 0 to High(AVGMODE_VALUES) do + if AVGMODE_VALUES[i] = T.TXSpecAvgMode then + begin + FCmbTXSpecAvg.ItemIndex := i; + Break; + end; + end; + if FEdTXSpecAvgTime <> nil then FEdTXSpecAvgTime.Value := T.TXSpecAvgTimeMS; + if FCmbTXWfDet <> nil then + FCmbTXWfDet.ItemIndex := EnsureRange(T.TXWfDetector, 0, FCmbTXWfDet.Items.Count - 1); + if FCmbTXWfAvg <> nil then + begin + FCmbTXWfAvg.ItemIndex := 3; + for i := 0 to High(WF_AVGMODE_VALUES) do + if WF_AVGMODE_VALUES[i] = T.TXWfAvgMode then + begin + FCmbTXWfAvg.ItemIndex := i; + Break; + end; + end; + if FEdTXWfAvgTime <> nil then FEdTXWfAvgTime.Value := T.TXWfAvgTimeMS; + finally + FLoading := False; + end; +end; + // --------------------------------------------------------------------------- // BuildPATab // --------------------------------------------------------------------------- diff --git a/SpectrumView.pas b/SpectrumView.pas index 0b3e73d..9c9be41 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -274,19 +274,20 @@ begin end; // Цветовая схема водопада для светлой темы. -// Шум исчезает в фоне (кремовый), сигналы: зелень → бирюза → синева → индиго. +// Шум растворяется в фоне панели; сигналы: синий → cyan → зелёный → жёлтый → оранжевый → красный. function WaterfallLightTheme(ValueDB, LowDB, HighDB: Double): LongWord; const NSTOPS = 8; - SR: array[0..NSTOPS-1] of Integer = (240, 210, 148, 60, 14, 8, 24, 64); - SG: array[0..NSTOPS-1] of Integer = (238, 228, 200, 164, 132, 96, 52, 6); - SB: array[0..NSTOPS-1] of Integer = (230, 190, 118, 68, 96, 148, 166, 100); + // panel steel blue cyan green yell orange red + SR: array[0..NSTOPS-1] of Integer = (224, 160, 20, 0, 0, 220, 255, 255); + SG: array[0..NSTOPS-1] of Integer = (230, 185, 80, 185, 200, 210, 90, 20); + SB: array[0..NSTOPS-1] of Integer = (232, 210, 200, 210, 80, 0, 0, 20); var T: Double; Seg: Integer; R, G, B: Integer; begin - if ValueDB <= LowDB then begin Result := $FFF0EEE6; Exit; end; + if ValueDB <= LowDB then begin Result := $FFE0E6E8; Exit; end; if ValueDB >= HighDB then begin Result := ($FF shl 24) or (SR[NSTOPS-1] shl 16) or (SG[NSTOPS-1] shl 8) or SB[NSTOPS-1]; Exit; end; T := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB) * (NSTOPS - 1); Seg := Min(NSTOPS - 2, Trunc(T)); diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 9386f14..bce0dbe 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -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.