mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
fix(tci): дефекты живого прогона — length аудио, маршруты тапов, MOX, рекордер, EOF сокета
Восемь дефектов, найденных прогоном настоящего TCI-клиента (три приёмника: NFM, DIGU, FMRAW) и его отчётом. 1. UI доп. панорам не перерисовывался: rfSliceState рассылался, но ветки в MainForm.OnControllerState не было (частоту несёт отдельный rfSliceFreq). 2. Stream.length у аудио — сэмплы НА КАНАЛ (§4.3), у IQ — вещественные отсчёты (§3.4: комплексных = length/channels). Было ×каналы везде, у стерео получалось вдвое больше. Развилка в TCIFillHeader + разбор TX-аудио в HandleBinary. 3+4. Дыры в маршрутах аудио движка: demod-тап звался только для DMR/FMRAW (у DIGU не было RX_AUDIO), а пост-громкостный — только для нецифровых (у FMRAW не было LINEOUT). Плюс мьют слайса больше не убивает RX_AUDIO: движку сообщают SetAudioTapsActive. 5. Клиент, поставивший TRX, уходил — MOX оставался. FTrxOwner + StopTxOf; TCIMicRequested снимается и по окончании любой передачи. 6. Гонка снятия IQ-тапа: SetIQTap(nil) возвращался раньше, чем DSP-поток выходил из вызова. FIQTapLock (порядок FSliceLock → FIQTapLock). 7. Рекордер был кольцом «последние N секунд», а §4.3 говорит про МАКСИМАЛЬНОЕ время записи с удалением по истечении. Переделан в линейный буфер с окном по часам от START. 8. TCIServer.HandleClient считал recv = 0 таймаутом: ноль — это EOF, errno при нём не трогается и несёт EAGAIN от прошлого истёкшего TCI_POLL_MS. Обычный TCP-разрыв без close-кадра не освобождал слот до остановки сервера, и после нескольких аварийных отключений новые клиенты упирались в TCI_MAX_CLIENTS. Теперь R = 0 рвёт связь безусловно, errno спрашивается только при R < 0. Попутно: MainForm.RecreateDSPEngine (смена sample rate до START) терял внутренние колбэки контроллера — введён AttachEngineCallbacks. Стенд test/tci заведён в репозиторий (run.sh, 126/126 зелёных, включая сквозной прогон через живой WDSP), доп. проверки на оба пути отключения клиента. doc/TCI.md приведена в соответствие: правило про recv = 0 в §1.1, единицы Stream.length, линейный буфер рекордера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+72
-14
@@ -524,6 +524,15 @@ type
|
||||
FLastError: string;
|
||||
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
|
||||
FIQTap: TIQTapEvent; // тап сырого IQ наружу (TCI), не владеет
|
||||
// ★Лок тапа. Снять тап обязан тот, кто вот-вот освободит его владельца
|
||||
// (адаптер TCI), а зовёт тап DSP-поток. Без лока SetIQTap(nil) возвращался
|
||||
// раньше, чем DSP-поток выходил из вызова, — и следующая же строка
|
||||
// деструктора адаптера убивала объект прямо под ним.
|
||||
FIQTapLock: TCriticalSection;
|
||||
FIQTapOn: Boolean; // дешёвая проверка «тап вообще есть»
|
||||
// Кто-то (TCI/web) слушает аудио помимо динамика: тогда молчащий (mute)
|
||||
// слайс всё равно надо считать — RX_AUDIO снимается ДО громкости и мьюта.
|
||||
FAudioTapsActive: Boolean;
|
||||
|
||||
function ModeToWDSP(Mode: Integer): Integer;
|
||||
function ModeToWDSPTX(Mode: Integer): Integer;
|
||||
@@ -638,6 +647,10 @@ type
|
||||
// Тап сырого RX-IQ наружу (потоки IQ по TCI). nil — снять. Ставит и
|
||||
// снимает поток контроллера; вызывается тап из DSP-потока.
|
||||
procedure SetIQTap(T: TIQTapEvent);
|
||||
// Есть ли слушатели аудио-тапов (TCI/web). Влияет только на молчащие
|
||||
// слайсы: с ними их всё равно надо считать, иначе клиент TCI остаётся без
|
||||
// RX_AUDIO ровно потому, что оператор убрал звук у себя в комнате.
|
||||
procedure SetAudioTapsActive(V: Boolean);
|
||||
|
||||
// --- Панадаптеры на аппаратных DDC (этап 3.2) ---
|
||||
// Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer
|
||||
@@ -1506,6 +1519,11 @@ begin
|
||||
FBcnPixCount := 0;
|
||||
SetLength(FBcnDispBuf, DISPLAY_BLOCK_SIZE * 2);
|
||||
|
||||
// Тап IQ наружу (TCI)
|
||||
FIQTapLock := TCriticalSection.Create;
|
||||
FIQTapOn := False;
|
||||
FAudioTapsActive := False;
|
||||
|
||||
// Мультислайсы
|
||||
FSliceLock := TCriticalSection.Create;
|
||||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||||
@@ -1606,6 +1624,7 @@ begin
|
||||
FSliceLock.Free;
|
||||
FMainLock.Free;
|
||||
FBcnLock.Free;
|
||||
FIQTapLock.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
@@ -2424,8 +2443,21 @@ begin
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetIQTap(T: TIQTapEvent);
|
||||
// Поток контроллера. Под локом: снятие обязано ДОЖДАТЬСЯ выхода DSP-потока из
|
||||
// вызова — сразу после нас владельца тапа освобождают.
|
||||
begin
|
||||
FIQTap := T;
|
||||
FIQTapLock.Enter;
|
||||
try
|
||||
FIQTap := T;
|
||||
FIQTapOn := Assigned(T);
|
||||
finally
|
||||
FIQTapLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetAudioTapsActive(V: Boolean);
|
||||
begin
|
||||
FAudioTapsActive := V;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
|
||||
@@ -2543,9 +2575,19 @@ begin
|
||||
// Тап IQ пана — как у главного тракта, один вызов на блок. Он идёт
|
||||
// ПОД FSliceLock (весь разбор пакета пана здесь), поэтому обработчик
|
||||
// обязан только скопировать данные и вернуться: любое ожидание тут
|
||||
// остановит DSP-поток вместе со всеми панами.
|
||||
if Assigned(FIQTap) then
|
||||
FIQTap(PanId, @Pan^.AccI[0], @Pan^.AccQ[0], Pan^.BufSize, Pan^.Rate);
|
||||
// остановит DSP-поток вместе со всеми панами. Порядок локов
|
||||
// FSliceLock → FIQTapLock; обратного нет нигде (SetIQTap не берёт
|
||||
// FSliceLock), так что клина не выходит.
|
||||
if FIQTapOn then
|
||||
begin
|
||||
FIQTapLock.Enter;
|
||||
try
|
||||
if Assigned(FIQTap) then
|
||||
FIQTap(PanId, @Pan^.AccI[0], @Pan^.AccQ[0], Pan^.BufSize, Pan^.Rate);
|
||||
finally
|
||||
FIQTapLock.Leave;
|
||||
end;
|
||||
end;
|
||||
if Assigned(FOnSliceAudio) or Assigned(FOnSliceDemodAudio) then
|
||||
try
|
||||
ProcessSlicesFor(PanId, Pan^.AccI, Pan^.AccQ, Pan^.BufSize);
|
||||
@@ -2666,8 +2708,17 @@ begin
|
||||
// Тап IQ наружу — по накопленному блоку и ДО обработки: fexchange0
|
||||
// забирает аккумулятор как вход и не портит его, но полагаться на это
|
||||
// незачем, а один вызов на блок вместо вызова на сэмпл экономит всё.
|
||||
if Assigned(FIQTap) then
|
||||
FIQTap(0, @FRXAccI[0], @FRXAccQ[0], FBufSize, FSampleRate);
|
||||
// Под FIQTapLock: см. SetIQTap.
|
||||
if FIQTapOn then
|
||||
begin
|
||||
FIQTapLock.Enter;
|
||||
try
|
||||
if Assigned(FIQTap) then
|
||||
FIQTap(0, @FRXAccI[0], @FRXAccQ[0], FBufSize, FSampleRate);
|
||||
finally
|
||||
FIQTapLock.Leave;
|
||||
end;
|
||||
end;
|
||||
ProcessRXBlock;
|
||||
end;
|
||||
end;
|
||||
@@ -2923,7 +2974,10 @@ begin
|
||||
if not (FSlices[s].Active and FSlices[s].Opened) then Continue;
|
||||
if FSlices[s].PanId <> PanId then Continue;
|
||||
// Muting a DMR speaker must not make its decoder lose synchronization.
|
||||
if FSlices[s].Muted and not (FSlices[s].Mode in [MODE_DMR, MODE_FMRAW]) then Continue;
|
||||
// ★То же и для тапов: RX_AUDIO по TCI снимается ДО громкости и мьюта, и
|
||||
// молчащий в комнате слайс обязан продолжать отдавать звук клиенту.
|
||||
if FSlices[s].Muted and not (FSlices[s].Mode in [MODE_DMR, MODE_FMRAW])
|
||||
and not FAudioTapsActive then Continue;
|
||||
// Свежая копия входного IQ — fexchange0 портит вход in-place.
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
@@ -2939,13 +2993,17 @@ begin
|
||||
FSliceOutL[k] := FSliceOut[k * 2];
|
||||
FSliceOutR[k] := FSliceOut[k * 2 + 1];
|
||||
end;
|
||||
if FSlices[s].Mode in [MODE_DMR, MODE_FMRAW] then
|
||||
begin
|
||||
if Assigned(FOnSliceDemodAudio) then
|
||||
FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR,
|
||||
FAudioBufSize);
|
||||
Continue; // decoder-grade signal uses the independent pre-volume route
|
||||
end;
|
||||
// Отвод ДО громкости и мьюта — у КАЖДОГО слайса, а не только у цифровых.
|
||||
// Это выход демодулятора: им кормятся декодеры (DMR/FM RAW) и он же уходит
|
||||
// в RX_AUDIO по TCI. Пока вызов стоял только в цифровой ветке, клиент на
|
||||
// обычном слайсе (DIGU у скиммера) не получал аудиопоток вовсе.
|
||||
if Assigned(FOnSliceDemodAudio) then
|
||||
FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize);
|
||||
// Цифровым дальше нельзя: их звук идёт мимо громкости и мьюта (см. выше).
|
||||
if FSlices[s].Mode in [MODE_DMR, MODE_FMRAW] then Continue;
|
||||
// Мьют — это тишина в динамике и в линейном выходе; сюда мы дошли только
|
||||
// ради тапа выше.
|
||||
if FSlices[s].Muted then Continue;
|
||||
if Assigned(FOnSliceAudio) then
|
||||
begin
|
||||
V := FSlices[s].Volume;
|
||||
|
||||
Reference in New Issue
Block a user