mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали. Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска на спектре и маркер VFO верны без правок в UI, а слайсы получают свой сдвиг по своему режиму. Правила эфира: - голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только PTT; несущую даёт кейер, бит CWX или тон TUN; - байт 5 собирается из настроек и перепосылается на каждой смене режима и TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на замыкание ключа посреди SSB; - TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO; - приёмник на передаче в CW не глушится, иначе умолкает программный сайдтон и эфир между посылками. Программная передача текста (CWMorse): поток с абсолютными дедлайнами дёргает бит CWX в High Priority, элементы рисует прошивка. Касание манипулятора или снятие MOX обрывают передачу. Память сообщений — окно CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY. Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS. Программный сайдтон точен для передачи текста и прямого ключа; при иамбике таймингом владеет FPGA, поэтому там верен только аппаратный тон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+312
-21
@@ -50,7 +50,7 @@ uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse;
|
||||
|
||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||||
@@ -433,6 +433,22 @@ type
|
||||
// TTXProfile). Истина для эфира — всегда живые FTXSettings/FDrivePercent;
|
||||
// активный профиль ведётся за ними (StoreActiveTXProfile).
|
||||
FTXProfiles: TTXProfileList;
|
||||
// Телеграф: параметры кейера/сайдтона/pitch (DUC Specific 5..13,17 + CW-тракт).
|
||||
FCWSettings: TCWSettings;
|
||||
// Программная передача текста (бит CWX). Поток создаётся лениво — у тех,
|
||||
// кто телеграфом не пользуется, лишней нити не заводится.
|
||||
FCWSender: TCWSender;
|
||||
// Состояние ключа для ПРОГРАММНОГО сайдтона. Пишется потоком передачи и
|
||||
// разбором HP-статуса (прямой ключ), читается DSP-потоком в MixSidetone —
|
||||
// толерантная гонка на одном булеве, как FHPp*.
|
||||
FCWKeyDown: Boolean;
|
||||
// Последний отправленный байт 5 — чтобы не гнать DUC-пакет на каждую
|
||||
// смену режима, а только когда телеграфная конфигурация реально поменялась.
|
||||
FLastCWOptions: Byte;
|
||||
FCWSTPhase: Double; // фаза генератора (только DSP-поток)
|
||||
FCWSTEnv: Double; // огибающая 0..1, сглаживает щелчки (DSP-поток)
|
||||
FCWSTBufL: array of Single; // буферы подмешивания (только DSP-поток)
|
||||
FCWSTBufR: array of Single;
|
||||
FAlexSettings: TAlexSettings;
|
||||
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
|
||||
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
|
||||
@@ -584,6 +600,15 @@ type
|
||||
procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP
|
||||
procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть
|
||||
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP
|
||||
// ---- Телеграф ----------------------------------------------------------
|
||||
function CWTXActive: Boolean; // передачей владеет FPGA-кейер
|
||||
function CWOptionsByte: Byte; // DUC Specific байт 5
|
||||
procedure SyncCWKeyer; // байт 5 под текущий TX-режим
|
||||
procedure CWXKeyEvent(Down: Boolean); // колбэк потока передачи
|
||||
// Подмешать программный сайдтон в блок RX-аудио (DSP-поток). Возвращает
|
||||
// False, если подмешивать нечего — тогда вызывающий отдаёт исходный блок.
|
||||
function MixSidetone(const Left, Right: array of Single;
|
||||
Count: Integer): Boolean;
|
||||
function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате)
|
||||
|
||||
// ---- TX-профили --------------------------------------------------------
|
||||
@@ -917,6 +942,19 @@ type
|
||||
procedure SetMOX(On_: Boolean);
|
||||
procedure SetTune(On_: Boolean); // TUN
|
||||
|
||||
// ---- Телеграф ----------------------------------------------------------
|
||||
// Настройки применяются целиком: pitch уезжает в движок (там он двигает
|
||||
// гетеродин и полосу), остальное — в DUC Specific прошивке.
|
||||
function CWSettings: TCWSettings;
|
||||
procedure SetCWSettings(const C: TCWSettings);
|
||||
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
|
||||
// CWX в High Priority — точки/тире рисует прошивка по этому биту (эталон
|
||||
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
||||
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
|
||||
procedure CWXSend(const Text: string);
|
||||
procedure CWXAbort;
|
||||
function CWXBusy: Boolean;
|
||||
|
||||
// ---- PureSignal ----
|
||||
// Доступность PS у активного бэкенда/платы (Caps.HasPureSignal).
|
||||
function PSAvailable: Boolean;
|
||||
@@ -1108,6 +1146,8 @@ begin
|
||||
DefaultFilterTable(FFilterTable); // до подключения — заводские фильтры
|
||||
ApplyFilterSlot(FFilter);
|
||||
FAGCMode := 1; FAGCTop := 90;
|
||||
TSettingsManager.DefaultCW(FCWSettings); // до подключения — заводской телеграф
|
||||
FLastCWOptions := $FF; // «ещё не слали» — первый пуш пройдёт
|
||||
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
|
||||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||||
FVolume := 70; FTxMonVolume := 40; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||||
@@ -1239,6 +1279,12 @@ end;
|
||||
|
||||
destructor TRadioController.Destroy;
|
||||
begin
|
||||
// Поток передачи телеграфа гасим ПЕРВЫМ: его колбэк ключа лезет в бэкенд.
|
||||
if Assigned(FCWSender) then
|
||||
begin
|
||||
FCWSender.AbortSending;
|
||||
FreeAndNil(FCWSender);
|
||||
end;
|
||||
FreeEngines;
|
||||
FreeAndNil(FDeviceStore);
|
||||
FreeAndNil(FSettings);
|
||||
@@ -1385,6 +1431,7 @@ var
|
||||
SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double;
|
||||
Bi: Integer;
|
||||
Is6m, IsTx: Boolean;
|
||||
KeyDot, KeyDash: Boolean;
|
||||
begin
|
||||
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
|
||||
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
|
||||
@@ -1447,6 +1494,17 @@ begin
|
||||
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
|
||||
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
|
||||
FHPpOverload := Status.ADCOverload;
|
||||
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
|
||||
// Любое касание обрывает программную передачу — как в Thetis/pihpsdr
|
||||
// («key hit» останавливает CAT CW), иначе оператор не может вклиниться.
|
||||
KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
|
||||
KeyDash := (Status.StatusBits and HPS_DASH) <> 0;
|
||||
if (KeyDot or KeyDash) and CWXBusy then CWXAbort;
|
||||
// Программный сайдтон от лепестков верен ТОЛЬКО у прямого ключа: при иамбике
|
||||
// элементы считает FPGA, а PC видит лишь замыкание контакта.
|
||||
if (FCWSettings.KeyerMode = 0) and (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||||
and (not CWXBusy) then
|
||||
FCWKeyDown := KeyDot or KeyDash;
|
||||
Invoke(ApplyHPStatus);
|
||||
end;
|
||||
|
||||
@@ -1533,6 +1591,19 @@ begin
|
||||
// the independent pre-volume tap is handled by OnDemodAudioReady.
|
||||
if FMode in [MODE_DMR, MODE_FMRAW] then Exit;
|
||||
|
||||
// Программный сайдтон подмешиваем ДО всех потребителей: его должны слышать
|
||||
// и локальная звуковуха, и web-клиент, и динамик трансивера.
|
||||
if MixSidetone(Left, Right, Count) then
|
||||
begin
|
||||
if FSendAudioToRadio then
|
||||
FNetwork.SendSpeakerAudio(FCWSTBufL, FCWSTBufR, Count);
|
||||
if Assigned(FOnAudioConsume) and FOnAudioConsume(FCWSTBufL, FCWSTBufR, Count) then
|
||||
Exit;
|
||||
if FLocalAudio then
|
||||
FAudioOut.Write(FCWSTBufL, FCWSTBufR, Count);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if FSendAudioToRadio then
|
||||
FNetwork.SendSpeakerAudio(Left, Right, Count);
|
||||
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
|
||||
@@ -2255,6 +2326,7 @@ begin
|
||||
ActiveMicInput.Flush;
|
||||
end;
|
||||
end;
|
||||
if FTxSliceId = Id then SyncCWKeyer;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceFilter(Id, Low, High: Integer);
|
||||
@@ -2909,17 +2981,26 @@ begin
|
||||
// Мультислайс-TX: если выбран слайс-источник — передаём на его частоте
|
||||
// (слайсы не несут repeater-конфиг, FM-сдвиг не применяем).
|
||||
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then
|
||||
Exit(S.TargetHz);
|
||||
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
|
||||
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
|
||||
// за активным RX (TX-A горит, но передача шла на B при активном B).
|
||||
if FSplitTxB then Result := FVfoB
|
||||
else Result := FVfoA;
|
||||
if FMode = MODE_FM then
|
||||
case FFMRptDir of
|
||||
RPT_MINUS: Result := Result - FFMRptOffsetHz;
|
||||
RPT_PLUS: Result := Result + FFMRptOffsetHz;
|
||||
end;
|
||||
Result := S.TargetHz
|
||||
else
|
||||
begin
|
||||
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
|
||||
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
|
||||
// за активным RX (TX-A горит, но передача шла на B при активном B).
|
||||
if FSplitTxB then Result := FVfoB
|
||||
else Result := FVfoA;
|
||||
if FMode = MODE_FM then
|
||||
case FFMRptDir of
|
||||
RPT_MINUS: Result := Result - FFMRptOffsetHz;
|
||||
RPT_PLUS: Result := Result + FFMRptOffsetHz;
|
||||
end;
|
||||
end;
|
||||
// Телеграф + TUN: несущую даёт SSB-модулятор из тона pitch, поэтому DUC надо
|
||||
// сдвинуть навстречу — иначе настроечная несущая уедет от VFO на pitch. При
|
||||
// работе ключом (кейер/CWX) сдвигать нечего: РЧ формирует прошивка на DUC.
|
||||
if FTuning and (ActiveTXMode in [MODE_CWL, MODE_CWU]) and FWDSPReady
|
||||
and Assigned(FDSPEngine) then
|
||||
Result := Result + FDSPEngine.CWLOOffset(ActiveTXMode);
|
||||
end;
|
||||
|
||||
function TRadioController.ActiveTXMode: Integer;
|
||||
@@ -2975,6 +3056,7 @@ begin
|
||||
// без сброса первые ~200мс передачи ушли бы в эфир «из прошлого».
|
||||
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
|
||||
ActiveMicInput.Flush;
|
||||
SyncCWKeyer; // кейер прошивки — под режим нового источника
|
||||
FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса
|
||||
ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса
|
||||
PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx
|
||||
@@ -4043,8 +4125,15 @@ begin
|
||||
ActiveMicInput.Flush;
|
||||
end;
|
||||
// Если TUN активен — пересчитываем знак частоты под новый режим
|
||||
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||||
if FTuning then
|
||||
begin
|
||||
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
|
||||
FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG)
|
||||
else
|
||||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||||
end;
|
||||
end;
|
||||
SyncCWKeyer;
|
||||
ApplyModeFilter;
|
||||
Changed(rfMode);
|
||||
end;
|
||||
@@ -4941,21 +5030,65 @@ begin
|
||||
FDSPEngine.SetANF(FANF);
|
||||
end;
|
||||
|
||||
function TRadioController.CWTXActive: Boolean;
|
||||
// «Телеграфом управляет железо»: режим ИСТОЧНИКА передачи — CW и кейер в
|
||||
// прошивке. В этом состоянии голосовой TXA не запускается вовсе (эталон
|
||||
// Thetis, console.cs:29570), а точки/тире формирует FPGA.
|
||||
begin
|
||||
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU]) and FCWSettings.FWKeyer;
|
||||
end;
|
||||
|
||||
function TRadioController.CWOptionsByte: Byte;
|
||||
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же
|
||||
// объяснение, почему iambic-бит выставляется и для Mode B).
|
||||
// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT
|
||||
// на замыкание ключа посреди SSB (Thetis, console.cs:27394).
|
||||
begin
|
||||
Result := 0;
|
||||
if not CWTXActive then Exit;
|
||||
Result := Result or CW_MODE;
|
||||
if FCWSettings.ReverseKeys then Result := Result or CW_REVERSE_KEYS;
|
||||
case FCWSettings.KeyerMode of
|
||||
1: Result := Result or CW_IAMBIC; // Mode A
|
||||
2: Result := Result or CW_IAMBIC or CW_MODE_B; // Mode B
|
||||
end; // 0 = прямой ключ
|
||||
if FCWSettings.SidetoneHW and (FCWSettings.SidetoneHWLevel > 0) then
|
||||
Result := Result or CW_SIDETONE;
|
||||
if FCWSettings.StrictSpacing then Result := Result or CW_STRICT_SPACING;
|
||||
if FCWSettings.BreakIn then Result := Result or CW_BREAK_IN;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SendDUCSpecificFromSettings;
|
||||
var
|
||||
DUCPkt: TDUCSpecificPacket;
|
||||
RateKsps: Word;
|
||||
HangMs: Word;
|
||||
begin
|
||||
if not (FNetwork.Connected and FNetwork.Running) then Exit;
|
||||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||||
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
|
||||
RateKsps := FDSPEngine.TXSampleRate div 1000;
|
||||
DUCPkt.NumDACs := 1;
|
||||
DUCPkt.SidetoneLevel := 50;
|
||||
DUCPkt.SidetoneFreqHi := Hi(600);
|
||||
DUCPkt.SidetoneFreqLo := Lo(600);
|
||||
DUCPkt.KeyerSpeed := 20;
|
||||
DUCPkt.KeyerWeight := 50;
|
||||
DUCPkt.CWOptions := CWOptionsByte;
|
||||
// Уровень/частота аппаратного сайдтона — тон в наушниках САМОГО трансивера.
|
||||
if FCWSettings.SidetoneHW then
|
||||
DUCPkt.SidetoneLevel := Byte(EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127))
|
||||
else
|
||||
DUCPkt.SidetoneLevel := 0;
|
||||
DUCPkt.SidetoneFreqHi := Hi(Word(EnsureRange(FCWSettings.Pitch, 200, 1200)));
|
||||
DUCPkt.SidetoneFreqLo := Lo(Word(EnsureRange(FCWSettings.Pitch, 200, 1200)));
|
||||
DUCPkt.KeyerSpeed := Byte(EnsureRange(FCWSettings.Speed, 1, 60));
|
||||
DUCPkt.KeyerWeight := Byte(EnsureRange(FCWSettings.Weight, 33, 66));
|
||||
// Hang — сколько прошивка держит передачу после последнего элемента (semi
|
||||
// break-in). При выключенном break-in железу его знать незачем.
|
||||
if FCWSettings.BreakIn then
|
||||
HangMs := Word(EnsureRange(FCWSettings.HangTimeMS, 0, 2000))
|
||||
else
|
||||
HangMs := 0;
|
||||
DUCPkt.HangDelayHi := Hi(HangMs);
|
||||
DUCPkt.HangDelayLo := Lo(HangMs);
|
||||
DUCPkt.RFDelay := Byte(EnsureRange(FCWSettings.RFDelayMS, 0, 255));
|
||||
DUCPkt.CWRampPeriod := Byte(EnsureRange(FCWSettings.RampMS, 0, 20));
|
||||
DUCPkt.DUC0RateHi := Hi(RateKsps);
|
||||
DUCPkt.DUC0RateLo := Lo(RateKsps);
|
||||
DUCPkt.DUC0Bits := 24;
|
||||
@@ -4969,6 +5102,7 @@ begin
|
||||
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
|
||||
DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
|
||||
DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
|
||||
FLastCWOptions := DUCPkt.CWOptions;
|
||||
FNetwork.SendDUCSpecific(DUCPkt);
|
||||
end;
|
||||
|
||||
@@ -5316,6 +5450,9 @@ begin
|
||||
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
|
||||
Exit;
|
||||
WasTransmitting := FTransmitting;
|
||||
// Снятие MOX руками обрывает программную передачу текста: оператор решил
|
||||
// прекратить — ключ обязан отпуститься немедленно.
|
||||
if (not On_) and CWXBusy then CWXAbort;
|
||||
FTransmitting := On_;
|
||||
// 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник.
|
||||
if (not On_) and FTwoTone then
|
||||
@@ -5386,13 +5523,27 @@ begin
|
||||
ActiveMicInput.Flush;
|
||||
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
|
||||
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
|
||||
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
|
||||
// ★Телеграф — исключение: приёмник должен жить, иначе с ним умолкает и
|
||||
// программный сайдтон (он подмешивается в RX-аудио), а при break-in
|
||||
// пропадал бы эфир между посылками. От утечки прикрывает ATT on TX.
|
||||
FDSPEngine.KeepRXDuringTX := FDisplayDuplex or
|
||||
(ActiveTXMode in [MODE_CWL, MODE_CWU]);
|
||||
// Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог
|
||||
// оставить главный режим). Ставим перед стартом TX-потока.
|
||||
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
|
||||
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
|
||||
FDSPEngine.PSSetMox(FTransmitting);
|
||||
FDSPEngine.SetTXRun(FTransmitting);
|
||||
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ (эталон Thetis,
|
||||
// console.cs:29570). В WDSP режим TXA_CWL — буквально ветка SSB, поэтому
|
||||
// без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц.
|
||||
// Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN.
|
||||
// Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки
|
||||
// голос в телеграфном участке недопустим (тогда MOX = только PTT).
|
||||
if FTransmitting and (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||||
and (not FTuning) and (not FTwoTone) then
|
||||
FDSPEngine.SetTXRun(False)
|
||||
else
|
||||
FDSPEngine.SetTXRun(FTransmitting);
|
||||
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
|
||||
begin
|
||||
FDSPEngine.FlushRX;
|
||||
@@ -5439,8 +5590,16 @@ begin
|
||||
if On_ then
|
||||
begin
|
||||
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
|
||||
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
|
||||
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis
|
||||
// (console.cs:31757, ветка cw_fw_keyer && chkTUN).
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||||
begin
|
||||
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
|
||||
FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG)
|
||||
else
|
||||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||||
end;
|
||||
FTuning := True;
|
||||
FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень)
|
||||
ApplyWDSPDrive(FTXSettings.TUNLevel);
|
||||
@@ -5465,6 +5624,131 @@ begin
|
||||
Changed(rfTuning);
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Телеграф
|
||||
// ===========================================================================
|
||||
|
||||
function TRadioController.CWSettings: TCWSettings;
|
||||
begin
|
||||
Result := FCWSettings;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetCWSettings(const C: TCWSettings);
|
||||
// Одна дверь для любой правки телеграфа: pitch уезжает в движок (там он
|
||||
// двигает гетеродин и полосу всех CW-каналов), кейер/сайдтон — в прошивку,
|
||||
// скорость — в поток программной передачи.
|
||||
begin
|
||||
FCWSettings := C;
|
||||
FCWSettings.Pitch := EnsureRange(FCWSettings.Pitch, 200, 1200);
|
||||
FCWSettings.Speed := EnsureRange(FCWSettings.Speed, 5, 60);
|
||||
FCWSettings.Weight := EnsureRange(FCWSettings.Weight, 33, 66);
|
||||
FCWSettings.SidetoneHWLevel := EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127);
|
||||
FCWSettings.SidetoneSWLevel := EnsureRange(FCWSettings.SidetoneSWLevel, 0, 100);
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
|
||||
if Assigned(FCWSender) then
|
||||
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
||||
SendDUCSpecificFromSettings;
|
||||
// Тон TUN в телеграфе равен pitch (несущая обязана лечь на VFO, см. SetTune):
|
||||
// если крутят pitch прямо на настройке — обновляем живьём.
|
||||
if FTuning and FWDSPReady and Assigned(FDSPEngine)
|
||||
and (ActiveTXMode in [MODE_CWL, MODE_CWU]) then
|
||||
FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG);
|
||||
if FDevConnected and Assigned(FSettings) then
|
||||
begin
|
||||
FSettings.SaveCW(FDevMAC, FCWSettings);
|
||||
FSettings.Save;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SyncCWKeyer;
|
||||
// Байт 5 зависит от режима ИСТОЧНИКА передачи, поэтому каждая смена режима или
|
||||
// TX-слайса обязана его перепослать: вне телеграфа он должен стать нулевым,
|
||||
// иначе прошивка поднимет PTT на замыкание ключа посреди SSB.
|
||||
begin
|
||||
if not (ActiveTXMode in [MODE_CWL, MODE_CWU]) then CWXAbort;
|
||||
if CWOptionsByte <> FLastCWOptions then SendDUCSpecificFromSettings;
|
||||
end;
|
||||
|
||||
procedure TRadioController.CWXKeyEvent(Down: Boolean);
|
||||
// Поток передачи, НЕ GUI. Обе операции потокобезопасны: SetCWXKey шлёт HP-пакет
|
||||
// под своим локом бэкенда, FCWKeyDown читает DSP-поток.
|
||||
begin
|
||||
FCWKeyDown := Down;
|
||||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||
FNetwork.SetCWXKey(Down);
|
||||
end;
|
||||
|
||||
function TRadioController.CWXBusy: Boolean;
|
||||
begin
|
||||
Result := Assigned(FCWSender) and FCWSender.Busy;
|
||||
end;
|
||||
|
||||
procedure TRadioController.CWXSend(const Text: string);
|
||||
// Передача текста. Break-in ВКЛ — PTT поднимает прошивка сама (как на ключ);
|
||||
// выключен — оператор держит MOX руками, как при ручном T/R.
|
||||
begin
|
||||
if Text = '' then Exit;
|
||||
if not (ActiveTXMode in [MODE_CWL, MODE_CWU]) then Exit;
|
||||
if not FCWSettings.FWKeyer then Exit;
|
||||
if not Assigned(FCWSender) then
|
||||
FCWSender := TCWSender.Create(Self.CWXKeyEvent);
|
||||
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
||||
FCWSender.Enqueue(Text);
|
||||
end;
|
||||
|
||||
procedure TRadioController.CWXAbort;
|
||||
begin
|
||||
if Assigned(FCWSender) then FCWSender.AbortSending;
|
||||
end;
|
||||
|
||||
function TRadioController.MixSidetone(const Left, Right: array of Single;
|
||||
Count: Integer): Boolean;
|
||||
// DSP-поток. Генератор синуса на pitch с трапецеидальной огибающей — без неё
|
||||
// фронт посылки даёт щелчок в наушниках громче самой посылки. Результат
|
||||
// кладём в свои буферы (входные массивы const), вызывающий отдаёт их дальше.
|
||||
const
|
||||
RAMP_MS = 5;
|
||||
AUDIO_SR = 48000;
|
||||
var
|
||||
i: Integer;
|
||||
Amp, Step, Rate, S: Double;
|
||||
begin
|
||||
Result := False;
|
||||
if not FCWSettings.SidetoneSW then Exit;
|
||||
if FCWSettings.SidetoneSWLevel <= 0 then Exit;
|
||||
// Ключ отпущен и огибающая уже сошлась в ноль — блок трогать незачем.
|
||||
if (not FCWKeyDown) and (FCWSTEnv <= 0.0) then Exit;
|
||||
if Count <= 0 then Exit;
|
||||
if Length(FCWSTBufL) < Count then
|
||||
begin
|
||||
SetLength(FCWSTBufL, Count);
|
||||
SetLength(FCWSTBufR, Count);
|
||||
end;
|
||||
Amp := (FCWSettings.SidetoneSWLevel / 100.0) * 0.7;
|
||||
Step := 2.0 * Pi * FCWSettings.Pitch / AUDIO_SR;
|
||||
Rate := 1000.0 / (RAMP_MS * AUDIO_SR);
|
||||
for i := 0 to Count - 1 do
|
||||
begin
|
||||
if FCWKeyDown then
|
||||
begin
|
||||
FCWSTEnv := FCWSTEnv + Rate;
|
||||
if FCWSTEnv > 1.0 then FCWSTEnv := 1.0;
|
||||
end
|
||||
else
|
||||
begin
|
||||
FCWSTEnv := FCWSTEnv - Rate;
|
||||
if FCWSTEnv < 0.0 then FCWSTEnv := 0.0;
|
||||
end;
|
||||
FCWSTPhase := FCWSTPhase + Step;
|
||||
if FCWSTPhase > 2.0 * Pi then FCWSTPhase := FCWSTPhase - 2.0 * Pi;
|
||||
S := Amp * FCWSTEnv * Sin(FCWSTPhase);
|
||||
FCWSTBufL[i] := EnsureRange(Left[i] + S, -1.0, 1.0);
|
||||
FCWSTBufR[i] := EnsureRange(Right[i] + S, -1.0, 1.0);
|
||||
end;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2)
|
||||
// ===========================================================================
|
||||
@@ -5964,6 +6248,10 @@ begin
|
||||
ApplyAntPorts;
|
||||
// QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено.
|
||||
ApplyRxMuteToEngine;
|
||||
// Тон телеграфа в движок: на момент ConnectDevice канал WDSP мог быть ещё
|
||||
// не открыт, а здесь он гарантированно жив.
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
|
||||
SendDUCSpecificFromSettings;
|
||||
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||||
ApplyTXSettingsToDSP;
|
||||
@@ -6175,6 +6463,9 @@ begin
|
||||
FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache);
|
||||
// TX/Alex/XVTR per-device
|
||||
FSettings.LoadTX(FDevMAC, FTXSettings);
|
||||
FSettings.LoadCW(FDevMAC, FCWSettings);
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
|
||||
FSettings.LoadAlex(FDevMAC, FAlexSettings);
|
||||
FSettings.LoadAnt936x(FDevMAC, FAnt936x);
|
||||
FNetwork.SetAlexConfig(FAlexSettings);
|
||||
|
||||
Reference in New Issue
Block a user