fix(tci,cat): резерв бюджета переезжает писателю; строгий разбор полей ZZ-команд

Три замечания по 79132f1.

1. [P2] Лимит памяти рекордеров обходился при SAVE. Take собирал сплошную
   копию записи, а DropData тут же возвращал исходные куски в бюджет —
   копия жила дальше в асинхронном TTCIWavWriter уже неучтённой. Один SAVE
   поднимал настоящее потребление примерно вдвое, а на медленном или
   зависшем сетевом каталоге очередь writer-потоков и их буферов росла
   мимо потолка вовсе. Теперь копия НАСЛЕДУЕТ резерв кусков, из которых
   собрана: Take отдаёт его out-параметром Reserved, писатель держит до
   конца записи и отпускает в ReleaseData — ровно один раз, сколько бы
   путей выхода ни было у Execute. Новых денег у бюджета копия не берёт,
   так что потолок теперь считает и очередь сохранений тоже.

2. [P2] Пять ZZ-команд проверяли длину поля, но не содержимое:
   StrToIntDef(s, 0) превращал любую нечисловую пару символов в индекс 0.
   ZZBSxx; переключал диапазон на нулевой вместо ?;, ZZBMxx; и
   ZZAUxx;/ZZBPxx; двигали VFO, ZZFIxx; выбирал фильтр 0. Разбор приведён
   к идиоме ZZFL/ZZFH: TryStrToInt, иначе ошибка формата.

3. [P3] Сообщение об отказе запуска TCI звало в Settings → Advanced, а
   настройки там уже не живут — вкладка CAT (переезд был в de0830f).

Стенд 194/194: Take отдаёт резерв размером с копию, после Take занят ровно
он, писатель возвращает его по окончании. Без фикса первая проверка
краснеет. GUI (--ws=qt6) и демон зелёные. doc/TCI.md §2.5 и сводка стенда,
doc/CAT_STATUS.md — таблица разбора.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:50:21 +03:00
co-authored by Claude Opus 5
parent 79132f133c
commit 9b77809a73
7 changed files with 119 additions and 31 deletions
+40 -10
View File
@@ -562,12 +562,13 @@ var
Sz: LongWord;
W: TTCIWavWriter;
Waited: Integer;
Was: Int64;
Was, Held: Int64;
begin
WriteLn('C. Рекордер линейного выхода');
// Максимум записи — 1 секунда: подаём полторы, лишнее не берём. ★Именно НЕ
// берём: окно записи по §4.3 начинается со START, а не «последняя секунда».
Was := TCIRecBudgetUsed; // счёт бюджета ДО записи
Rec := TTCIRecorder.Create(0, 48000, 1, nil);
try
Total := 0;
@@ -577,12 +578,25 @@ begin
Rec.Feed(L, R, 8192);
Inc(Total, 8192);
end;
Data := Rec.Take;
Data := Rec.Take(Held);
Check('рекордер: длина по максимуму', Length(Data) = 48000 * 2,
IntToStr(Length(Data)));
// ★Take отдаёт данные ВМЕСТЕ с их местом в бюджете: копия живёт дальше в
// писателе, и до конца записи она обязана оставаться учтённой. Иначе SAVE
// был бы дырой в потолке — куски вернулись бы сразу, а копия висела бы
// неучтённой, и очередь сохранений на медленном диске росла бы без границ.
Check('рекордер: Take передаёт резерв размером с копию',
Held = Int64(Length(Data)) * SizeOf(SmallInt),
IntToStr(Held));
Check('рекордер: после Take занят ровно резерв копии',
TCIRecBudgetUsed = Was + Held,
IntToStr(TCIRecBudgetUsed - Was));
TCIRecBudgetFree(Held); // как это сделает писатель
Check('рекордер: возврат резерва закрывает счёт',
TCIRecBudgetUsed = Was, IntToStr(TCIRecBudgetUsed - Was));
Check('рекордер: уровень сохранён',
(Data[0] = Round(0.25 * 32767)) and (Data[1] = -Round(0.25 * 32767)));
Check('рекордер: Take завершает запись', Length(Rec.Take) = 0);
Check('рекордер: Take завершает запись', Length(Rec.Take(Held)) = 0);
finally
Rec.Free;
end;
@@ -596,7 +610,7 @@ begin
R[0] := 0;
Rec.Feed(L, R, 1);
end;
Data := Rec.Take;
Data := Rec.Take(Held);
Check('рекордер: с начала записи, а не с конца',
(Length(Data) = 2000) and (Data[0] = 0) and
(Abs(Data[1998] - Round((999 / 4000.0) * 32767)) <= 1),
@@ -611,7 +625,7 @@ begin
try
for i := 0 to 8191 do begin L[i] := 0.25; R[i] := -0.25; end;
Rec.Feed(L, R, 8192);
Check('рекордер: до срока запись есть', Length(Rec.Take) > 0);
Check('рекордер: до срока запись есть', Length(Rec.Take(Held)) > 0);
finally
Rec.Free;
end;
@@ -619,9 +633,9 @@ begin
try
Rec.Feed(L, R, 8192);
Sleep(1100); // окно закрылось
Check('рекордер: после срока запись удалена', Length(Rec.Take) = 0);
Check('рекордер: после срока запись удалена', Length(Rec.Take(Held)) = 0);
Rec.Feed(L, R, 8192); // и новое аудио уже не принимает
Check('рекордер: истёкший не оживает', Length(Rec.Take) = 0);
Check('рекордер: истёкший не оживает', Length(Rec.Take(Held)) = 0);
finally
Rec.Free;
end;
@@ -654,7 +668,7 @@ begin
try
Rec.Feed(L, R, 8192);
Check('бюджет: при отказе запись не рушится, а стоит пустой',
Length(Rec.Take) = 0);
Length(Rec.Take(Held)) = 0);
finally
Rec.Free;
end;
@@ -683,7 +697,7 @@ begin
DeleteFile(Path);
SetLength(Data, 2000);
for i := 0 to 1999 do Data[i] := i * 8;
W := TTCIWavWriter.Create(Path, Data, 48000);
W := TTCIWavWriter.Create(Path, Data, 48000, 0);
Waited := 0;
while (not FileExists(Path)) and (Waited < 2000) do
begin
@@ -713,12 +727,28 @@ begin
finally
FS.Free;
end;
// ★Писатель отпускает резерв, когда данные ему больше не нужны — иначе
// потолок памяти держал бы только сами записи, а очередь сохранений на
// медленном диске росла бы мимо него.
Was := TCIRecBudgetUsed;
Check('WAV: резерв под писателя взят', TCIRecBudgetTake(4096));
DeleteFile(Path);
TTCIWavWriter.Create(Path, Data, 48000, 4096);
Waited := 0;
while (TCIRecBudgetUsed <> Was) and (Waited < 2000) do
begin
Sleep(10);
Inc(Waited, 10);
end;
Check('WAV: писатель вернул резерв по окончании',
TCIRecBudgetUsed = Was, IntToStr(TCIRecBudgetUsed - Was));
// ★Существующий файл не трогаем: имя приходит из сети, и fmCreate затирал
// бы любой доступный процессу файл. Пишем поверх заведомо другой длиной —
// файл обязан остаться прежним.
SetLength(Data, 10);
for i := 0 to 9 do Data[i] := 1;
TTCIWavWriter.Create(Path, Data, 48000);
TTCIWavWriter.Create(Path, Data, 48000, 0);
Sleep(200);
FS := TFileStream.Create(Path, fmOpenRead);
try