mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(tci): этап 2 — бинарные потоки IQ/аудио, TX-аудио и запись линейного выхода
Реализованы все четыре потока §3.4 и рекордер:
• RX_AUDIO_STREAM — тап ДО громкости и мьюта (OnDemodAudioReady): скиммеру
и цифре нужен звук приёмника, а не то, что осталось после ручки;
• LINEOUT_STREAM — тап ПОСЛЕ (OnAudioReady), то есть что слышно;
• IQ_STREAM — тап сырого IQ в движке, ОДИН вызов на накопленный блок;
• TX_AUDIO_STREAM + TX_CHRONO — TRX:0,true,tci берёт модуляцию из потока
клиента (флаг TCIMicRequested впереди web в SetMOX), маркеры времени идут
из тика по часам, аудио клиента разворачивается в 48 кГц моно в тот же
ринг, что и web-микрофон;
• LINE_OUT_RECORDER_* — кольцо int16 на приёмник, WAV пишет отдельный поток.
Тапы аудио в контроллере многоадресные (AddAudioTap): слушают, ничего не
забирая, в отличие от OnAudioConsume, которым владеет web. Блоки нарезает и
раскладывает по кольцам клиентов сам DSP-поток, в сокет пишет поток клиента —
та же дисциплина, что у команд. Порядок локов везде FSliceLock → FStreamLock.
У очереди команд и кольца блоков разная политика переполнения: команду терять
нельзя, блок потока — можно (теряется самый старый).
Пересчёт частоты многоступенчатый (TCIStreams). Одноступенчатый FIR на верхнем
пресете Pluto (5760 кГц, коэффициент 120) упирался в потолок отводов и давал
завал 1.3 дБ в полосе при подавлении зеркала 16 дБ — то есть поток IQ с
мусором. Теперь коэффициент раскладывается на множители, спецификацию фильтра
каждой ступени задаёт ИТОГОВАЯ полоса, а свёртка идёт со сложением
симметричных пар: −83 дБ на любом коэффициенте, ≈10% ядра на 5.76 МГц.
Согласование частот с железом: из пресетов Pluto 576 и 960 кГц на 384 не
делятся, поэтому отдаём наибольшую ЗАКОННУЮ частоту, делящую источник нацело
(576/960 → 192 кГц). Ответ на IQ_SAMPLERATE называет достижимое, а не просьбу
клиента, и переобъявляется без запроса при смене rate и устройства.
Приёмный буфер соединения 4 → 32 КБ: блок TX-аудио это 64 байта заголовка плюс
data[16384], а кадр крупнее буфера не собирается никогда.
Настройки TCI переехали из Advanced на вкладку CAT, справа от TCP CAT Server:
это такой же канал внешнего управления трансивером.
Стенд (scratchpad, tcitest.pas): 112 проверок, все зелёные — включая сквозной
прогон через живой WDSP (синтетический IQ → блоки RX-аудио и IQ у настоящего
WS-клиента, и обратно TX-аудио клиента → блоки TX-IQ).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -203,6 +203,14 @@ type
|
||||
TOnTXIQReady = procedure(const Buf: array of Double; Count: Integer) of object;
|
||||
TOnWaterfallReady = procedure(const Pixels: array of Single;
|
||||
Count: Integer) of object;
|
||||
// Тап сырого RX-IQ (потоки IQ по TCI, §3.4). Зовётся из DSP-потока ОДИН РАЗ
|
||||
// на накопленный блок — не на сэмпл: сэмплов тут до 384 тысяч в секунду на
|
||||
// каждый пан, и вызов метода на каждый стоил бы дороже самой работы.
|
||||
// PanId: 0 = главный тракт, 1.. = доп. пан. I/Q — два массива по N отсчётов,
|
||||
// живущих только на время вызова.
|
||||
TIQTapEvent = procedure(PanId: Integer; PI_, PQ_: PDouble;
|
||||
N, RateHz: Integer) of object;
|
||||
|
||||
// Аудио готового слайса — вызывается из DSP-потока по каждому активному слайсу.
|
||||
// SliceId — логический id (из RadioController), не WDSP-канал.
|
||||
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
|
||||
@@ -515,6 +523,7 @@ type
|
||||
FVolume: Double;
|
||||
FLastError: string;
|
||||
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
|
||||
FIQTap: TIQTapEvent; // тап сырого IQ наружу (TCI), не владеет
|
||||
|
||||
function ModeToWDSP(Mode: Integer): Integer;
|
||||
function ModeToWDSPTX(Mode: Integer): Integer;
|
||||
@@ -626,6 +635,10 @@ type
|
||||
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
|
||||
procedure SetBeaconDecoder(D: TBeaconDecoder);
|
||||
|
||||
// Тап сырого RX-IQ наружу (потоки IQ по TCI). nil — снять. Ставит и
|
||||
// снимает поток контроллера; вызывается тап из DSP-потока.
|
||||
procedure SetIQTap(T: TIQTapEvent);
|
||||
|
||||
// --- Панадаптеры на аппаратных DDC (этап 3.2) ---
|
||||
// Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer
|
||||
// PAN_DISP_BASE+PanId. RateHz — rate его DDC, кратен FAudioRate.
|
||||
@@ -2410,6 +2423,11 @@ begin
|
||||
if (D <> nil) and (FSampleRate > 0) then D.Configure(FSampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetIQTap(T: TIQTapEvent);
|
||||
begin
|
||||
FIQTap := T;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
|
||||
DataOffset: Integer; IQPairs: Integer);
|
||||
// Вызывается из СЕТЕВОГО потока — только кладём в очередь и возвращаемся немедленно
|
||||
@@ -2522,6 +2540,12 @@ begin
|
||||
if Pan^.AccPos >= Pan^.BufSize then
|
||||
begin
|
||||
Pan^.AccPos := 0;
|
||||
// Тап IQ пана — как у главного тракта, один вызов на блок. Он идёт
|
||||
// ПОД FSliceLock (весь разбор пакета пана здесь), поэтому обработчик
|
||||
// обязан только скопировать данные и вернуться: любое ожидание тут
|
||||
// остановит DSP-поток вместе со всеми панами.
|
||||
if Assigned(FIQTap) then
|
||||
FIQTap(PanId, @Pan^.AccI[0], @Pan^.AccQ[0], Pan^.BufSize, Pan^.Rate);
|
||||
if Assigned(FOnSliceAudio) or Assigned(FOnSliceDemodAudio) then
|
||||
try
|
||||
ProcessSlicesFor(PanId, Pan^.AccI, Pan^.AccQ, Pan^.BufSize);
|
||||
@@ -2638,7 +2662,14 @@ begin
|
||||
Inc(FRXAccPos);
|
||||
|
||||
if FRXAccPos >= FBufSize then
|
||||
begin
|
||||
// Тап IQ наружу — по накопленному блоку и ДО обработки: fexchange0
|
||||
// забирает аккумулятор как вход и не портит его, но полагаться на это
|
||||
// незачем, а один вызов на блок вместо вызова на сэмпл экономит всё.
|
||||
if Assigned(FIQTap) then
|
||||
FIQTap(0, @FRXAccI[0], @FRXAccQ[0], FBufSize, FSampleRate);
|
||||
ProcessRXBlock;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user