perf(dsp): молчащие слайсы считались от одного факта, что поднят TCI-сервер

RX_AUDIO по TCI снимается ДО громкости и мьюта, поэтому замьюченный слайс
обязан продолжать считаться, пока его кто-то качает по сети — иначе клиент
остаётся без звука ровно потому, что оператор убрал его у себя в комнате. Гейт
в ProcessSlicesFor это и делал, но спрашивал ГЛОБАЛЬНЫЙ флаг «тапы аудио
установлены», а его взводит SetTaps(True) на старте сервера. То есть галка
«Enable TCI server» одна, без единого клиента, заставляла гонять полный
fexchange0 по каждому молчащему слайсу — до шести штук, ~47 блоков в секунду на
каждый, плюс два входа в критические секции на блок в DSP-потоке. Клиент,
качающий один слайс из шести или вообще только IQ, оплачивал остальные так же.

Флаг стал ПОСЛАЙСОВЫМ (TSliceChan.TapWanted, SetSliceTapWanted). Кто его
выставляет — владелец потоков: TTCIAdapter.PushTapWants полностью пересчитывает
картину по FStreams и раскладывает по слотам. Считаются только потоки RX_AUDIO:
LINE_OUT и рекордер снимаются ПОСЛЕ мьюта, и на молчащем слайсе им достаётся
тишина независимо от того, посчитали его или нет.

Пересчёт зовётся отовсюду, где множество потоков или карта слайсов меняются:
StartStream (синхронно и ДО ответа клиенту — тогда не теряется ни один блок),
StopStream, DropClientStreams, DropDeadRxStreams, StopAllStreams и OnState по
MapChanged — слот мог сменить хозяина, и разрешение обязано переехать вместе с
ним. Список слушателей снимается под FStreamLock, и лок отпускается ДО похода в
движок: DSP-поток входит в тап, уже держа FSliceLock движка, и берёт в нём
FStreamLock — обратный порядок дал бы клин.

Глобальные FAudioTapsActive/SetAudioTapsActive/PushAudioTapsActive удалены.
FAudioTapCount остаётся: он по-прежнему гейтит сам вызов тапов (FireAudioTap) и
сборку звука DMR. Из AttachEngineCallbacks флаг не восстанавливается намеренно —
слайсы пересозданный движок не наследует вовсе.

Заодно: StopStream завершал перебор через Exit прямо из-под try, минуя хвост
процедуры, — заменено на Break.

