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
+17 -5
View File
@@ -1743,10 +1743,13 @@ end;
function TCATEngine.ZZAU(const s: string): string; function TCATEngine.ZZAU(const s: string): string;
// ZZAU: сдвиг VFO A вверх на один шаг с индексом nn (00..14, см. StepIdxToHz). // ZZAU: сдвиг VFO A вверх на один шаг с индексом nn (00..14, см. StepIdxToHz).
// ★Нечисловое поле — ошибка формата: StrToIntDef молча дал бы индекс 0, то
// есть «ZZAUxx;» двигал бы VFO вместо честного «?;» (как в ZZFL/ZZFH).
var idx: Integer; var idx: Integer;
begin begin
if Length(s) = 2 then begin if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14); if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 14);
if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(SafeGetVfoA + StepIdxToHz(idx)); if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(SafeGetVfoA + StepIdxToHz(idx));
Result := ''; Result := '';
end else end else
@@ -1755,10 +1758,12 @@ end;
function TCATEngine.ZZBP(const s: string): string; function TCATEngine.ZZBP(const s: string): string;
// ZZBP: сдвиг VFO B вверх на один шаг с индексом nn (00..14, см. StepIdxToHz). // ZZBP: сдвиг VFO B вверх на один шаг с индексом nn (00..14, см. StepIdxToHz).
// Разбор поля — как у ZZAU: нечисловое значит ошибку, а не нулевой индекс.
var idx: Integer; var idx: Integer;
begin begin
if Length(s) = 2 then begin if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14); if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 14);
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB + StepIdxToHz(idx)); if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB + StepIdxToHz(idx));
Result := ''; Result := '';
end else end else
@@ -1811,10 +1816,12 @@ end;
function TCATEngine.ZZBM(const s: string): string; function TCATEngine.ZZBM(const s: string): string;
// ZZBM: сдвиг VFO B ВНИЗ на один шаг с индексом nn (00..14) — пара к ZZBP. // ZZBM: сдвиг VFO B ВНИЗ на один шаг с индексом nn (00..14) — пара к ZZBP.
// (Режим — это ZZMD; раньше ZZBM был ошибочно заалиашен на него.) // (Режим — это ZZMD; раньше ZZBM был ошибочно заалиашен на него.)
// Разбор поля — как у ZZAU: нечисловое значит ошибку, а не нулевой индекс.
var idx: Integer; var idx: Integer;
begin begin
if Length(s) = 2 then begin if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14); if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 14);
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB - StepIdxToHz(idx)); if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB - StepIdxToHz(idx));
Result := ''; Result := '';
end else end else
@@ -1825,10 +1832,13 @@ function TCATEngine.ZZBS(const s: string): string;
// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и // ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке // несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты. // диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
// то есть уводил бы оператора с рабочего бэнда по опечатке клиента.
var idx: Integer; var idx: Integer;
begin begin
if Length(s) = 2 then begin if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 10); if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 10);
if Assigned(FCtx.DoBandByIndex) then FCtx.DoBandByIndex(idx); if Assigned(FCtx.DoBandByIndex) then FCtx.DoBandByIndex(idx);
Result := ''; Result := '';
end else if Length(s) = 0 then end else if Length(s) = 0 then
@@ -2000,10 +2010,12 @@ end;
function TCATEngine.ZZFI(const s: string): string; function TCATEngine.ZZFI(const s: string): string;
// ZZFI: индекс текущего фильтра (2 цифры). // ZZFI: индекс текущего фильтра (2 цифры).
// Разбор поля — как у ZZFL/ZZFH: нечисловое значит ошибку, а не фильтр 0.
var idx: Integer; var idx: Integer;
begin begin
if Length(s) = 2 then begin if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 9); if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 9);
if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx); if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx);
Result := ''; Result := '';
end else if Length(s) = 0 then end else if Length(s) = 0 then
+1 -1
View File
@@ -2735,7 +2735,7 @@ begin
if not FTCIAdapter.ApplySettings(FTCICfg) then if not FTCIAdapter.ApplySettings(FTCICfg) then
ShowMessage('TCI server failed to start on ' + FTCICfg.BindAddr + ':' + ShowMessage('TCI server failed to start on ' + FTCICfg.BindAddr + ':' +
IntToStr(FTCICfg.Port) + '.' + LineEnding + IntToStr(FTCICfg.Port) + '.' + LineEnding +
'Port busy or address invalid — check Settings → Advanced.'); 'Port busy or address invalid — check Settings → CAT.');
OnActivate := TCIFormActivate; OnActivate := TCIFormActivate;
OnDeactivate := TCIFormDeactivate; OnDeactivate := TCIFormDeactivate;
+6 -2
View File
@@ -2999,6 +2999,7 @@ var
Rx, Sec, Rate: Integer; Rx, Sec, Rate: Integer;
Path, Req: string; Path, Req: string;
Data: TTCIPcm; Data: TTCIPcm;
Held: Int64; // байты бюджета, уходящие писателю вместе с Data
Old, New_: TTCIRecorder; Old, New_: TTCIRecorder;
begin begin
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
@@ -3085,6 +3086,7 @@ begin
// потом снимаем с него данные: копия кольца — это десятки мегабайт, и делать // потом снимаем с него данные: копия кольца — это десятки мегабайт, и делать
// её под локом DSP-потока нельзя. // её под локом DSP-потока нельзя.
Data := nil; Data := nil;
Held := 0;
Rate := TCI_AUDIO_ENGINE_RATE; Rate := TCI_AUDIO_ENGINE_RATE;
FStreamLock.Enter; FStreamLock.Enter;
try try
@@ -3095,7 +3097,9 @@ begin
end; end;
if Old <> nil then if Old <> nil then
begin begin
Data := Old.Take; // ★Резерв бюджета переезжает вместе с данными: копию держит писатель, и
// отпустит её он же. Иначе SAVE был бы дырой в потолке памяти.
Data := Old.Take(Held);
Rate := Old.Rate; Rate := Old.Rate;
Old.Free; Old.Free;
end; end;
@@ -3106,7 +3110,7 @@ begin
end; end;
// Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в // Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в
// потоке клиента, который в это время не читает свой сокет. // потоке клиента, который в это время не читает свой сокет.
TTCIWavWriter.Create(Path, Data, Rate); TTCIWavWriter.Create(Path, Data, Rate, Held);
end; end;
procedure TTCIAdapter.DropClientRecorders(C: TTCIClient); procedure TTCIAdapter.DropClientRecorders(C: TTCIClient);
+44 -11
View File
@@ -214,8 +214,12 @@ type
destructor Destroy; override; destructor Destroy; override;
procedure Feed(const L, R: array of Single; N: Integer); procedure Feed(const L, R: array of Single; N: Integer);
{ Забрать накопленное и завершить запись. nil либо не записано ничего, { Забрать накопленное и завершить запись. nil либо не записано ничего,
либо окно уже истекло (по документу это одно и то же: записи нет). } либо окно уже истекло (по документу это одно и то же: записи нет).
function Take: TTCIPcm; ★Reserved сколько байт бюджета УХОДИТ ВМЕСТЕ С ДАННЫМИ: копия живёт
дальше в писателе, и пока он её не отпустит, она обязана оставаться
учтённой. Вызывающий передаёт это число писателю (или возвращает сам
через TCIRecBudgetFree, если писателя не будет). }
function Take(out Reserved: Int64): TTCIPcm;
{ Окно закрылось по часам. Спрашивает тик сервера: у мёртвого приёмника { Окно закрылось по часам. Спрашивает тик сервера: у мёртвого приёмника
Feed не зовут вовсе, и без этого вопроса память жила бы до Stop. } Feed не зовут вовсе, и без этого вопроса память жила бы до Stop. }
function Expired: Boolean; function Expired: Boolean;
@@ -225,17 +229,23 @@ type
end; end;
{ Писатель WAV в своём потоке: файл до 60 МБ, а зовут сохранение из тика { Писатель WAV в своём потоке: файл до 60 МБ, а зовут сохранение из тика
сервера блокировать его на секунду диска нельзя. Данные забирает себе. } сервера блокировать его на секунду диска нельзя. Данные забирает себе
ВМЕСТЕ С ИХ РЕЗЕРВОМ в общем бюджете (AReserved из TTCIRecorder.Take) и
отпускает его, только когда данные больше не нужны. Пока писатель ждёт
медленный диск, эти байты остаются занятыми так потолок и держит
очередь сохранений, а не только сами записи. }
TTCIWavWriter = class(TThread) TTCIWavWriter = class(TThread)
private private
FPath: string; FPath: string;
FData: TTCIPcm; FData: TTCIPcm;
FRate: Integer; FRate: Integer;
FReserved: Int64;
procedure ReleaseData;
protected protected
procedure Execute; override; procedure Execute; override;
public public
constructor Create(const APath: string; const AData: TTCIPcm; constructor Create(const APath: string; const AData: TTCIPcm;
ARateHz: Integer); ARateHz: Integer; AReserved: Int64);
end; end;
{ Коэффициент прореживания SrcRate WantRate: наибольший целый делитель, { Коэффициент прореживания SrcRate WantRate: наибольший целый делитель,
@@ -843,11 +853,13 @@ begin
end; end;
end; end;
function TTCIRecorder.Take: TTCIPcm; function TTCIRecorder.Take(out Reserved: Int64): TTCIPcm;
var var
C, n, Left: Integer; C, n, Left: Integer;
Copy_: Int64;
begin begin
Result := nil; Result := nil;
Reserved := 0;
FLock.Enter; FLock.Enter;
try try
// Проверка срока и здесь: аудио могло не идти вовсе (мьют, стоящий // Проверка срока и здесь: аудио могло не идти вовсе (мьют, стоящий
@@ -857,6 +869,13 @@ begin
// Склейка кусков в один буфер — единственное копирование за всю запись, и // Склейка кусков в один буфер — единственное копирование за всю запись, и
// оно идёт уже вне DSP-потока: SAVE вынимает рекордер из таблицы раньше, // оно идёт уже вне DSP-потока: SAVE вынимает рекордер из таблицы раньше,
// чем зовёт Take, так что кормить его больше некому. // чем зовёт Take, так что кормить его больше некому.
// ★Копия НЕ берёт у бюджета новых денег и НЕ отпускает старых: она
// наследует резерв кусков, из которых собрана. Иначе SAVE был бы дырой в
// потолке — DropData возвращал бы куски сразу, а копия жила бы дальше в
// писателе неучтённой, и на медленном (или зависшем) сетевом каталоге
// writer-потоки копились бы без всякой границы.
Copy_ := Int64(FCount) * 2 * SizeOf(SmallInt);
if Copy_ > FBytes then Copy_ := FBytes; // не отдать больше, чем занято
SetLength(Result, FCount * 2); SetLength(Result, FCount * 2);
Left := FCount; Left := FCount;
for C := 0 to High(FChunks) do for C := 0 to High(FChunks) do
@@ -867,6 +886,10 @@ begin
Move(FChunks[C][0], Result[(FCount - Left) * 2], n * 2 * SizeOf(SmallInt)); Move(FChunks[C][0], Result[(FCount - Left) * 2], n * 2 * SizeOf(SmallInt));
Dec(Left, n); Dec(Left, n);
end; end;
// Из общего резерва оставляем ровно то, что перешло в копию, остальное
// (хвост последнего куска) возвращаем — этим и займётся DropData.
Reserved := Copy_;
Dec(FBytes, Copy_);
DropData; // SAVE завершает запись (§4.3) DropData; // SAVE завершает запись (§4.3)
finally finally
FLock.Leave; FLock.Leave;
@@ -878,16 +901,26 @@ end;
═══════════════════════════════════════════════════════════════════════════ } ═══════════════════════════════════════════════════════════════════════════ }
constructor TTCIWavWriter.Create(const APath: string; constructor TTCIWavWriter.Create(const APath: string;
const AData: TTCIPcm; ARateHz: Integer); const AData: TTCIPcm; ARateHz: Integer; AReserved: Int64);
begin begin
inherited Create(True); inherited Create(True);
FreeOnTerminate := True; FreeOnTerminate := True;
FPath := APath; FPath := APath;
FData := AData; FData := AData;
FRate := ARateHz; FRate := ARateHz;
FReserved := AReserved;
Start; Start;
end; end;
procedure TTCIWavWriter.ReleaseData;
// Данные и их место в бюджете уходят вместе и ровно один раз — сколько бы
// путей выхода ни было у Execute.
begin
FData := nil;
TCIRecBudgetFree(FReserved);
FReserved := 0;
end;
procedure TTCIWavWriter.Execute; procedure TTCIWavWriter.Execute;
// Заголовок собираем в буфере: WAV — это фиксированные 44 байта, и городить // Заголовок собираем в буфере: WAV — это фиксированные 44 байта, и городить
// два десятка отдельных Write ради них незачем (а строковые литералы в // два десятка отдельных Write ради них незачем (а строковые литералы в
@@ -954,7 +987,7 @@ begin
// клиенту уже некому: команда давно подтверждена. Молчим, но и не падаем: // клиенту уже некому: команда давно подтверждена. Молчим, но и не падаем:
// исключение из потока утащило бы за собой процесс. // исключение из потока утащило бы за собой процесс.
end; end;
FData := nil; ReleaseData;
end; end;
{ ═══════════════════════════════════════════════════════════════════════════ { ═══════════════════════════════════════════════════════════════════════════
+1
View File
@@ -272,6 +272,7 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
|---|---| |---|---|
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Эталон отбраковывает лишний суффикс в парсере, по таблице ширин; у нас таблицы нет — список безаргументных команд теперь в `IsParamless` | | `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Эталон отбраковывает лишний суффикс в парсере, по таблице ширин; у нас таблицы нет — список безаргументных команд теперь в `IsParamless` |
| `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий | | `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий |
| `ZZAU` `ZZBP` `ZZBM` `ZZBS` `ZZFI` | длину поля проверяли, а содержимое — нет: `StrToIntDef(s, 0)` превращал любую нечисловую пару символов в индекс 0. То есть `ZZBSxx;` **переключал диапазон** на нулевой вместо `?;`, `ZZBMxx;` и `ZZAUxx;`/`ZZBPxx;` двигали VFO, а `ZZFIxx;` выбирал фильтр 0. Разбор приведён к идиоме `ZZFL`/`ZZFH`: `TryStrToInt`, иначе ошибка формата |
| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время | | `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время |
| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле | | `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле |
| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт | | `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт |
+10 -2
View File
@@ -528,7 +528,14 @@ ExpertSDR3 давно бы не было.
2. **Общий бюджет `TCI_RECORD_MAX_BYTES` (128 МБ) на все рекордеры сразу.** 2. **Общий бюджет `TCI_RECORD_MAX_BYTES` (128 МБ) на все рекордеры сразу.**
Спрашивается при выделении каждого куска. Отказ не рушит запись: набранное Спрашивается при выделении каждого куска. Отказ не рушит запись: набранное
остаётся сохраняемым, просто дальше она не растёт — иначе клиент терял бы остаётся сохраняемым, просто дальше она не растёт — иначе клиент терял бы
уже записанное из-за чужой записи на другом приёмнике. уже записанное из-за чужой записи на другом приёмнике.`SAVE` бюджет не
обходит: `Take` собирает куски в сплошную копию и **передаёт ей их резерв**
(out-параметр `Reserved`), а не возвращает его сразу — копия живёт дальше в
`TTCIWavWriter`, и отпускает место он же, когда данные больше не нужны
(`ReleaseData` — ровно один раз, сколько бы путей выхода ни было у
`Execute`). Иначе один `SAVE` поднимал бы настоящее потребление вдвое, а на
медленном или зависшем сетевом каталоге очередь writer-потоков росла бы
мимо потолка вовсе.
3. **Освобождение по трём событиям, а не по одному.** Раньше срок проверял 3. **Освобождение по трём событиям, а не по одному.** Раньше срок проверял
только `Feed`, то есть DSP-поток — а к мёртвому приёмнику он не приходит только `Feed`, то есть DSP-поток — а к мёртвому приёмнику он не приходит
никогда, и `START` на несуществующий номер оставлял память навсегда. никогда, и `START` на несуществующий номер оставлял память навсегда.
@@ -861,7 +868,8 @@ ExpertSDR3 давно бы не было.
выделяет ни байта, бюджет растёт кусками по мере звука и возвращается по выделяет ни байта, бюджет растёт кусками по мере звука и возвращается по
`Free`, потолок берётся целиком и сверх него следует отказ, при отказе запись `Free`, потолок берётся целиком и сверх него следует отказ, при отказе запись
не рушится, окно истекает по часам **без единого `Feed`** и отдаёт память не рушится, окно истекает по часам **без единого `Feed`** и отдаёт память
само. ★Имя файла: простое имя ложится в каталог записей, а каталог из само, `Take` отдаёт резерв размером с копию и после него занят ровно он, а
писатель возвращает этот резерв по окончании записи. ★Имя файла: простое имя ложится в каталог записей, а каталог из
просьбы отбрасывается — абсолютный путь, `..` и буква диска наружу не просьбы отбрасывается — абсолютный путь, `..` и буква диска наружу не
выводят; пусто, `..`, не-`.wav`, управляющий символ и отсутствие каталога выводят; пусто, `..`, не-`.wav`, управляющий символ и отсутствие каталога
записей дают отказ; существующий файл писатель не перезаписывает. записей дают отказ; существующий файл писатель не перезаписывает.
+40 -10
View File
@@ -562,12 +562,13 @@ var
Sz: LongWord; Sz: LongWord;
W: TTCIWavWriter; W: TTCIWavWriter;
Waited: Integer; Waited: Integer;
Was: Int64; Was, Held: Int64;
begin begin
WriteLn('C. Рекордер линейного выхода'); WriteLn('C. Рекордер линейного выхода');
// Максимум записи — 1 секунда: подаём полторы, лишнее не берём. ★Именно НЕ // Максимум записи — 1 секунда: подаём полторы, лишнее не берём. ★Именно НЕ
// берём: окно записи по §4.3 начинается со START, а не «последняя секунда». // берём: окно записи по §4.3 начинается со START, а не «последняя секунда».
Was := TCIRecBudgetUsed; // счёт бюджета ДО записи
Rec := TTCIRecorder.Create(0, 48000, 1, nil); Rec := TTCIRecorder.Create(0, 48000, 1, nil);
try try
Total := 0; Total := 0;
@@ -577,12 +578,25 @@ begin
Rec.Feed(L, R, 8192); Rec.Feed(L, R, 8192);
Inc(Total, 8192); Inc(Total, 8192);
end; end;
Data := Rec.Take; Data := Rec.Take(Held);
Check('рекордер: длина по максимуму', Length(Data) = 48000 * 2, Check('рекордер: длина по максимуму', Length(Data) = 48000 * 2,
IntToStr(Length(Data))); 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('рекордер: уровень сохранён', Check('рекордер: уровень сохранён',
(Data[0] = Round(0.25 * 32767)) and (Data[1] = -Round(0.25 * 32767))); (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 finally
Rec.Free; Rec.Free;
end; end;
@@ -596,7 +610,7 @@ begin
R[0] := 0; R[0] := 0;
Rec.Feed(L, R, 1); Rec.Feed(L, R, 1);
end; end;
Data := Rec.Take; Data := Rec.Take(Held);
Check('рекордер: с начала записи, а не с конца', Check('рекордер: с начала записи, а не с конца',
(Length(Data) = 2000) and (Data[0] = 0) and (Length(Data) = 2000) and (Data[0] = 0) and
(Abs(Data[1998] - Round((999 / 4000.0) * 32767)) <= 1), (Abs(Data[1998] - Round((999 / 4000.0) * 32767)) <= 1),
@@ -611,7 +625,7 @@ begin
try try
for i := 0 to 8191 do begin L[i] := 0.25; R[i] := -0.25; end; for i := 0 to 8191 do begin L[i] := 0.25; R[i] := -0.25; end;
Rec.Feed(L, R, 8192); Rec.Feed(L, R, 8192);
Check('рекордер: до срока запись есть', Length(Rec.Take) > 0); Check('рекордер: до срока запись есть', Length(Rec.Take(Held)) > 0);
finally finally
Rec.Free; Rec.Free;
end; end;
@@ -619,9 +633,9 @@ begin
try try
Rec.Feed(L, R, 8192); Rec.Feed(L, R, 8192);
Sleep(1100); // окно закрылось Sleep(1100); // окно закрылось
Check('рекордер: после срока запись удалена', Length(Rec.Take) = 0); Check('рекордер: после срока запись удалена', Length(Rec.Take(Held)) = 0);
Rec.Feed(L, R, 8192); // и новое аудио уже не принимает Rec.Feed(L, R, 8192); // и новое аудио уже не принимает
Check('рекордер: истёкший не оживает', Length(Rec.Take) = 0); Check('рекордер: истёкший не оживает', Length(Rec.Take(Held)) = 0);
finally finally
Rec.Free; Rec.Free;
end; end;
@@ -654,7 +668,7 @@ begin
try try
Rec.Feed(L, R, 8192); Rec.Feed(L, R, 8192);
Check('бюджет: при отказе запись не рушится, а стоит пустой', Check('бюджет: при отказе запись не рушится, а стоит пустой',
Length(Rec.Take) = 0); Length(Rec.Take(Held)) = 0);
finally finally
Rec.Free; Rec.Free;
end; end;
@@ -683,7 +697,7 @@ begin
DeleteFile(Path); DeleteFile(Path);
SetLength(Data, 2000); SetLength(Data, 2000);
for i := 0 to 1999 do Data[i] := i * 8; 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; Waited := 0;
while (not FileExists(Path)) and (Waited < 2000) do while (not FileExists(Path)) and (Waited < 2000) do
begin begin
@@ -713,12 +727,28 @@ begin
finally finally
FS.Free; FS.Free;
end; 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 затирал // ★Существующий файл не трогаем: имя приходит из сети, и fmCreate затирал
// бы любой доступный процессу файл. Пишем поверх заведомо другой длиной — // бы любой доступный процессу файл. Пишем поверх заведомо другой длиной —
// файл обязан остаться прежним. // файл обязан остаться прежним.
SetLength(Data, 10); SetLength(Data, 10);
for i := 0 to 9 do Data[i] := 1; for i := 0 to 9 do Data[i] := 1;
TTCIWavWriter.Create(Path, Data, 48000); TTCIWavWriter.Create(Path, Data, 48000, 0);
Sleep(200); Sleep(200);
FS := TFileStream.Create(Path, fmOpenRead); FS := TFileStream.Create(Path, fmOpenRead);
try try