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
+13 -12
View File
@@ -251,7 +251,6 @@ type
FAudioTapCount: LongInt; // быстрый гейт: без тапов не берём и лок
FIQTap: TIQTapEvent; // тап IQ (TCI): помним ради пересоздания движка
procedure Changed(Field: TRadioField);
procedure PushAudioTapsActive;
procedure FireAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
const Left, Right: array of Single; Count: Integer);
// --- Мультислайсы ---
@@ -911,6 +910,11 @@ type
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
procedure SetSliceMute(Id: Integer; Mute: Boolean);
// Слайс слушает внешний потребитель (поток RX_AUDIO по TCI): молчащий в
// комнате слайс обязан считаться, пока его кто-то качает по сети. Зеркала
// в TCtrlSlice нет намеренно — флаг принадлежит владельцу потоков, а не
// модели, и пересоздание движка слайсы всё равно не переживают.
procedure SetSliceTapWanted(Id: Integer; V: Boolean);
procedure SetSliceRxMuteOnTx(Id: Integer; On_: Boolean); // self-monitor слайса на TX
function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана
function PanHasFMSlice(PanId: Integer): Boolean; // есть слайс в канальном режиме (FM/DMR/FMRAW)?
@@ -1425,7 +1429,9 @@ begin
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
// Тапы наружу (TCI): их владелец живёт дольше движка.
FDSPEngine.SetIQTap(FIQTap);
PushAudioTapsActive;
// Послайсовые флаги «этот слайс слушают» сюда не входят: слайсы пересозданный
// движок не наследует вовсе (их заводит заново тот, кто их создавал), а
// владелец потоков расставит флаги на первом же обновлении карты слайсов.
// Декодер маяка QO-100 создаётся позже самого движка (в CreateEngines ниже),
// зато при ПЕРЕсоздании движка он уже есть и его надо вернуть на место.
if Assigned(FBeaconDec) then FDSPEngine.SetBeaconDecoder(FBeaconDec);
@@ -1585,7 +1591,6 @@ begin
finally
FAudioTapLock.Leave;
end;
PushAudioTapsActive;
end;
procedure TRadioController.RemoveAudioTap(T: TRadioAudioTapEvent);
@@ -1606,15 +1611,6 @@ begin
finally
FAudioTapLock.Leave;
end;
PushAudioTapsActive;
end;
procedure TRadioController.PushAudioTapsActive;
// Движку интересен только факт «слушатели есть»: от него зависит, считать ли
// молчащие (mute) слайсы — их звук всё равно уходит в RX_AUDIO по TCI.
begin
if Assigned(FDSPEngine) then
FDSPEngine.SetAudioTapsActive(FAudioTapCount > 0);
end;
procedure TRadioController.FireAudioTap(Kind: TRadioAudioKind;
@@ -2847,6 +2843,11 @@ begin
SliceStateChanged(Id);
end;
procedure TRadioController.SetSliceTapWanted(Id: Integer; V: Boolean);
begin
if Assigned(FDSPEngine) then FDSPEngine.SetSliceTapWanted(Id, V);
end;
procedure TRadioController.SetSliceRxMuteOnTx(Id: Integer; On_: Boolean);
var idx: Integer;
begin