mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
+15
-9
@@ -2998,8 +2998,7 @@ procedure TTCIAdapter.CmdRecorder(Client: TTCIClient; const M: TTCIMessage);
|
|||||||
var
|
var
|
||||||
Rx, Sec, Rate: Integer;
|
Rx, Sec, Rate: Integer;
|
||||||
Path, Req: string;
|
Path, Req: string;
|
||||||
Data: TTCIPcm;
|
Rec: TTCIRecTake; // куски записи + их место в бюджете
|
||||||
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,8 +3084,10 @@ begin
|
|||||||
// Забираем рекордер из таблицы (сохранение завершает запись, §4.3) и только
|
// Забираем рекордер из таблицы (сохранение завершает запись, §4.3) и только
|
||||||
// потом снимаем с него данные: копия кольца — это десятки мегабайт, и делать
|
// потом снимаем с него данные: копия кольца — это десятки мегабайт, и делать
|
||||||
// её под локом DSP-потока нельзя.
|
// её под локом DSP-потока нельзя.
|
||||||
Data := nil;
|
Rec.Chunks := nil;
|
||||||
Held := 0;
|
Rec.Chunk := 0;
|
||||||
|
Rec.Count := 0;
|
||||||
|
Rec.Reserved := 0;
|
||||||
Rate := TCI_AUDIO_ENGINE_RATE;
|
Rate := TCI_AUDIO_ENGINE_RATE;
|
||||||
FStreamLock.Enter;
|
FStreamLock.Enter;
|
||||||
try
|
try
|
||||||
@@ -3097,20 +3098,25 @@ begin
|
|||||||
end;
|
end;
|
||||||
if Old <> nil then
|
if Old <> nil then
|
||||||
begin
|
begin
|
||||||
// ★Резерв бюджета переезжает вместе с данными: копию держит писатель, и
|
// ★Куски записи переезжают писателю КАК ЕСТЬ, вместе со своим местом в
|
||||||
// отпустит её он же. Иначе SAVE был бы дырой в потолке памяти.
|
// бюджете: сплошная копия удваивала бы пик, а невыпущенный резерв был бы
|
||||||
Data := Old.Take(Held);
|
// дырой в потолке.
|
||||||
|
Rec := Old.Take;
|
||||||
Rate := Old.Rate;
|
Rate := Old.Rate;
|
||||||
Old.Free;
|
Old.Free;
|
||||||
end;
|
end;
|
||||||
if Data = nil then
|
if Rec.Count <= 0 then
|
||||||
begin
|
begin
|
||||||
|
// Резерва тут быть неоткуда (кусок выделяется только под сэмпл, который
|
||||||
|
// тут же и пишется), но возвращаем на всякий случай: единственный путь,
|
||||||
|
// на котором запись не доходит до писателя.
|
||||||
|
TCIRecBudgetFree(Rec.Reserved);
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'nothing recorded']));
|
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'nothing recorded']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в
|
// Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в
|
||||||
// потоке клиента, который в это время не читает свой сокет.
|
// потоке клиента, который в это время не читает свой сокет.
|
||||||
TTCIWavWriter.Create(Path, Data, Rate, Held);
|
TTCIWavWriter.Create(Path, Rec, Rate);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.DropClientRecorders(C: TTCIClient);
|
procedure TTCIAdapter.DropClientRecorders(C: TTCIClient);
|
||||||
|
|||||||
+69
-54
@@ -52,6 +52,21 @@ const
|
|||||||
type
|
type
|
||||||
{ Накопленная запись: 16-битный PCM с чередованием L/R. }
|
{ Накопленная запись: 16-битный PCM с чередованием L/R. }
|
||||||
TTCIPcm = array of SmallInt;
|
TTCIPcm = array of SmallInt;
|
||||||
|
TTCIPcmChunks = array of TTCIPcm;
|
||||||
|
|
||||||
|
{ ★Забранная запись — КУСКИ КАК ЕСТЬ, без сплошной копии. Копия удваивала бы
|
||||||
|
пик памяти ровно в тот момент, когда её меньше всего: при полном бюджете
|
||||||
|
рядом жили бы 128 МБ кусков и 128 МБ копии, а счётчик показывал бы 128.
|
||||||
|
Куски переезжают к писателю вместе со своим местом в бюджете (Reserved),
|
||||||
|
и WAV собирается из них подряд — данные в нём и так лежат встык.
|
||||||
|
Chunk — сэмплов на канал в ПОЛНОМ куске (последний бывает неполным),
|
||||||
|
Count — сколько их занято всего. }
|
||||||
|
TTCIRecTake = record
|
||||||
|
Chunks: TTCIPcmChunks;
|
||||||
|
Chunk: Integer;
|
||||||
|
Count: Integer;
|
||||||
|
Reserved: Int64;
|
||||||
|
end;
|
||||||
|
|
||||||
{ Дециматор с целым коэффициентом. Прямая свёртка по линии задержки: выход
|
{ Дециматор с целым коэффициентом. Прямая свёртка по линии задержки: выход
|
||||||
считается только на нужной фазе, поэтому цена не зависит от M. }
|
считается только на нужной фазе, поэтому цена не зависит от M. }
|
||||||
@@ -196,7 +211,7 @@ type
|
|||||||
TTCIRecorder = class
|
TTCIRecorder = class
|
||||||
private
|
private
|
||||||
FLock: TCriticalSection;
|
FLock: TCriticalSection;
|
||||||
FChunks: array of TTCIPcm; // куски по FChunk сэмплов на канал, L/R вперемешку
|
FChunks: TTCIPcmChunks; // куски по FChunk сэмплов на канал, L/R вперемешку
|
||||||
FChunk: Integer; // сэмплов на канал в куске
|
FChunk: Integer; // сэмплов на канал в куске
|
||||||
FCap: Integer; // потолок в сэмплах на канал
|
FCap: Integer; // потолок в сэмплах на канал
|
||||||
FCount: Integer; // накоплено сэмплов на канал
|
FCount: Integer; // накоплено сэмплов на канал
|
||||||
@@ -213,13 +228,13 @@ type
|
|||||||
constructor Create(ARx, ARateHz, AMaxSec: Integer; AOwner: TObject);
|
constructor Create(ARx, ARateHz, AMaxSec: Integer; AOwner: TObject);
|
||||||
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 — либо не записано ничего,
|
{ Забрать накопленное и завершить запись. Count = 0 — либо не записано
|
||||||
либо окно уже истекло (по документу это одно и то же: записи нет).
|
ничего, либо окно уже истекло (по документу это одно и то же: записи
|
||||||
★Reserved — сколько байт бюджета УХОДИТ ВМЕСТЕ С ДАННЫМИ: копия живёт
|
нет). ★Вместе с кусками уходит и их место в бюджете (Reserved): пока
|
||||||
дальше в писателе, и пока он её не отпустит, она обязана оставаться
|
писатель их не отпустит, они обязаны оставаться учтёнными. Вызывающий
|
||||||
учтённой. Вызывающий передаёт это число писателю (или возвращает сам
|
передаёт запись писателю — или сам возвращает Reserved через
|
||||||
через TCIRecBudgetFree, если писателя не будет). }
|
TCIRecBudgetFree, если писателя не будет. }
|
||||||
function Take(out Reserved: Int64): TTCIPcm;
|
function Take: TTCIRecTake;
|
||||||
{ Окно закрылось по часам. Спрашивает тик сервера: у мёртвого приёмника
|
{ Окно закрылось по часам. Спрашивает тик сервера: у мёртвого приёмника
|
||||||
Feed не зовут вовсе, и без этого вопроса память жила бы до Stop. }
|
Feed не зовут вовсе, и без этого вопроса память жила бы до Stop. }
|
||||||
function Expired: Boolean;
|
function Expired: Boolean;
|
||||||
@@ -230,22 +245,21 @@ type
|
|||||||
|
|
||||||
{ Писатель WAV в своём потоке: файл до 60 МБ, а зовут сохранение из тика
|
{ Писатель WAV в своём потоке: файл до 60 МБ, а зовут сохранение из тика
|
||||||
сервера — блокировать его на секунду диска нельзя. Данные забирает себе
|
сервера — блокировать его на секунду диска нельзя. Данные забирает себе
|
||||||
ВМЕСТЕ С ИХ РЕЗЕРВОМ в общем бюджете (AReserved из TTCIRecorder.Take) и
|
КУСКАМИ, вместе с их местом в общем бюджете (TTCIRecTake.Reserved), и
|
||||||
отпускает его, только когда данные больше не нужны. Пока писатель ждёт
|
отпускает его, только когда они больше не нужны. Пока писатель ждёт
|
||||||
медленный диск, эти байты остаются занятыми — так потолок и держит
|
медленный диск, эти байты остаются занятыми — так потолок и держит
|
||||||
очередь сохранений, а не только сами записи. }
|
очередь сохранений, а не только сами записи. }
|
||||||
TTCIWavWriter = class(TThread)
|
TTCIWavWriter = class(TThread)
|
||||||
private
|
private
|
||||||
FPath: string;
|
FPath: string;
|
||||||
FData: TTCIPcm;
|
FRec: TTCIRecTake;
|
||||||
FRate: Integer;
|
FRate: Integer;
|
||||||
FReserved: Int64;
|
|
||||||
procedure ReleaseData;
|
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 ARec: TTCIRecTake;
|
||||||
ARateHz: Integer; AReserved: Int64);
|
ARateHz: Integer);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Коэффициент прореживания SrcRate → WantRate: наибольший целый делитель,
|
{ Коэффициент прореживания SrcRate → WantRate: наибольший целый делитель,
|
||||||
@@ -853,44 +867,33 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIRecorder.Take(out Reserved: Int64): TTCIPcm;
|
function TTCIRecorder.Take: TTCIRecTake;
|
||||||
var
|
|
||||||
C, n, Left: Integer;
|
|
||||||
Copy_: Int64;
|
|
||||||
begin
|
begin
|
||||||
Result := nil;
|
Result.Chunks := nil;
|
||||||
Reserved := 0;
|
Result.Chunk := FChunk;
|
||||||
|
Result.Count := 0;
|
||||||
|
Result.Reserved := 0;
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
try
|
try
|
||||||
// Проверка срока и здесь: аудио могло не идти вовсе (мьют, стоящий
|
// Проверка срока и здесь: аудио могло не идти вовсе (мьют, стоящий
|
||||||
// приёмник), и тогда Feed часы не смотрел ни разу.
|
// приёмник), и тогда Feed часы не смотрел ни разу.
|
||||||
if (not FExpired) and (GetTickCount64 >= FEndsAt) then DropData;
|
if (not FExpired) and (GetTickCount64 >= FEndsAt) then DropData;
|
||||||
if FExpired or (FCount <= 0) then Exit;
|
if FExpired or (FCount <= 0) then
|
||||||
// Склейка кусков в один буфер — единственное копирование за всю запись, и
|
|
||||||
// оно идёт уже вне DSP-потока: SAVE вынимает рекордер из таблицы раньше,
|
|
||||||
// чем зовёт Take, так что кормить его больше некому.
|
|
||||||
// ★Копия НЕ берёт у бюджета новых денег и НЕ отпускает старых: она
|
|
||||||
// наследует резерв кусков, из которых собрана. Иначе SAVE был бы дырой в
|
|
||||||
// потолке — DropData возвращал бы куски сразу, а копия жила бы дальше в
|
|
||||||
// писателе неучтённой, и на медленном (или зависшем) сетевом каталоге
|
|
||||||
// writer-потоки копились бы без всякой границы.
|
|
||||||
Copy_ := Int64(FCount) * 2 * SizeOf(SmallInt);
|
|
||||||
if Copy_ > FBytes then Copy_ := FBytes; // не отдать больше, чем занято
|
|
||||||
SetLength(Result, FCount * 2);
|
|
||||||
Left := FCount;
|
|
||||||
for C := 0 to High(FChunks) do
|
|
||||||
begin
|
begin
|
||||||
if Left <= 0 then Break;
|
DropData; // SAVE завершает запись в любом случае (§4.3)
|
||||||
n := FChunk;
|
Exit;
|
||||||
if n > Left then n := Left;
|
|
||||||
Move(FChunks[C][0], Result[(FCount - Left) * 2], n * 2 * SizeOf(SmallInt));
|
|
||||||
Dec(Left, n);
|
|
||||||
end;
|
end;
|
||||||
// Из общего резерва оставляем ровно то, что перешло в копию, остальное
|
// ★Куски отдаём КАК ЕСТЬ — ни одного лишнего байта. Сплошная копия жила бы
|
||||||
// (хвост последнего куска) возвращаем — этим и займётся DropData.
|
// рядом с ними до самого DropData и удваивала пик (см. TTCIRecTake).
|
||||||
Reserved := Copy_;
|
Result.Chunks := FChunks;
|
||||||
Dec(FBytes, Copy_);
|
Result.Count := FCount;
|
||||||
DropData; // SAVE завершает запись (§4.3)
|
Result.Reserved := FBytes;
|
||||||
|
// Дальше — то же, что делает DropData, но БЕЗ возврата бюджета и без
|
||||||
|
// освобождения кусков: и то и другое переехало к писателю.
|
||||||
|
FChunks := nil;
|
||||||
|
FBytes := 0;
|
||||||
|
FCount := 0;
|
||||||
|
FExpired := True;
|
||||||
finally
|
finally
|
||||||
FLock.Leave;
|
FLock.Leave;
|
||||||
end;
|
end;
|
||||||
@@ -901,24 +904,25 @@ end;
|
|||||||
═══════════════════════════════════════════════════════════════════════════ }
|
═══════════════════════════════════════════════════════════════════════════ }
|
||||||
|
|
||||||
constructor TTCIWavWriter.Create(const APath: string;
|
constructor TTCIWavWriter.Create(const APath: string;
|
||||||
const AData: TTCIPcm; ARateHz: Integer; AReserved: Int64);
|
const ARec: TTCIRecTake; ARateHz: Integer);
|
||||||
begin
|
begin
|
||||||
inherited Create(True);
|
inherited Create(True);
|
||||||
FreeOnTerminate := True;
|
FreeOnTerminate := True;
|
||||||
FPath := APath;
|
FPath := APath;
|
||||||
FData := AData;
|
FRec := ARec;
|
||||||
FRate := ARateHz;
|
FRate := ARateHz;
|
||||||
FReserved := AReserved;
|
|
||||||
Start;
|
Start;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIWavWriter.ReleaseData;
|
procedure TTCIWavWriter.ReleaseData;
|
||||||
// Данные и их место в бюджете уходят вместе и ровно один раз — сколько бы
|
// Данные и их место в бюджете уходят вместе и ровно один раз — сколько бы
|
||||||
// путей выхода ни было у Execute.
|
// путей выхода ни было у Execute.
|
||||||
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
FData := nil;
|
for i := 0 to High(FRec.Chunks) do FRec.Chunks[i] := nil;
|
||||||
TCIRecBudgetFree(FReserved);
|
FRec.Chunks := nil;
|
||||||
FReserved := 0;
|
TCIRecBudgetFree(FRec.Reserved);
|
||||||
|
FRec.Reserved := 0;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIWavWriter.Execute;
|
procedure TTCIWavWriter.Execute;
|
||||||
@@ -930,6 +934,7 @@ var
|
|||||||
H: THandle;
|
H: THandle;
|
||||||
Hdr: array[0..43] of Byte;
|
Hdr: array[0..43] of Byte;
|
||||||
DataBytes: LongWord;
|
DataBytes: LongWord;
|
||||||
|
i, n, Left: Integer;
|
||||||
|
|
||||||
procedure PutTag(Ofs: Integer; const Tag: string);
|
procedure PutTag(Ofs: Integer; const Tag: string);
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
@@ -950,7 +955,7 @@ var
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
try
|
try
|
||||||
DataBytes := LongWord(Length(FData) * SizeOf(SmallInt));
|
DataBytes := LongWord(Int64(FRec.Count) * 2 * SizeOf(SmallInt));
|
||||||
FillChar(Hdr, SizeOf(Hdr), 0);
|
FillChar(Hdr, SizeOf(Hdr), 0);
|
||||||
PutTag(0, 'RIFF');
|
PutTag(0, 'RIFF');
|
||||||
PutU32(4, 36 + DataBytes);
|
PutU32(4, 36 + DataBytes);
|
||||||
@@ -976,7 +981,17 @@ begin
|
|||||||
FS := THandleStream.Create(H);
|
FS := THandleStream.Create(H);
|
||||||
try
|
try
|
||||||
FS.Write(Hdr[0], SizeOf(Hdr));
|
FS.Write(Hdr[0], SizeOf(Hdr));
|
||||||
if DataBytes > 0 then FS.Write(FData[0], DataBytes);
|
// Куски пишем подряд: в WAV сэмплы и так лежат встык, а хвост
|
||||||
|
// последнего куска за FRec.Count — не наши данные.
|
||||||
|
Left := FRec.Count;
|
||||||
|
for i := 0 to High(FRec.Chunks) do
|
||||||
|
begin
|
||||||
|
if Left <= 0 then Break;
|
||||||
|
n := FRec.Chunk;
|
||||||
|
if n > Left then n := Left;
|
||||||
|
FS.Write(FRec.Chunks[i][0], n * 2 * SizeOf(SmallInt));
|
||||||
|
Dec(Left, n);
|
||||||
|
end;
|
||||||
finally
|
finally
|
||||||
FS.Free;
|
FS.Free;
|
||||||
FileClose(H);
|
FileClose(H);
|
||||||
|
|||||||
+14
-9
@@ -529,13 +529,16 @@ ExpertSDR3 давно бы не было.
|
|||||||
Спрашивается при выделении каждого куска. Отказ не рушит запись: набранное
|
Спрашивается при выделении каждого куска. Отказ не рушит запись: набранное
|
||||||
остаётся сохраняемым, просто дальше она не растёт — иначе клиент терял бы
|
остаётся сохраняемым, просто дальше она не растёт — иначе клиент терял бы
|
||||||
уже записанное из-за чужой записи на другом приёмнике. ★`SAVE` бюджет не
|
уже записанное из-за чужой записи на другом приёмнике. ★`SAVE` бюджет не
|
||||||
обходит: `Take` собирает куски в сплошную копию и **передаёт ей их резерв**
|
обходит **и не удваивает пик**: `Take` отдаёт писателю САМИ КУСКИ
|
||||||
(out-параметр `Reserved`), а не возвращает его сразу — копия живёт дальше в
|
(`TTCIRecTake`) вместе с их местом в бюджете, а не сплошную копию. Копия
|
||||||
`TTCIWavWriter`, и отпускает место он же, когда данные больше не нужны
|
была бы худшим из вариантов ровно там, где памяти меньше всего: при полном
|
||||||
(`ReleaseData` — ровно один раз, сколько бы путей выхода ни было у
|
бюджете рядом жили бы 128 МБ кусков и 128 МБ копии, а счётчик показывал бы
|
||||||
`Execute`). Иначе один `SAVE` поднимал бы настоящее потребление вдвое, а на
|
128. Куски и так лежат встык, поэтому `TTCIWavWriter` пишет их в файл
|
||||||
медленном или зависшем сетевом каталоге очередь writer-потоков росла бы
|
подряд (хвост последнего за `Count` — не данные), а место отпускает в
|
||||||
мимо потолка вовсе.
|
`ReleaseData` — ровно один раз, сколько бы путей выхода ни было у
|
||||||
|
`Execute`. Пока писатель ждёт медленный или зависший сетевой каталог, эти
|
||||||
|
байты остаются занятыми: потолок держит и очередь сохранений, а не только
|
||||||
|
сами записи.
|
||||||
3. **Освобождение по трём событиям, а не по одному.** Раньше срок проверял
|
3. **Освобождение по трём событиям, а не по одному.** Раньше срок проверял
|
||||||
только `Feed`, то есть DSP-поток — а к мёртвому приёмнику он не приходит
|
только `Feed`, то есть DSP-поток — а к мёртвому приёмнику он не приходит
|
||||||
никогда, и `START` на несуществующий номер оставлял память навсегда.
|
никогда, и `START` на несуществующий номер оставлял память навсегда.
|
||||||
@@ -868,8 +871,10 @@ ExpertSDR3 давно бы не было.
|
|||||||
выделяет ни байта, бюджет растёт кусками по мере звука и возвращается по
|
выделяет ни байта, бюджет растёт кусками по мере звука и возвращается по
|
||||||
`Free`, потолок берётся целиком и сверх него следует отказ, при отказе запись
|
`Free`, потолок берётся целиком и сверх него следует отказ, при отказе запись
|
||||||
не рушится, окно истекает по часам **без единого `Feed`** и отдаёт память
|
не рушится, окно истекает по часам **без единого `Feed`** и отдаёт память
|
||||||
само, `Take` отдаёт резерв размером с копию и после него занят ровно он, а
|
само, `Take` отдаёт САМИ КУСКИ (2.5 с записи = три куска по секунде, а не
|
||||||
писатель возвращает этот резерв по окончании записи. ★Имя файла: простое имя ложится в каталог записей, а каталог из
|
один свёрнутый — сплошной копии не появляется ни на миг), счёт бюджета при
|
||||||
|
этом не меняется, склеенные куски дают непрерывный звук, и писатель
|
||||||
|
возвращает резерв по окончании записи. ★Имя файла: простое имя ложится в каталог записей, а каталог из
|
||||||
просьбы отбрасывается — абсолютный путь, `..` и буква диска наружу не
|
просьбы отбрасывается — абсолютный путь, `..` и буква диска наружу не
|
||||||
выводят; пусто, `..`, не-`.wav`, управляющий символ и отсутствие каталога
|
выводят; пусто, `..`, не-`.wav`, управляющий символ и отсутствие каталога
|
||||||
записей дают отказ; существующий файл писатель не перезаписывает.
|
записей дают отказ; существующий файл писатель не перезаписывает.
|
||||||
|
|||||||
+97
-18
@@ -550,6 +550,26 @@ end;
|
|||||||
C. Рекордер и WAV
|
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;
|
procedure TestRecorder;
|
||||||
var
|
var
|
||||||
Rec: TTCIRecorder;
|
Rec: TTCIRecorder;
|
||||||
@@ -562,7 +582,8 @@ var
|
|||||||
Sz: LongWord;
|
Sz: LongWord;
|
||||||
W: TTCIWavWriter;
|
W: TTCIWavWriter;
|
||||||
Waited: Integer;
|
Waited: Integer;
|
||||||
Was, Held: Int64;
|
Was: Int64;
|
||||||
|
Tk: TTCIRecTake;
|
||||||
begin
|
begin
|
||||||
WriteLn('C. Рекордер линейного выхода');
|
WriteLn('C. Рекордер линейного выхода');
|
||||||
|
|
||||||
@@ -578,29 +599,69 @@ begin
|
|||||||
Rec.Feed(L, R, 8192);
|
Rec.Feed(L, R, 8192);
|
||||||
Inc(Total, 8192);
|
Inc(Total, 8192);
|
||||||
end;
|
end;
|
||||||
Data := Rec.Take(Held);
|
Tk := Rec.Take; Data := FlatTake(Tk);
|
||||||
Check('рекордер: длина по максимуму', Length(Data) = 48000 * 2,
|
Check('рекордер: длина по максимуму', Length(Data) = 48000 * 2,
|
||||||
IntToStr(Length(Data)));
|
IntToStr(Length(Data)));
|
||||||
// ★Take отдаёт данные ВМЕСТЕ с их местом в бюджете: копия живёт дальше в
|
// ★Take отдаёт данные ВМЕСТЕ с их местом в бюджете: копия живёт дальше в
|
||||||
// писателе, и до конца записи она обязана оставаться учтённой. Иначе SAVE
|
// писателе, и до конца записи она обязана оставаться учтённой. Иначе SAVE
|
||||||
// был бы дырой в потолке — куски вернулись бы сразу, а копия висела бы
|
// был бы дырой в потолке — куски вернулись бы сразу, а копия висела бы
|
||||||
// неучтённой, и очередь сохранений на медленном диске росла бы без границ.
|
// неучтённой, и очередь сохранений на медленном диске росла бы без границ.
|
||||||
Check('рекордер: Take передаёт резерв размером с копию',
|
// ★Куски отдаются КАК ЕСТЬ: сплошной копии нет ни на миг, поэтому и пика
|
||||||
Held = Int64(Length(Data)) * SizeOf(SmallInt),
|
// в два бюджета нет. Резерв переезжает вместе с ними — целиком.
|
||||||
IntToStr(Held));
|
Check('рекордер: Take отдаёт сами куски, не копию',
|
||||||
Check('рекордер: после Take занят ровно резерв копии',
|
(Length(Tk.Chunks) > 0) and (Tk.Count = Length(Data) div 2),
|
||||||
TCIRecBudgetUsed = Was + Held,
|
IntToStr(Length(Tk.Chunks)));
|
||||||
IntToStr(TCIRecBudgetUsed - Was));
|
Check('рекордер: резерв переехал целиком, счёт не изменился',
|
||||||
TCIRecBudgetFree(Held); // как это сделает писатель
|
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('рекордер: возврат резерва закрывает счёт',
|
Check('рекордер: возврат резерва закрывает счёт',
|
||||||
TCIRecBudgetUsed = Was, IntToStr(TCIRecBudgetUsed - Was));
|
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(Held)) = 0);
|
Check('рекордер: Take завершает запись', Length(FlatTake(Rec.Take)) = 0);
|
||||||
finally
|
finally
|
||||||
Rec.Free;
|
Rec.Free;
|
||||||
end;
|
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 отсчётов
|
Rec := TTCIRecorder.Create(0, 1000, 1, nil); // ёмкость 1000 отсчётов
|
||||||
try
|
try
|
||||||
@@ -610,7 +671,7 @@ begin
|
|||||||
R[0] := 0;
|
R[0] := 0;
|
||||||
Rec.Feed(L, R, 1);
|
Rec.Feed(L, R, 1);
|
||||||
end;
|
end;
|
||||||
Data := Rec.Take(Held);
|
Tk := Rec.Take; Data := FlatTake(Tk);
|
||||||
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),
|
||||||
@@ -625,7 +686,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(Held)) > 0);
|
Check('рекордер: до срока запись есть', Length(FlatTake(Rec.Take)) > 0);
|
||||||
finally
|
finally
|
||||||
Rec.Free;
|
Rec.Free;
|
||||||
end;
|
end;
|
||||||
@@ -633,9 +694,9 @@ begin
|
|||||||
try
|
try
|
||||||
Rec.Feed(L, R, 8192);
|
Rec.Feed(L, R, 8192);
|
||||||
Sleep(1100); // окно закрылось
|
Sleep(1100); // окно закрылось
|
||||||
Check('рекордер: после срока запись удалена', Length(Rec.Take(Held)) = 0);
|
Check('рекордер: после срока запись удалена', Length(FlatTake(Rec.Take)) = 0);
|
||||||
Rec.Feed(L, R, 8192); // и новое аудио уже не принимает
|
Rec.Feed(L, R, 8192); // и новое аудио уже не принимает
|
||||||
Check('рекордер: истёкший не оживает', Length(Rec.Take(Held)) = 0);
|
Check('рекордер: истёкший не оживает', Length(FlatTake(Rec.Take)) = 0);
|
||||||
finally
|
finally
|
||||||
Rec.Free;
|
Rec.Free;
|
||||||
end;
|
end;
|
||||||
@@ -668,7 +729,7 @@ begin
|
|||||||
try
|
try
|
||||||
Rec.Feed(L, R, 8192);
|
Rec.Feed(L, R, 8192);
|
||||||
Check('бюджет: при отказе запись не рушится, а стоит пустой',
|
Check('бюджет: при отказе запись не рушится, а стоит пустой',
|
||||||
Length(Rec.Take(Held)) = 0);
|
Length(FlatTake(Rec.Take)) = 0);
|
||||||
finally
|
finally
|
||||||
Rec.Free;
|
Rec.Free;
|
||||||
end;
|
end;
|
||||||
@@ -697,7 +758,13 @@ 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, 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;
|
Waited := 0;
|
||||||
while (not FileExists(Path)) and (Waited < 2000) do
|
while (not FileExists(Path)) and (Waited < 2000) do
|
||||||
begin
|
begin
|
||||||
@@ -733,7 +800,13 @@ begin
|
|||||||
Was := TCIRecBudgetUsed;
|
Was := TCIRecBudgetUsed;
|
||||||
Check('WAV: резерв под писателя взят', TCIRecBudgetTake(4096));
|
Check('WAV: резерв под писателя взят', TCIRecBudgetTake(4096));
|
||||||
DeleteFile(Path);
|
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;
|
Waited := 0;
|
||||||
while (TCIRecBudgetUsed <> Was) and (Waited < 2000) do
|
while (TCIRecBudgetUsed <> Was) and (Waited < 2000) do
|
||||||
begin
|
begin
|
||||||
@@ -748,7 +821,13 @@ begin
|
|||||||
// файл обязан остаться прежним.
|
// файл обязан остаться прежним.
|
||||||
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, 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);
|
Sleep(200);
|
||||||
FS := TFileStream.Create(Path, fmOpenRead);
|
FS := TFileStream.Create(Path, fmOpenRead);
|
||||||
try
|
try
|
||||||
|
|||||||
Reference in New Issue
Block a user