mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
+17
-5
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|
||||||
{ ═══════════════════════════════════════════════════════════════════════════
|
{ ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user