Стенд (+3 проверки, негативный контроль в обе стороны): мьют слайса не глушит
RX_AUDIO по TCI; без потока молчащий слайс не считается вовсе (наблюдатель —
обычный аудио-тап, который теперь права требовать демодуляции не имеет);
попросили поток снова — слайс снова в работе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 16:07:25 +03:00
co-authored by Claude Opus 5
parent 3a5dced17c
commit c2fa1da2b0
4 changed files with 181 additions and 24 deletions
+70 -1
View File
@@ -231,6 +231,8 @@ type
procedure PushIQRate(Client: TTCIClient); // переобъявить её клиенту
procedure StopAllStreams;
procedure SetTaps(On_: Boolean); // поток контроллера
procedure PushTapWants; // поток контроллера: кого из слайсов слушают
procedure WantTaps; // то же, но зовут из любого потока
function HasAudioStream(C: TTCIClient): Boolean;
function StreamRxOf(PanId, SliceId: Integer): Integer; // −1 = не наш канал
procedure OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
@@ -2856,6 +2858,57 @@ begin
finally
FStreamLock.Leave;
end;
// Слайс мог быть замьючен: без этого первый же блок для клиента не посчитался
// бы вовсе. Синхронно и ДО ответа клиенту — тогда не теряется ни один блок.
if K = tstRXAudio then WantTaps;
end;
procedure TTCIAdapter.PushTapWants;
// Поток контроллера. Кому из слайсов нужен демодулятор, даже когда оператор
// убрал у него звук: ровно тем, на кого есть живой поток RX_AUDIO.
//
// ★Считаем только RX_AUDIO. LINE_OUT и рекордер снимаются ПОСЛЕ мьюта (см.
// ProcessSlicesFor: FOnSliceAudio идёт за «if Muted then Continue»), поэтому на
// молчащем слайсе им и так достаётся тишина — считать его ради них незачем.
// Главный тракт (Rx 0) этим гейтом не управляется вовсе: он не слайс.
//
// ★Список слушателей снимаем под FStreamLock и отпускаем его ДО похода в
// движок: DSP-поток входит в тап, уже держа FSliceLock движка, и берёт в нём
// FStreamLock — противоположный порядок здесь дал бы классический клин.
var
Want: array[0..MAX_SLICES-1] of Boolean;
i, Rx, Id: Integer;
begin
if FController = nil then Exit;
FillChar(Want, SizeOf(Want), 0);
FStreamLock.Enter;
try
for i := 0 to High(FStreams) do
if (FStreams[i] <> nil) and (FStreams[i].Kind = tstRXAudio) then
begin
Rx := FStreams[i].Rx;
if (Rx >= 1) and (Rx <= MAX_SLICES) then Want[Rx - 1] := True;
end;
finally
FStreamLock.Leave;
end;
// Номер приёмника — это слот слайса плюс один, поэтому идём по слотам: карта
// могла и поехать (слайс удалили — следующие сдвинулись), и флаг обязан
// сняться со старого хозяина слота ровно так же, как встать на нового.
for i := 0 to MAX_SLICES - 1 do
begin
Id := FController.SliceIdBySlot(i);
if Id > 0 then FController.SetSliceTapWanted(Id, Want[i]);
end;
end;
procedure TTCIAdapter.WantTaps;
// Потоки заводят и гасят потоки клиентов, а таблицу слайсов трогает только
// поток контроллера — поэтому через Invoke. CanInvoke=False бывает ровно на
// остановке сервера, и останавливает его как раз он: зовём напрямую.
begin
if FController = nil then Exit;
if CanInvoke then FController.Invoke(PushTapWants) else PushTapWants;
end;
procedure TTCIAdapter.StopStream(C: TTCIClient; K: TTCIStreamType; Rx: Integer);
@@ -2869,22 +2922,28 @@ begin
FStreams[i].Free;
for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1];
SetLength(FStreams, Length(FStreams) - 1);
Exit;
Break;
end;
finally
FStreamLock.Leave;
end;
// Слушателей у слайса могло не остаться — снимаем с него демодулятор. Строго
// ПОСЛЕ удаления записи: иначе гейт закрылся бы раньше, чем ушёл хвост.
if K = tstRXAudio then WantTaps;
end;
procedure TTCIAdapter.DropClientStreams(C: TTCIClient);
var i, j: Integer;
Had: Boolean;
begin
Had := False;
FStreamLock.Enter;
try
i := 0;
while i <= High(FStreams) do
if (FStreams[i] <> nil) and (FStreams[i].Client = C) then
begin
if FStreams[i].Kind = tstRXAudio then Had := True;
FStreams[i].Free;
for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1];
SetLength(FStreams, Length(FStreams) - 1);
@@ -2894,6 +2953,7 @@ begin
finally
FStreamLock.Leave;
end;
if Had then WantTaps; // ушедший клиент мог быть последним слушателем слайса
end;
procedure TTCIAdapter.RestartStreams(C: TTCIClient; K: TTCIStreamType);
@@ -2945,6 +3005,7 @@ begin
finally
FStreamLock.Leave;
end;
PushTapWants; // уже поток контроллера
end;
procedure TTCIAdapter.StopAllStreams;
@@ -2958,6 +3019,9 @@ begin
finally
FStreamLock.Leave;
end;
// Потоков не осталось — снимаем демодулятор со всех молчащих слайсов. Зовут
// нас из потока контроллера (ApplySettings, Destroy), Invoke не нужен.
PushTapWants;
ClearTxClient(nil);
// Страховка: клиент мог держать эфир и без своего аудио (TRX без 'tci').
StopTxOf(nil);
@@ -3628,6 +3692,11 @@ begin
MapChanged := Sig <> SliceMapSig;
end;
// Карта слайсов поехала: слот сменил хозяина, и «этот слайс слушают» обязано
// переехать вместе с ним — слайс, занявший слот, приходит с чистым флагом, а
// на прежнем он остался бы висеть навсегда. Мы в потоке контроллера.
if MapChanged then PushTapWants;
// Границы настройки — тоже до гейта по клиентам: их кэш обязан пережить
// время, когда не подключён никто (см. PushVfoLimits).
if Field in [rfDevice, rfXvtr, rfBand] then PushVfoLimits;