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; // быстрый гейт: без тапов не берём и лок FAudioTapCount: LongInt; // быстрый гейт: без тапов не берём и лок
FIQTap: TIQTapEvent; // тап IQ (TCI): помним ради пересоздания движка FIQTap: TIQTapEvent; // тап IQ (TCI): помним ради пересоздания движка
procedure Changed(Field: TRadioField); procedure Changed(Field: TRadioField);
procedure PushAudioTapsActive;
procedure FireAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer; procedure FireAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
const Left, Right: array of Single; Count: Integer); const Left, Right: array of Single; Count: Integer);
// --- Мультислайсы --- // --- Мультислайсы ---
@@ -911,6 +910,11 @@ type
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode); procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1 procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
procedure SetSliceMute(Id: Integer; Mute: Boolean); 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 procedure SetSliceRxMuteOnTx(Id: Integer; On_: Boolean); // self-monitor слайса на TX
function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана
function PanHasFMSlice(PanId: Integer): Boolean; // есть слайс в канальном режиме (FM/DMR/FMRAW)? function PanHasFMSlice(PanId: Integer): Boolean; // есть слайс в канальном режиме (FM/DMR/FMRAW)?
@@ -1425,7 +1429,9 @@ begin
FDSPEngine.SetDisplayResLimit(FDisplayPixels); FDSPEngine.SetDisplayResLimit(FDisplayPixels);
// Тапы наружу (TCI): их владелец живёт дольше движка. // Тапы наружу (TCI): их владелец живёт дольше движка.
FDSPEngine.SetIQTap(FIQTap); FDSPEngine.SetIQTap(FIQTap);
PushAudioTapsActive; // Послайсовые флаги «этот слайс слушают» сюда не входят: слайсы пересозданный
// движок не наследует вовсе (их заводит заново тот, кто их создавал), а
// владелец потоков расставит флаги на первом же обновлении карты слайсов.
// Декодер маяка QO-100 создаётся позже самого движка (в CreateEngines ниже), // Декодер маяка QO-100 создаётся позже самого движка (в CreateEngines ниже),
// зато при ПЕРЕсоздании движка он уже есть и его надо вернуть на место. // зато при ПЕРЕсоздании движка он уже есть и его надо вернуть на место.
if Assigned(FBeaconDec) then FDSPEngine.SetBeaconDecoder(FBeaconDec); if Assigned(FBeaconDec) then FDSPEngine.SetBeaconDecoder(FBeaconDec);
@@ -1585,7 +1591,6 @@ begin
finally finally
FAudioTapLock.Leave; FAudioTapLock.Leave;
end; end;
PushAudioTapsActive;
end; end;
procedure TRadioController.RemoveAudioTap(T: TRadioAudioTapEvent); procedure TRadioController.RemoveAudioTap(T: TRadioAudioTapEvent);
@@ -1606,15 +1611,6 @@ begin
finally finally
FAudioTapLock.Leave; FAudioTapLock.Leave;
end; end;
PushAudioTapsActive;
end;
procedure TRadioController.PushAudioTapsActive;
// Движку интересен только факт «слушатели есть»: от него зависит, считать ли
// молчащие (mute) слайсы — их звук всё равно уходит в RX_AUDIO по TCI.
begin
if Assigned(FDSPEngine) then
FDSPEngine.SetAudioTapsActive(FAudioTapCount > 0);
end; end;
procedure TRadioController.FireAudioTap(Kind: TRadioAudioKind; procedure TRadioController.FireAudioTap(Kind: TRadioAudioKind;
@@ -2847,6 +2843,11 @@ begin
SliceStateChanged(Id); SliceStateChanged(Id);
end; 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); procedure TRadioController.SetSliceRxMuteOnTx(Id: Integer; On_: Boolean);
var idx: Integer; var idx: Integer;
begin begin
+70 -1
View File
@@ -231,6 +231,8 @@ type
procedure PushIQRate(Client: TTCIClient); // переобъявить её клиенту procedure PushIQRate(Client: TTCIClient); // переобъявить её клиенту
procedure StopAllStreams; procedure StopAllStreams;
procedure SetTaps(On_: Boolean); // поток контроллера procedure SetTaps(On_: Boolean); // поток контроллера
procedure PushTapWants; // поток контроллера: кого из слайсов слушают
procedure WantTaps; // то же, но зовут из любого потока
function HasAudioStream(C: TTCIClient): Boolean; function HasAudioStream(C: TTCIClient): Boolean;
function StreamRxOf(PanId, SliceId: Integer): Integer; // −1 = не наш канал function StreamRxOf(PanId, SliceId: Integer): Integer; // −1 = не наш канал
procedure OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer; procedure OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
@@ -2856,6 +2858,57 @@ begin
finally finally
FStreamLock.Leave; FStreamLock.Leave;
end; 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; end;
procedure TTCIAdapter.StopStream(C: TTCIClient; K: TTCIStreamType; Rx: Integer); procedure TTCIAdapter.StopStream(C: TTCIClient; K: TTCIStreamType; Rx: Integer);
@@ -2869,22 +2922,28 @@ begin
FStreams[i].Free; FStreams[i].Free;
for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1]; for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1];
SetLength(FStreams, Length(FStreams) - 1); SetLength(FStreams, Length(FStreams) - 1);
Exit; Break;
end; end;
finally finally
FStreamLock.Leave; FStreamLock.Leave;
end; end;
// Слушателей у слайса могло не остаться — снимаем с него демодулятор. Строго
// ПОСЛЕ удаления записи: иначе гейт закрылся бы раньше, чем ушёл хвост.
if K = tstRXAudio then WantTaps;
end; end;
procedure TTCIAdapter.DropClientStreams(C: TTCIClient); procedure TTCIAdapter.DropClientStreams(C: TTCIClient);
var i, j: Integer; var i, j: Integer;
Had: Boolean;
begin begin
Had := False;
FStreamLock.Enter; FStreamLock.Enter;
try try
i := 0; i := 0;
while i <= High(FStreams) do while i <= High(FStreams) do
if (FStreams[i] <> nil) and (FStreams[i].Client = C) then if (FStreams[i] <> nil) and (FStreams[i].Client = C) then
begin begin
if FStreams[i].Kind = tstRXAudio then Had := True;
FStreams[i].Free; FStreams[i].Free;
for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1]; for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1];
SetLength(FStreams, Length(FStreams) - 1); SetLength(FStreams, Length(FStreams) - 1);
@@ -2894,6 +2953,7 @@ begin
finally finally
FStreamLock.Leave; FStreamLock.Leave;
end; end;
if Had then WantTaps; // ушедший клиент мог быть последним слушателем слайса
end; end;
procedure TTCIAdapter.RestartStreams(C: TTCIClient; K: TTCIStreamType); procedure TTCIAdapter.RestartStreams(C: TTCIClient; K: TTCIStreamType);
@@ -2945,6 +3005,7 @@ begin
finally finally
FStreamLock.Leave; FStreamLock.Leave;
end; end;
PushTapWants; // уже поток контроллера
end; end;
procedure TTCIAdapter.StopAllStreams; procedure TTCIAdapter.StopAllStreams;
@@ -2958,6 +3019,9 @@ begin
finally finally
FStreamLock.Leave; FStreamLock.Leave;
end; end;
// Потоков не осталось — снимаем демодулятор со всех молчащих слайсов. Зовут
// нас из потока контроллера (ApplySettings, Destroy), Invoke не нужен.
PushTapWants;
ClearTxClient(nil); ClearTxClient(nil);
// Страховка: клиент мог держать эфир и без своего аудио (TRX без 'tci'). // Страховка: клиент мог держать эфир и без своего аудио (TRX без 'tci').
StopTxOf(nil); StopTxOf(nil);
@@ -3628,6 +3692,11 @@ begin
MapChanged := Sig <> SliceMapSig; MapChanged := Sig <> SliceMapSig;
end; end;
// Карта слайсов поехала: слот сменил хозяина, и «этот слайс слушают» обязано
// переехать вместе с ним — слайс, занявший слот, приходит с чистым флагом, а
// на прежнем он остался бы висеть навсегда. Мы в потоке контроллера.
if MapChanged then PushTapWants;
// Границы настройки — тоже до гейта по клиентам: их кэш обязан пережить // Границы настройки — тоже до гейта по клиентам: их кэш обязан пережить
// время, когда не подключён никто (см. PushVfoLimits). // время, когда не подключён никто (см. PushVfoLimits).
if Field in [rfDevice, rfXvtr, rfBand] then PushVfoLimits; if Field in [rfDevice, rfXvtr, rfBand] then PushVfoLimits;
+24 -10
View File
@@ -255,6 +255,7 @@ type
FMSQLevel: Integer; // порог squelch 0..100 FMSQLevel: Integer; // порог squelch 0..100
SMeter: Double; // последнее показание S-метра канала (dBm), пишет DSP-поток SMeter: Double; // последнее показание S-метра канала (dBm), пишет DSP-поток
Opened: Boolean; // WDSP-канал реально открыт (после Open движка) Opened: Boolean; // WDSP-канал реально открыт (после Open движка)
TapWanted: Boolean; // этот слайс слушает внешний потребитель (RX_AUDIO по TCI)
end; end;
// Доп. панадаптер (этап 3): свой DDC-поток (rate 192-768к), свой analyzer // Доп. панадаптер (этап 3): свой DDC-поток (rate 192-768к), свой analyzer
@@ -532,7 +533,6 @@ type
FIQTapOn: Boolean; // дешёвая проверка «тап вообще есть» FIQTapOn: Boolean; // дешёвая проверка «тап вообще есть»
// Кто-то (TCI/web) слушает аудио помимо динамика: тогда молчащий (mute) // Кто-то (TCI/web) слушает аудио помимо динамика: тогда молчащий (mute)
// слайс всё равно надо считать — RX_AUDIO снимается ДО громкости и мьюта. // слайс всё равно надо считать — RX_AUDIO снимается ДО громкости и мьюта.
FAudioTapsActive: Boolean;
function ModeToWDSP(Mode: Integer): Integer; function ModeToWDSP(Mode: Integer): Integer;
function ModeToWDSPTX(Mode: Integer): Integer; function ModeToWDSPTX(Mode: Integer): Integer;
@@ -647,10 +647,14 @@ type
// Тап сырого RX-IQ наружу (потоки IQ по TCI). nil — снять. Ставит и // Тап сырого RX-IQ наружу (потоки IQ по TCI). nil — снять. Ставит и
// снимает поток контроллера; вызывается тап из DSP-потока. // снимает поток контроллера; вызывается тап из DSP-потока.
procedure SetIQTap(T: TIQTapEvent); procedure SetIQTap(T: TIQTapEvent);
// Есть ли слушатели аудио-тапов (TCI/web). Влияет только на молчащие // Слушает ли слайс кто-то снаружи (поток RX_AUDIO по TCI). Влияет только
// слайсы: с ними их всё равно надо считать, иначе клиент TCI остаётся без // на молчащие слайсы: их всё равно надо считать, иначе клиент остаётся без
// RX_AUDIO ровно потому, что оператор убрал звук у себя в комнате. // RX_AUDIO ровно потому, что оператор убрал звук у себя в комнате.
procedure SetAudioTapsActive(V: Boolean); // ★Флаг именно ПОСЛАЙСОВЫЙ. Общий «тапы включены» означал бы, что поднятый
// TCI-сервер заставляет считать ВСЕ молчащие слайсы — и без клиентов, и
// когда клиент качает один слайс из шести, и когда ему нужен только IQ.
// Ставит поток контроллера, читает DSP-поток (оба под FSliceLock).
procedure SetSliceTapWanted(Id: Integer; V: Boolean);
// --- Панадаптеры на аппаратных DDC (этап 3.2) --- // --- Панадаптеры на аппаратных DDC (этап 3.2) ---
// Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer // Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer
@@ -1522,7 +1526,6 @@ begin
// Тап IQ наружу (TCI) // Тап IQ наружу (TCI)
FIQTapLock := TCriticalSection.Create; FIQTapLock := TCriticalSection.Create;
FIQTapOn := False; FIQTapOn := False;
FAudioTapsActive := False;
// Мультислайсы // Мультислайсы
FSliceLock := TCriticalSection.Create; FSliceLock := TCriticalSection.Create;
@@ -2455,10 +2458,6 @@ begin
end; end;
end; end;
procedure TWDSPEngine.SetAudioTapsActive(V: Boolean);
begin
FAudioTapsActive := V;
end;
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte; procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
DataOffset: Integer; IQPairs: Integer); DataOffset: Integer; IQPairs: Integer);
@@ -2977,7 +2976,7 @@ begin
// ★То же и для тапов: RX_AUDIO по TCI снимается ДО громкости и мьюта, и // ★То же и для тапов: RX_AUDIO по TCI снимается ДО громкости и мьюта, и
// молчащий в комнате слайс обязан продолжать отдавать звук клиенту. // молчащий в комнате слайс обязан продолжать отдавать звук клиенту.
if FSlices[s].Muted and not (FSlices[s].Mode in [MODE_DMR, MODE_FMRAW]) if FSlices[s].Muted and not (FSlices[s].Mode in [MODE_DMR, MODE_FMRAW])
and not FAudioTapsActive then Continue; and not FSlices[s].TapWanted then Continue;
// Свежая копия входного IQ — fexchange0 портит вход in-place. // Свежая копия входного IQ — fexchange0 портит вход in-place.
for i := 0 to N - 1 do for i := 0 to N - 1 do
begin begin
@@ -3167,6 +3166,9 @@ begin
FSlices[slot].ANFOn := False; FSlices[slot].ANFOn := False;
FSlices[slot].SMeter := -130.0; FSlices[slot].SMeter := -130.0;
FSlices[slot].Opened := False; FSlices[slot].Opened := False;
// Слушателей у нового слайса нет: их проставит владелец потоков (TCI),
// когда клиент попросит RX_AUDIO именно с него.
FSlices[slot].TapWanted := False;
if FInitialized then OpenSliceChannel(slot); if FInitialized then OpenSliceChannel(slot);
Result := True; Result := True;
finally finally
@@ -3289,6 +3291,18 @@ begin
end; end;
end; end;
procedure TWDSPEngine.SetSliceTapWanted(Id: Integer; V: Boolean);
var idx: Integer;
begin
FSliceLock.Enter;
try
idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].TapWanted := V;
finally
FSliceLock.Leave;
end;
end;
procedure TWDSPEngine.SetSliceMute(Id: Integer; Mute: Boolean); procedure TWDSPEngine.SetSliceMute(Id: Integer; Mute: Boolean);
var idx: Integer; var idx: Integer;
begin begin
+73
View File
@@ -1466,16 +1466,29 @@ type
THost = class THost = class
procedure DoInvoke(M: TThreadMethod); procedure DoInvoke(M: TThreadMethod);
procedure OnTXIQ(const Buf: array of Double; Count: Integer); procedure OnTXIQ(const Buf: array of Double; Count: Integer);
procedure OnTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
const Left, Right: array of Single; Count: Integer);
end; end;
var var
TXIQBlocks: Integer = 0; TXIQBlocks: Integer = 0;
DemodBlocks: Integer = 0; // блоки rakDemod со слайса WatchSlice
WatchSlice: Integer = 0;
procedure THost.DoInvoke(M: TThreadMethod); procedure THost.DoInvoke(M: TThreadMethod);
begin begin
M(); M();
end; end;
procedure THost.OnTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
const Left, Right: array of Single; Count: Integer);
// Наблюдатель за выходом демодулятора слайса. Сам по себе считать слайс не
// заставляет: тап — это слушатель, а не заказчик (см. TapWanted).
begin
if (Kind = rakDemod) and (SliceId = WatchSlice) and (Count > 0) then
Inc(DemodBlocks);
end;
procedure THost.OnTXIQ(const Buf: array of Double; Count: Integer); procedure THost.OnTXIQ(const Buf: array of Double; Count: Integer);
// Готовый блок TX-IQ: единственный наблюдаемый признак того, что аудио // Готовый блок TX-IQ: единственный наблюдаемый признак того, что аудио
// клиента прошло весь тракт (ринг микрофона вычерпывается быстрее, чем его // клиента прошло весь тракт (ринг микрофона вычерпывается быстрее, чем его
@@ -2086,6 +2099,66 @@ begin
end; end;
Check('аудио слайса идёт под номером 1', n > 0, IntToStr(n)); Check('аудио слайса идёт под номером 1', n > 0, IntToStr(n));
// ── ★Мьют слайса и стоимость молчащих слайсов ────────────────────────
// RX_AUDIO снимается ДО громкости и мьюта, поэтому слайс, у которого
// оператор убрал звук в комнате, обязан продолжать кормить клиента. Но
// считается он ровно пока его кто-то качает: разрешение послайсовое
// (TapWanted), а не «поднят TCI-сервер» — иначе шесть молчащих слайсов
// жгли бы WDSP и без единого клиента.
WatchSlice := SliceId;
Ctrl.AddAudioTap(Host.OnTap);
Ctrl.SetSliceMute(SliceId, True);
C.Pump(100);
while C.NextFrame(Op, Pay) do ; // выгребаем хвост
DemodBlocks := 0;
n := 0;
for k := 0 to 40 do
begin
Ctrl.FDSPEngine.PushDDCPacket(Pkt, 0, PAIRS);
Sleep(2);
end;
C.Pump(400);
while C.NextFrame(Op, Pay) do
if (Op = $02) and (Length(Pay) >= SizeOf(H)) then
begin
Move(Pay[0], H, SizeOf(H));
if (H.StreamType = LongWord(Ord(tstRXAudio))) and (H.Receiver = 1) then
Inc(n);
end;
Check('мьют слайса не глушит RX_AUDIO по TCI', n > 0, IntToStr(n));
// Поток убрали — молчащий слайс не должен считаться вовсе. Тап при этом
// остаётся навешенным: он наблюдатель и права требовать демодуляции не
// имеет (раньше именно он её и включал — на все слайсы разом).
C.SendText('audio_stop:1;');
C.Pump(200);
DemodBlocks := 0;
for k := 0 to 40 do
begin
Ctrl.FDSPEngine.PushDDCPacket(Pkt, 0, PAIRS);
Sleep(2);
end;
Sleep(100);
Check('без потока молчащий слайс не считается', DemodBlocks = 0,
IntToStr(DemodBlocks));
// И обратно: попросили поток — слайс снова в работе.
C.SendText('audio_start:1;');
C.Pump(200);
DemodBlocks := 0;
for k := 0 to 40 do
begin
Ctrl.FDSPEngine.PushDDCPacket(Pkt, 0, PAIRS);
Sleep(2);
end;
Sleep(100);
Check('поток вернулся — слайс снова считается', DemodBlocks > 0,
IntToStr(DemodBlocks));
Ctrl.RemoveAudioTap(Host.OnTap);
Ctrl.SetSliceMute(SliceId, False);
C.Pump(200);
while C.NextFrame(Op, Pay) do ;
// ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ───────── // ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ─────────
// MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает // MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает
// ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if // ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if