mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +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:
+55
-29
@@ -165,23 +165,37 @@ type
|
||||
property SrcRate: Integer read FSrcRate;
|
||||
end;
|
||||
|
||||
{ Запись линейного выхода (LINE_OUT_RECORDER_*, §4.3). Кольцо на MaxSec
|
||||
секунд 48 кГц стерео в int16: во-первых, ровно то, что уйдёт в WAV, а
|
||||
во-вторых, float32 на предельных 300 с — это 115 МБ вместо 57. }
|
||||
{ Запись линейного выхода (LINE_OUT_RECORDER_*, §4.3). Буфер на MaxSec секунд
|
||||
48 кГц стерео в int16: во-первых, ровно то, что уйдёт в WAV, а во-вторых,
|
||||
float32 на предельных 300 с — это 115 МБ вместо 57.
|
||||
|
||||
★Не кольцо. Документ говорит про arg2 «максимальное время записи», и дальше
|
||||
прямо: «по истечении времени запись УДАЛЯЕТСЯ, чтобы сохранить запись в файл
|
||||
необходимо в заданном временном интервале выслать LINE_OUT_RECORDER_SAVE».
|
||||
То есть START открывает окно длиной MaxSec, и всё, что не сохранили внутри
|
||||
него, пропадает. Кольцо «последние N секунд» вело себя иначе в обе стороны:
|
||||
начало активной записи затирало само себя, а SAVE через час после START
|
||||
отдавал файл, которого у ExpertSDR3 давно бы не было.
|
||||
|
||||
Срок считаем по ЧАСАМ, а не по накопленным сэмплам: линейный выход молчит
|
||||
(мьют, пауза приёмника), а время записи всё равно идёт. }
|
||||
TTCIRecorder = class
|
||||
private
|
||||
FLock: TCriticalSection;
|
||||
FRing: array of SmallInt; // чередование L/R
|
||||
FBuf: array of SmallInt; // чередование L/R
|
||||
FCap: Integer; // ёмкость в сэмплах на канал
|
||||
FCount: Integer; // накоплено сэмплов на канал
|
||||
FHead: Integer; // позиция записи (в сэмплах на канал)
|
||||
FRate: Integer;
|
||||
FRx: Integer;
|
||||
FExpired: Boolean; // окно записи закрылось, данные удалены
|
||||
FEndsAt: QWord; // GetTickCount64 конца окна
|
||||
procedure DropData; // под FLock
|
||||
public
|
||||
constructor Create(ARx, ARateHz, AMaxSec: Integer);
|
||||
destructor Destroy; override;
|
||||
procedure Feed(const L, R: array of Single; N: Integer);
|
||||
{ Забрать накопленное В ПОРЯДКЕ ВРЕМЕНИ и обнулить кольцо. }
|
||||
{ Забрать накопленное и завершить запись. nil — либо не записано ничего,
|
||||
либо окно уже истекло (по документу это одно и то же: записи нет). }
|
||||
function Take: TTCIPcm;
|
||||
property Rx: Integer read FRx;
|
||||
property Rate: Integer read FRate;
|
||||
@@ -671,9 +685,12 @@ begin
|
||||
if AMaxSec < 1 then AMaxSec := 1;
|
||||
if AMaxSec > TCI_RECORD_MAX_SEC then AMaxSec := TCI_RECORD_MAX_SEC;
|
||||
FCap := ARateHz * AMaxSec;
|
||||
SetLength(FRing, FCap * 2);
|
||||
FCount := 0;
|
||||
FHead := 0;
|
||||
SetLength(FBuf, FCap * 2);
|
||||
FCount := 0;
|
||||
FExpired := False;
|
||||
// Окно открывается прямо здесь: клиент отсчитывает его от своей команды
|
||||
// START, и ждать первого блока аудио, чтобы завести часы, нельзя.
|
||||
FEndsAt := GetTickCount64 + QWord(AMaxSec) * 1000;
|
||||
end;
|
||||
|
||||
destructor TTCIRecorder.Destroy;
|
||||
@@ -682,10 +699,17 @@ begin
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TTCIRecorder.DropData;
|
||||
// Под FLock. Память отдаём сразу: истёкшая запись на 300 с держала бы 57 МБ
|
||||
// до тех пор, пока клиент не вспомнит про BREAK.
|
||||
begin
|
||||
FCount := 0;
|
||||
FExpired := True;
|
||||
SetLength(FBuf, 0);
|
||||
end;
|
||||
|
||||
procedure TTCIRecorder.Feed(const L, R: array of Single; N: Integer);
|
||||
// DSP-поток. Кольцо: по исчерпании ёмкости затирается самое старое — запись
|
||||
// «последние N секунд» именно так и работает (§4.3: по истечении времени
|
||||
// накопленное пропадает, если клиент не сохранил).
|
||||
// DSP-поток. Пишем линейно до конца окна; истекло — данных больше нет (§4.3).
|
||||
var
|
||||
i: Integer;
|
||||
A, B: Single;
|
||||
@@ -695,17 +719,24 @@ begin
|
||||
if N > Length(R) then N := Length(R);
|
||||
FLock.Enter;
|
||||
try
|
||||
if FExpired then Exit;
|
||||
// Часы и ёмкость закрывают окно вместе: ёмкость — это те же MaxSec звука,
|
||||
// но при паузе в аудио доживёт до конца только счёт по часам.
|
||||
if GetTickCount64 >= FEndsAt then
|
||||
begin
|
||||
DropData;
|
||||
Exit;
|
||||
end;
|
||||
if N > FCap - FCount then N := FCap - FCount;
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
A := L[i]; B := R[i];
|
||||
if A > 1.0 then A := 1.0; if A < -1.0 then A := -1.0;
|
||||
if B > 1.0 then B := 1.0; if B < -1.0 then B := -1.0;
|
||||
FRing[FHead * 2] := Round(A * 32767);
|
||||
FRing[FHead * 2 + 1] := Round(B * 32767);
|
||||
Inc(FHead);
|
||||
if FHead >= FCap then FHead := 0;
|
||||
if FCount < FCap then Inc(FCount);
|
||||
FBuf[(FCount + i) * 2] := Round(A * 32767);
|
||||
FBuf[(FCount + i) * 2 + 1] := Round(B * 32767);
|
||||
end;
|
||||
Inc(FCount, N);
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
@@ -713,23 +744,18 @@ end;
|
||||
|
||||
function TTCIRecorder.Take: TTCIPcm;
|
||||
var
|
||||
Start, i, n: Integer;
|
||||
i: Integer;
|
||||
begin
|
||||
Result := nil;
|
||||
FLock.Enter;
|
||||
try
|
||||
if FCount <= 0 then Exit;
|
||||
// Проверка срока и здесь: аудио могло не идти вовсе (мьют, стоящий
|
||||
// приёмник), и тогда Feed часы не смотрел ни разу.
|
||||
if (not FExpired) and (GetTickCount64 >= FEndsAt) then DropData;
|
||||
if FExpired or (FCount <= 0) then Exit;
|
||||
SetLength(Result, FCount * 2);
|
||||
Start := FHead - FCount;
|
||||
if Start < 0 then Inc(Start, FCap);
|
||||
for i := 0 to FCount - 1 do
|
||||
begin
|
||||
n := (Start + i) mod FCap;
|
||||
Result[i * 2] := FRing[n * 2];
|
||||
Result[i * 2 + 1] := FRing[n * 2 + 1];
|
||||
end;
|
||||
FCount := 0;
|
||||
FHead := 0;
|
||||
for i := 0 to FCount * 2 - 1 do Result[i] := FBuf[i];
|
||||
DropData; // SAVE завершает запись (§4.3)
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user