diff --git a/RadioController.pas b/RadioController.pas index 520bbcc..9ab96cd 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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 diff --git a/TCIAdapter.pas b/TCIAdapter.pas index e762c95..c4cd8b2 100644 --- a/TCIAdapter.pas +++ b/TCIAdapter.pas @@ -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; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index b867dea..6e520a2 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -255,6 +255,7 @@ type FMSQLevel: Integer; // порог squelch 0..100 SMeter: Double; // последнее показание S-метра канала (dBm), пишет DSP-поток Opened: Boolean; // WDSP-канал реально открыт (после Open движка) + TapWanted: Boolean; // этот слайс слушает внешний потребитель (RX_AUDIO по TCI) end; // Доп. панадаптер (этап 3): свой DDC-поток (rate 192-768к), свой analyzer @@ -532,7 +533,6 @@ type FIQTapOn: Boolean; // дешёвая проверка «тап вообще есть» // Кто-то (TCI/web) слушает аудио помимо динамика: тогда молчащий (mute) // слайс всё равно надо считать — RX_AUDIO снимается ДО громкости и мьюта. - FAudioTapsActive: Boolean; function ModeToWDSP(Mode: Integer): Integer; function ModeToWDSPTX(Mode: Integer): Integer; @@ -647,10 +647,14 @@ type // Тап сырого RX-IQ наружу (потоки IQ по TCI). nil — снять. Ставит и // снимает поток контроллера; вызывается тап из DSP-потока. procedure SetIQTap(T: TIQTapEvent); - // Есть ли слушатели аудио-тапов (TCI/web). Влияет только на молчащие - // слайсы: с ними их всё равно надо считать, иначе клиент TCI остаётся без + // Слушает ли слайс кто-то снаружи (поток RX_AUDIO по TCI). Влияет только + // на молчащие слайсы: их всё равно надо считать, иначе клиент остаётся без // RX_AUDIO ровно потому, что оператор убрал звук у себя в комнате. - procedure SetAudioTapsActive(V: Boolean); + // ★Флаг именно ПОСЛАЙСОВЫЙ. Общий «тапы включены» означал бы, что поднятый + // TCI-сервер заставляет считать ВСЕ молчащие слайсы — и без клиентов, и + // когда клиент качает один слайс из шести, и когда ему нужен только IQ. + // Ставит поток контроллера, читает DSP-поток (оба под FSliceLock). + procedure SetSliceTapWanted(Id: Integer; V: Boolean); // --- Панадаптеры на аппаратных DDC (этап 3.2) --- // Создаёт доп. пан PanId (1..MAX_PANS-1): аккумулятор + analyzer @@ -1522,7 +1526,6 @@ begin // Тап IQ наружу (TCI) FIQTapLock := TCriticalSection.Create; FIQTapOn := False; - FAudioTapsActive := False; // Мультислайсы FSliceLock := TCriticalSection.Create; @@ -2455,10 +2458,6 @@ begin end; end; -procedure TWDSPEngine.SetAudioTapsActive(V: Boolean); -begin - FAudioTapsActive := V; -end; procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte; DataOffset: Integer; IQPairs: Integer); @@ -2977,7 +2976,7 @@ begin // ★То же и для тапов: RX_AUDIO по TCI снимается ДО громкости и мьюта, и // молчащий в комнате слайс обязан продолжать отдавать звук клиенту. 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. for i := 0 to N - 1 do begin @@ -3167,6 +3166,9 @@ begin FSlices[slot].ANFOn := False; FSlices[slot].SMeter := -130.0; FSlices[slot].Opened := False; + // Слушателей у нового слайса нет: их проставит владелец потоков (TCI), + // когда клиент попросит RX_AUDIO именно с него. + FSlices[slot].TapWanted := False; if FInitialized then OpenSliceChannel(slot); Result := True; finally @@ -3289,6 +3291,18 @@ begin 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); var idx: Integer; begin diff --git a/test/tci/tcitest.pas b/test/tci/tcitest.pas index 88ec04d..a59e03a 100644 --- a/test/tci/tcitest.pas +++ b/test/tci/tcitest.pas @@ -1466,16 +1466,29 @@ type THost = class procedure DoInvoke(M: TThreadMethod); 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; var - TXIQBlocks: Integer = 0; + TXIQBlocks: Integer = 0; + DemodBlocks: Integer = 0; // блоки rakDemod со слайса WatchSlice + WatchSlice: Integer = 0; procedure THost.DoInvoke(M: TThreadMethod); begin M(); 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); // Готовый блок TX-IQ: единственный наблюдаемый признак того, что аудио // клиента прошло весь тракт (ринг микрофона вычерпывается быстрее, чем его @@ -2086,6 +2099,66 @@ begin end; 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 со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ───────── // MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает // ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if