fix(tci): SAVE отдаёт писателю сами куски записи, а не сплошную копию

Потолок 128 МБ всё ещё пробивался примерно вдвое на пике: Take собирал
линейную копию ДО освобождения FChunks, поэтому при полном бюджете рядом
жили ~128 МБ кусков и ~128 МБ копии, а счётчик показывал 128. Передача
резерва писателю (9b77809) закрывала учёт, но не сам пик.

Копии больше нет вовсе. Take отдаёт куски КАК ЕСТЬ — новый TTCIRecTake
(Chunks + Chunk + Count + Reserved), — и вместе с ними уезжает их место в
бюджете целиком. TTCIWavWriter пишет куски в файл подряд: в WAV сэмплы и
так лежат встык, а хвост последнего куска за Count просто не наш. Место
отпускается в ReleaseData, ровно один раз на любом пути выхода Execute.

Продовый путь теперь не выделяет под запись ни одного лишнего байта:
TTCIPcm остался только внутри куска. Склейка нужна одному стенду, чтобы
проверять порядок и уровень, — она и живёт в стенде (FlatTake).

Стенд 199/199: 2.5 с записи отдаются ТРЕМЯ кусками по секунде (свёрнутая
копия дала бы один), счёт бюджета при Take не меняется, резерва хватает
на отданные куски, склеенные куски дают непрерывный звук, писатель
возвращает резерв по окончании. Проверено, что копирующая реализация Take
краснит четыре проверки. GUI (--ws=qt6) и демон зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 13:18:20 +03:00
co-authored by Claude Opus 5
parent 9b77809a73
commit 7aae0fdfcd
4 changed files with 195 additions and 90 deletions
+97 -18
View File
@@ -550,6 +550,26 @@ end;
C. Рекордер и WAV
═══════════════════════════════════════════════════════════════════════════ }
function FlatTake(const T: TTCIRecTake): TTCIPcm;
// Склейка кусков записи в один буфер. Живёт ТОЛЬКО в стенде: продовый путь
// сплошной копии не делает вовсе — писатель пишет куски подряд, иначе на
// полном бюджете рядом жили бы куски и их копия, то есть двойной пик.
var C, n, Left: Integer;
begin
Result := nil;
if T.Count <= 0 then Exit;
SetLength(Result, T.Count * 2);
Left := T.Count;
for C := 0 to High(T.Chunks) do
begin
if Left <= 0 then Break;
n := T.Chunk;
if n > Left then n := Left;
Move(T.Chunks[C][0], Result[(T.Count - Left) * 2], n * 2 * SizeOf(SmallInt));
Dec(Left, n);
end;
end;
procedure TestRecorder;
var
Rec: TTCIRecorder;
@@ -562,7 +582,8 @@ var
Sz: LongWord;
W: TTCIWavWriter;
Waited: Integer;
Was, Held: Int64;
Was: Int64;
Tk: TTCIRecTake;
begin
WriteLn('C. Рекордер линейного выхода');
@@ -578,29 +599,69 @@ begin
Rec.Feed(L, R, 8192);
Inc(Total, 8192);
end;
Data := Rec.Take(Held);
Tk := Rec.Take; Data := FlatTake(Tk);
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('рекордер: Take отдаёт сами куски, не копию',
(Length(Tk.Chunks) > 0) and (Tk.Count = Length(Data) div 2),
IntToStr(Length(Tk.Chunks)));
Check('рекордер: резерв переехал целиком, счёт не изменился',
TCIRecBudgetUsed = Was + Tk.Reserved,
IntToStr(TCIRecBudgetUsed - Was) + '/' + IntToStr(Tk.Reserved));
Check('рекордер: резерва хватает на отданные куски',
Tk.Reserved >= Int64(Length(Data)) * SizeOf(SmallInt),
IntToStr(Tk.Reserved));
TCIRecBudgetFree(Tk.Reserved); // как это сделает писатель
Tk.Chunks := nil;
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(Held)) = 0);
Check('рекордер: Take завершает запись', Length(FlatTake(Rec.Take)) = 0);
finally
Rec.Free;
end;
// ★Отдаются именно куски, а не свёрнутая в один буфер копия: 2.5 секунды
// при куске в секунду — это ТРИ куска. Если бы Take собирал сплошную копию
// (как было), кусок пришёл бы один, а пик памяти на полном бюджете вырос бы
// вдвое: 128 МБ кусков и 128 МБ копии рядом, при счётчике в 128.
Was := TCIRecBudgetUsed;
Rec := TTCIRecorder.Create(0, 48000, 3, nil);
try
for i := 0 to 8191 do begin L[i] := 0.25; R[i] := -0.25; end;
Total := 0;
while Total < 120000 do // 2.5 с при 48 кГц
begin
Rec.Feed(L, R, 8192);
Inc(Total, 8192);
end;
Tk := Rec.Take;
Check('рекордер: кусков ровно по секундам записи',
(Tk.Chunk = 48000) and (Length(Tk.Chunks) = 3) and
(Tk.Count >= 120000),
IntToStr(Length(Tk.Chunks)) + ' × ' + IntToStr(Tk.Chunk));
Check('рекордер: сплошной копии не появилось',
TCIRecBudgetUsed = Was + Tk.Reserved,
IntToStr(TCIRecBudgetUsed - Was));
Data := FlatTake(Tk);
Check('рекордер: куски склеиваются в непрерывный звук',
(Length(Data) = Tk.Count * 2) and
(Data[0] = Round(0.25 * 32767)) and
(Data[Length(Data) - 2] = Round(0.25 * 32767)));
Tk.Chunks := nil;
TCIRecBudgetFree(Tk.Reserved);
finally
Rec.Free;
end;
Check('рекордер: счёт закрыт', TCIRecBudgetUsed = Was);
// Порядок отсчётов: первым обязан идти самый ПЕРВЫЙ записанный.
Rec := TTCIRecorder.Create(0, 1000, 1, nil); // ёмкость 1000 отсчётов
try
@@ -610,7 +671,7 @@ begin
R[0] := 0;
Rec.Feed(L, R, 1);
end;
Data := Rec.Take(Held);
Tk := Rec.Take; Data := FlatTake(Tk);
Check('рекордер: с начала записи, а не с конца',
(Length(Data) = 2000) and (Data[0] = 0) and
(Abs(Data[1998] - Round((999 / 4000.0) * 32767)) <= 1),
@@ -625,7 +686,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(Held)) > 0);
Check('рекордер: до срока запись есть', Length(FlatTake(Rec.Take)) > 0);
finally
Rec.Free;
end;
@@ -633,9 +694,9 @@ begin
try
Rec.Feed(L, R, 8192);
Sleep(1100); // окно закрылось
Check('рекордер: после срока запись удалена', Length(Rec.Take(Held)) = 0);
Check('рекордер: после срока запись удалена', Length(FlatTake(Rec.Take)) = 0);
Rec.Feed(L, R, 8192); // и новое аудио уже не принимает
Check('рекордер: истёкший не оживает', Length(Rec.Take(Held)) = 0);
Check('рекордер: истёкший не оживает', Length(FlatTake(Rec.Take)) = 0);
finally
Rec.Free;
end;
@@ -668,7 +729,7 @@ begin
try
Rec.Feed(L, R, 8192);
Check('бюджет: при отказе запись не рушится, а стоит пустой',
Length(Rec.Take(Held)) = 0);
Length(FlatTake(Rec.Take)) = 0);
finally
Rec.Free;
end;
@@ -697,7 +758,13 @@ begin
DeleteFile(Path);
SetLength(Data, 2000);
for i := 0 to 1999 do Data[i] := i * 8;
W := TTCIWavWriter.Create(Path, Data, 48000, 0);
Tk.Chunks := nil;
SetLength(Tk.Chunks, 1);
Tk.Chunks[0] := Data;
Tk.Chunk := Length(Data) div 2;
Tk.Count := Tk.Chunk;
Tk.Reserved := 0;
W := TTCIWavWriter.Create(Path, Tk, 48000);
Waited := 0;
while (not FileExists(Path)) and (Waited < 2000) do
begin
@@ -733,7 +800,13 @@ begin
Was := TCIRecBudgetUsed;
Check('WAV: резерв под писателя взят', TCIRecBudgetTake(4096));
DeleteFile(Path);
TTCIWavWriter.Create(Path, Data, 48000, 4096);
Tk.Chunks := nil;
SetLength(Tk.Chunks, 1);
Tk.Chunks[0] := Data;
Tk.Chunk := Length(Data) div 2;
Tk.Count := Tk.Chunk;
Tk.Reserved := 4096;
TTCIWavWriter.Create(Path, Tk, 48000);
Waited := 0;
while (TCIRecBudgetUsed <> Was) and (Waited < 2000) do
begin
@@ -748,7 +821,13 @@ begin
// файл обязан остаться прежним.
SetLength(Data, 10);
for i := 0 to 9 do Data[i] := 1;
TTCIWavWriter.Create(Path, Data, 48000, 0);
Tk.Chunks := nil;
SetLength(Tk.Chunks, 1);
Tk.Chunks[0] := Data;
Tk.Chunk := 5;
Tk.Count := 5;
Tk.Reserved := 0;
TTCIWavWriter.Create(Path, Tk, 48000);
Sleep(200);
FS := TFileStream.Create(Path, fmOpenRead);
try