mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 04:47:35 +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:
+71
-17
@@ -249,7 +249,9 @@ type
|
||||
FAudioTaps: array of TRadioAudioTapEvent;
|
||||
FAudioTapLock: TCriticalSection;
|
||||
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);
|
||||
// --- Мультислайсы ---
|
||||
@@ -627,6 +629,11 @@ type
|
||||
// FreeEngines закрывает и освобождает их; вызывается из Destroy.
|
||||
procedure CreateEngines(ASampleRate: Integer);
|
||||
procedure FreeEngines;
|
||||
// Внутренние колбэки движка (аудио слайсов, отвод демодулятора, калибровка
|
||||
// дисплея, тапы TCI). Зовёт CreateEngines — и ОБЯЗАН звать всякий, кто
|
||||
// пересоздал FDSPEngine: без этого новый движок остаётся без отвода
|
||||
// демодулятора (декодеры CW/DMR/FM RAW и RX_AUDIO по TCI) и без тапа IQ.
|
||||
procedure AttachEngineCallbacks;
|
||||
|
||||
// ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ----
|
||||
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
|
||||
@@ -1388,6 +1395,31 @@ begin
|
||||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||||
end;
|
||||
|
||||
procedure TRadioController.AttachEngineCallbacks;
|
||||
// Всё, что движок должен знать о САМОМ контроллере. Отдельным методом, потому
|
||||
// что движок пересоздают на ходу (смена sample rate до старта): раньше эти
|
||||
// колбэки навешивались только в CreateEngines, и после пересоздания движок
|
||||
// оставался без отвода демодулятора — молча пропадали декодеры CW/DMR/FM RAW,
|
||||
// аудио слайсов, калибровка дисплея и оба тапа TCI.
|
||||
begin
|
||||
if not Assigned(FDSPEngine) then Exit;
|
||||
// Роутинг аудио слайсов на их звуковухи — внутренний (не зависит от
|
||||
// фронтенда, в отличие от OnAudio/OnSpectrum).
|
||||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||
FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady;
|
||||
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
|
||||
// Калибровка уровня спектра/водопада на диапазон/трансвертер — движок
|
||||
// спрашивает её раз в кадр (сам он про диапазоны не знает).
|
||||
FDSPEngine.OnDispCal := DispCalFor;
|
||||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||
// Тапы наружу (TCI): их владелец живёт дольше движка.
|
||||
FDSPEngine.SetIQTap(FIQTap);
|
||||
PushAudioTapsActive;
|
||||
// Декодер маяка QO-100 создаётся позже самого движка (в CreateEngines ниже),
|
||||
// зато при ПЕРЕсоздании движка он уже есть и его надо вернуть на место.
|
||||
if Assigned(FBeaconDec) then FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||
end;
|
||||
|
||||
procedure TRadioController.CreateEngines(ASampleRate: Integer);
|
||||
// Создаёт движки в полях контроллера. Колбэки (OnAudio/OnSpectrum/OnHPStatus…)
|
||||
// навешивает вызывающий после возврата — они указывают на UI/демон.
|
||||
@@ -1397,15 +1429,7 @@ begin
|
||||
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
|
||||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||||
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
||||
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
||||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||
FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady;
|
||||
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
|
||||
// Калибровка уровня спектра/водопада на диапазон/трансвертер — движок
|
||||
// спрашивает её раз в кадр (сам он про диапазоны не знает).
|
||||
FDSPEngine.OnDispCal := DispCalFor;
|
||||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||
AttachEngineCallbacks;
|
||||
|
||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||||
@@ -1545,6 +1569,7 @@ begin
|
||||
finally
|
||||
FAudioTapLock.Leave;
|
||||
end;
|
||||
PushAudioTapsActive;
|
||||
end;
|
||||
|
||||
procedure TRadioController.RemoveAudioTap(T: TRadioAudioTapEvent);
|
||||
@@ -1560,11 +1585,20 @@ begin
|
||||
for j := i to High(FAudioTaps) - 1 do FAudioTaps[j] := FAudioTaps[j + 1];
|
||||
SetLength(FAudioTaps, Length(FAudioTaps) - 1);
|
||||
FAudioTapCount := Length(FAudioTaps);
|
||||
Exit;
|
||||
Break;
|
||||
end;
|
||||
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;
|
||||
@@ -1585,7 +1619,11 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetIQTap(T: TIQTapEvent);
|
||||
// Тап помним у себя: движок пересоздают (RecreateDSPEngine при смене sample
|
||||
// rate до старта), и новый про тап не знал бы — поток IQ по TCI молча умирал
|
||||
// на всю сессию. Восстанавливает его AttachEngineCallbacks.
|
||||
begin
|
||||
FIQTap := T;
|
||||
if Assigned(FDSPEngine) then FDSPEngine.SetIQTap(T);
|
||||
end;
|
||||
|
||||
@@ -1984,6 +2022,10 @@ begin
|
||||
else if FMode = MODE_FMRAW then
|
||||
begin
|
||||
// FM RAW decoder audio deliberately bypasses speaker volume and mute.
|
||||
// Значит и линейный выход у него — вот этот сигнал: OnAudioReady для
|
||||
// FM RAW выходит в самом начале, и без этой строки поток LINEOUT по TCI
|
||||
// на FM RAW не шёл вовсе.
|
||||
FireAudioTap(rakLineOut, 0, 0, Left, Right, Count);
|
||||
if FLocalAudio and Assigned(FAudioOut) then
|
||||
FAudioOut.Write(Left, Right, Count);
|
||||
end
|
||||
@@ -2082,9 +2124,15 @@ begin
|
||||
FireAudioTap(rakDemod, FSlices[idx].PanId, SliceId, Left, Right, Count);
|
||||
if (FSlices[idx].Mode = MODE_DMR) and Assigned(FSlices[idx].DMR) then
|
||||
FSlices[idx].DMR.FeedAudio(Left, Right, Count);
|
||||
if (FSlices[idx].Mode = MODE_FMRAW) and FLocalAudio and
|
||||
Assigned(FSlices[idx].Audio) then
|
||||
FSlices[idx].Audio.Write(Left, Right, Count);
|
||||
if FSlices[idx].Mode = MODE_FMRAW then
|
||||
begin
|
||||
// У FM RAW звук идёт мимо громкости и мьюта, поэтому линейный выход слайса
|
||||
// — это он же (OnSliceAudioReady для FM RAW не вызывается вовсе). Без этой
|
||||
// строки поток LINEOUT по TCI на FM RAW-слайсе молчал.
|
||||
FireAudioTap(rakLineOut, FSlices[idx].PanId, SliceId, Left, Right, Count);
|
||||
if FLocalAudio and Assigned(FSlices[idx].Audio) then
|
||||
FSlices[idx].Audio.Write(Left, Right, Count);
|
||||
end;
|
||||
end;
|
||||
|
||||
// DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output.
|
||||
@@ -2095,11 +2143,13 @@ var
|
||||
idx, i, Phase, OutPos, N: Integer;
|
||||
A, B, V, Gain: Single;
|
||||
begin
|
||||
if not FLocalAudio or (Count <= 0) then Exit;
|
||||
if (Count <= 0) or not (FLocalAudio or (FAudioTapCount > 0)) then Exit;
|
||||
idx := FindSliceIndex(SliceId);
|
||||
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or
|
||||
FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit;
|
||||
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or FSlices[idx].Muted then Exit;
|
||||
if SliceMutedByTx(idx) then Exit;
|
||||
// Звуковухи может не быть вовсе (демон), но линейный выход слайса слушает
|
||||
// ещё и TCI — тогда декодированный звук всё равно надо построить.
|
||||
if not (Assigned(FSlices[idx].Audio) or (FAudioTapCount > 0)) then Exit;
|
||||
N := Min(Count, Length(PCM));
|
||||
SetLength(Left, N * 6);
|
||||
SetLength(Right, N * 6);
|
||||
@@ -2117,7 +2167,11 @@ begin
|
||||
Inc(OutPos);
|
||||
end;
|
||||
end;
|
||||
FSlices[idx].Audio.Write(Left, Right, OutPos);
|
||||
// Линейный выход DMR-слайса — декодированный звук, как и у главного тракта
|
||||
// (см. OnDMRAudioReady): 4FSK до декодера в LINEOUT слать нечего.
|
||||
FireAudioTap(rakLineOut, FSlices[idx].PanId, SliceId, Left, Right, OutPos);
|
||||
if FLocalAudio and Assigned(FSlices[idx].Audio) then
|
||||
FSlices[idx].Audio.Write(Left, Right, OutPos);
|
||||
end;
|
||||
|
||||
procedure TRadioController.RefreshSliceShifts;
|
||||
|
||||
Reference in New Issue
Block a user