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
+15 -9
View File
@@ -2998,8 +2998,7 @@ procedure TTCIAdapter.CmdRecorder(Client: TTCIClient; const M: TTCIMessage);
var
Rx, Sec, Rate: Integer;
Path, Req: string;
Data: TTCIPcm;
Held: Int64; // байты бюджета, уходящие писателю вместе с Data
Rec: TTCIRecTake; // куски записи + их место в бюджете
Old, New_: TTCIRecorder;
begin
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
@@ -3085,8 +3084,10 @@ begin
// Забираем рекордер из таблицы (сохранение завершает запись, §4.3) и только
// потом снимаем с него данные: копия кольца — это десятки мегабайт, и делать
// её под локом DSP-потока нельзя.
Data := nil;
Held := 0;
Rec.Chunks := nil;
Rec.Chunk := 0;
Rec.Count := 0;
Rec.Reserved := 0;
Rate := TCI_AUDIO_ENGINE_RATE;
FStreamLock.Enter;
try
@@ -3097,20 +3098,25 @@ begin
end;
if Old <> nil then
begin
// ★Резерв бюджета переезжает вместе с данными: копию держит писатель, и
// отпустит её он же. Иначе SAVE был бы дырой в потолке памяти.
Data := Old.Take(Held);
// ★Куски записи переезжают писателю КАК ЕСТЬ, вместе со своим местом в
// бюджете: сплошная копия удваивала бы пик, а невыпущенный резерв был бы
// дырой в потолке.
Rec := Old.Take;
Rate := Old.Rate;
Old.Free;
end;
if Data = nil then
if Rec.Count <= 0 then
begin
// Резерва тут быть неоткуда (кусок выделяется только под сэмпл, который
// тут же и пишется), но возвращаем на всякий случай: единственный путь,
// на котором запись не доходит до писателя.
TCIRecBudgetFree(Rec.Reserved);
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'nothing recorded']));
Exit;
end;
// Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в
// потоке клиента, который в это время не читает свой сокет.
TTCIWavWriter.Create(Path, Data, Rate, Held);
TTCIWavWriter.Create(Path, Rec, Rate);
end;
procedure TTCIAdapter.DropClientRecorders(C: TTCIClient);