mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
Последняя невыполненная команда протокола. Ключ к ней в том, что arg3 — длительность интервала, который ТОЛЬКО ЧТО кончился, а не начинающегося. Документ задаёт это алгоритмом: первое нажатие keyer:0,true,0, отпускание keyer:0,false,142 («посылка длилась 142 мс»), следующее нажатие keyer:0,true,58 («пауза длилась 58 мс»). Отсюда перевод: тип элемента — это состояние ключа ДО фронта, то есть обратное пришедшему; arg3 = 0 играть нечего. Почему не дёргать ключ по приходу пакета: приход говорит, что интервал кончился, а не сколько он длился, и манипуляция «по приходу» — это сетевой джиттер прямо в эфир, тот самый «пьяный матрос», ради которого третий аргумент в протоколе и появился. Новый TCWElemPlayer (CWMorse.pas) держит очередь элементов и играет их подряд по абсолютным дедлайнам: сумма длительностей равна времени у клиента, значит отставание постоянно (сеть + один элемент) и не накапливается. Очередь опустела — ключ отпускается и следующая пачка начинается с чистого дедлайна: элементы чередуются, искажения нет, зато оборвавшийся клиент не оставляет в эфире несущую. Контроллер: CWKeyerElement(Mark, Ms) ставит элемент в очередь, CWElemKey раздаёт фронты — чужая манипуляция это прямой ключ с точными длительностями, поэтому у Pluto она идёт во вход прямого ключа локального генератора (он сам поднимает сессию, рисует огибающую и сайдтон), а у openHPSDR в бит CWX прошивки (тем же путём идёт передача текста). Гейт CWTXActive, как у CWXSend; обрыв общий с текстом — касание манипулятора, снятие MOX и уход из телеграфа гасят чужую манипуляцию тем же CWXAbort. Передачу KEYER не поднимает: при break-in PTT даёт прошивка (или сессия генератора), без него оператор держит MOX сам. Адаптер: номер передатчика разбирается как у TRX (bad receiver / receiver is not running), захват §3.5 общий с TRX — передатчик один, и ключ держит тот же, кто держит эфир. Паузы обрезаются TCI_KEYER_GAP_MAX_MS = 1 с (пауза целиком прибавляется к отставанию от клиента, а дольше секунды — это «оператор задумался», и честнее догнать реальное время), посылки — 5 с. Стенд test/tci: 238 проверок (было 219). Новая часть C2 меряет ДЛИТЕЛЬНОСТИ по фронтам ключа (посылка 150 / пауза 60 / посылка 150, допуск 30 мс), проверяет отпускание на пустой очереди, старт следующей пачки без «догона» дедлайна, обрыв и нулевую длительность; в части D — разбор аргументов команды и сквозная проверка, что keyer:0,true,<мс> ключ не замыкает (это пауза), а keyer:0,false,<мс> замыкает. Негативный контроль на инверсию перевода. ★Локальный генератор вооружается только при живом устройстве, поэтому сквозная проверка подставляет FDevConnected/FRunning на время. doc/TCI.md: §2.6 описывает команду целиком, §3.1 и §4 переписаны под то, что из пары KEYER/TX_FOOTSWITCH остался только второй; попутно убран устаревший абзац §2.3 про «потоки — этап 2». На железе с настоящим ключом по сети ещё не гонялось. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
604 lines
20 KiB
ObjectPascal
604 lines
20 KiB
ObjectPascal
unit CWMorse;
|
||
|
||
{$mode objfpc}{$H+}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Программная передача телеграфа: текст → точки/тире → ключ.
|
||
//
|
||
// Кто держит тайминги. У openHPSDR P2 есть ДВА пути в эфир:
|
||
// * манипулятор в разъёме трансивера — элементы формирует кейер в FPGA, PC
|
||
// только отдаёт ему скорость/вес (DUC Specific байт 5..10). Джиттера нет;
|
||
// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по
|
||
// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА
|
||
// нажать и отпустить, решает PC. Именно этот путь используется для
|
||
// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs).
|
||
//
|
||
// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и
|
||
// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка
|
||
// каждого сна накапливалась бы и знак «плыл» бы по длине.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, SyncObjs;
|
||
|
||
type
|
||
// Вызывается из потока передачи на каждом фронте ключа.
|
||
TCWKeyEvent = procedure(Down: Boolean) of object;
|
||
|
||
TCWSender = class(TThread)
|
||
private
|
||
FKeyEvent: TCWKeyEvent;
|
||
FLock: TCriticalSection;
|
||
FWake: TEvent;
|
||
FPending: string; // очередь текста (под FLock)
|
||
FAbortReq: Boolean; // «бросить и отпустить ключ» (под FLock)
|
||
FBusy: Boolean; // идёт передача (под FLock)
|
||
FWPM: Integer; // под FLock
|
||
FWeight: Integer; // 33..66, 50 = симметрично (под FLock)
|
||
// Эхо для окна-терминала: ушедшее в эфир и хвост очереди (под FLock).
|
||
FSent: string;
|
||
FUnsent: string;
|
||
FTrimTail: Boolean; // Backspace дотянулся до уже взятого текста
|
||
function TakeText: string;
|
||
function Aborted: Boolean;
|
||
procedure Key(Down: Boolean);
|
||
// Выдержать Ms миллисекунд, просыпаясь дробно ради отзывчивости на abort.
|
||
// Возвращает False, если передачу оборвали.
|
||
function Hold(Deadline: QWord): Boolean;
|
||
procedure SendChar(Ch: Char; var Deadline: QWord);
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(AKeyEvent: TCWKeyEvent);
|
||
destructor Destroy; override;
|
||
procedure Enqueue(const S: string);
|
||
procedure AbortSending;
|
||
procedure SetSpeed(WPM, Weight: Integer);
|
||
function Busy: Boolean;
|
||
// Терминал: знаки, ушедшие в эфир с прошлого опроса; хвост очереди; стереть
|
||
// последний ненабранный знак. Смысл тот же, что у TCWLocalKeyer.
|
||
function TakeSentText: string;
|
||
function PendingText: string;
|
||
function Backspace: Boolean;
|
||
end;
|
||
|
||
{ Элемент манипуляции с ЗАДАННОЙ длительностью: посылка (Mark = True) или
|
||
пауза. Из таких элементов состоит чужая манипуляция, пришедшая по сети —
|
||
у неё нет ни скорости, ни веса, есть только измеренные интервалы. }
|
||
TCWElement = record
|
||
Mark: Boolean;
|
||
Ms: Integer;
|
||
end;
|
||
|
||
{ Проигрыватель чужой манипуляции: очередь элементов → тот же колбэк ключа.
|
||
|
||
ЗАЧЕМ ОТДЕЛЬНО ОТ TCWSender. Текст мы разбираем сами и знаем скорость; здесь
|
||
наоборот — знаки не разбираются вовсе, а длительности приходят готовыми
|
||
(команда TCI KEYER: клиент замеряет свой ключ и присылает длину КАЖДОГО
|
||
завершившегося интервала). Проиграть их надо ровно так, как они были нажаты.
|
||
|
||
★Почему нельзя просто дёргать ключ по приходу пакета. Между клиентом и нами
|
||
сеть: приход пакета говорит, что интервал КОНЧИЛСЯ, а не сколько он длился.
|
||
Манипуляция «по приходу» — это сетевой джиттер прямо в эфир, тот самый
|
||
«пьяный матрос», ради которого в протоколе и заведён третий аргумент.
|
||
Поэтому элементы становятся в очередь и играются ПОДРЯД по абсолютным
|
||
дедлайнам: сумма длительностей равна времени у клиента, значит отставание
|
||
постоянно (сеть + один элемент) и не накапливается.
|
||
|
||
Очередь опустела — ключ отпускаем и начинаем следующую пачку с чистого
|
||
дедлайна: элементы всегда чередуются (посылка, пауза, посылка…), так что
|
||
отпускание на пустой очереди ничего не искажает, зато не оставляет в эфире
|
||
несущую, если клиент замолчал или отвалился. }
|
||
TCWElemPlayer = class(TThread)
|
||
private
|
||
FKeyEvent: TCWKeyEvent;
|
||
FLock: TCriticalSection;
|
||
FWake: TEvent;
|
||
FQ: array of TCWElement; // кольцо (под FLock)
|
||
FHead: Integer;
|
||
FCount: Integer;
|
||
FAbortReq: Boolean;
|
||
FBusy: Boolean;
|
||
FDown: Boolean; // состояние ключа (только поток)
|
||
procedure Key(Down: Boolean);
|
||
function Take(out E: TCWElement): Boolean;
|
||
function Aborted: Boolean;
|
||
function Hold(Deadline: QWord): Boolean;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(AKeyEvent: TCWKeyEvent);
|
||
destructor Destroy; override;
|
||
{ Поставить элемент в очередь. Слишком длинные обрезаются вызывающим —
|
||
здесь принимается то, что дали. }
|
||
procedure Enqueue(Mark: Boolean; Ms: Integer);
|
||
{ Бросить очередь и отпустить ключ (оператор тронул манипулятор, снят MOX). }
|
||
procedure AbortPlay;
|
||
function Busy: Boolean;
|
||
function Pending: Integer;
|
||
end;
|
||
|
||
// Сколько элементов держим в очереди. Живая манипуляция даёт ~30 элементов в
|
||
// секунду, так что это секунды звука: клиент, сыплющий быстрее, чем играется,
|
||
// нам не друг, и лишнее просто отбрасывается.
|
||
const
|
||
CW_ELEM_QUEUE_MAX = 512;
|
||
|
||
// Код знака: строка из '.' и '-'. Пустая — знака нет в таблице (пропускаем,
|
||
// иначе опечатка в макросе превратилась бы в мусор в эфире).
|
||
function MorseCode(Ch: Char): string;
|
||
|
||
implementation
|
||
|
||
const
|
||
// Точка при 1 WPM = 1200 мс (стандарт PARIS).
|
||
DIT_MS_AT_1WPM = 1200;
|
||
|
||
function MorseCode(Ch: Char): string;
|
||
begin
|
||
case UpCase(Ch) of
|
||
'A': Result := '.-'; 'B': Result := '-...'; 'C': Result := '-.-.';
|
||
'D': Result := '-..'; 'E': Result := '.'; 'F': Result := '..-.';
|
||
'G': Result := '--.'; 'H': Result := '....'; 'I': Result := '..';
|
||
'J': Result := '.---'; 'K': Result := '-.-'; 'L': Result := '.-..';
|
||
'M': Result := '--'; 'N': Result := '-.'; 'O': Result := '---';
|
||
'P': Result := '.--.'; 'Q': Result := '--.-'; 'R': Result := '.-.';
|
||
'S': Result := '...'; 'T': Result := '-'; 'U': Result := '..-';
|
||
'V': Result := '...-'; 'W': Result := '.--'; 'X': Result := '-..-';
|
||
'Y': Result := '-.--'; 'Z': Result := '--..';
|
||
'0': Result := '-----'; '1': Result := '.----'; '2': Result := '..---';
|
||
'3': Result := '...--'; '4': Result := '....-'; '5': Result := '.....';
|
||
'6': Result := '-....'; '7': Result := '--...'; '8': Result := '---..';
|
||
'9': Result := '----.';
|
||
'.': Result := '.-.-.-'; ',': Result := '--..--'; '?': Result := '..--..';
|
||
'/': Result := '-..-.'; '=': Result := '-...-'; '+': Result := '.-.-.';
|
||
'-': Result := '-....-'; '(': Result := '-.--.'; ')': Result := '-.--.-';
|
||
':': Result := '---...'; '''':Result := '.----.'; '"': Result := '.-..-.';
|
||
'@': Result := '.--.-.'; '!': Result := '-.-.--'; ';': Result := '-.-.-.';
|
||
'$': Result := '...-..-';
|
||
else
|
||
Result := '';
|
||
end;
|
||
end;
|
||
|
||
constructor TCWSender.Create(AKeyEvent: TCWKeyEvent);
|
||
begin
|
||
FKeyEvent := AKeyEvent;
|
||
FLock := TCriticalSection.Create;
|
||
FWake := TEvent.Create(nil, False, False, '');
|
||
FWPM := 20;
|
||
FWeight := 50;
|
||
FreeOnTerminate := False;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
destructor TCWSender.Destroy;
|
||
begin
|
||
Terminate;
|
||
FWake.SetEvent;
|
||
WaitFor;
|
||
FWake.Free;
|
||
FLock.Free;
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TCWSender.Enqueue(const S: string);
|
||
begin
|
||
if S = '' then Exit;
|
||
FLock.Enter;
|
||
try
|
||
FAbortReq := False;
|
||
FPending := FPending + S;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
FWake.SetEvent;
|
||
end;
|
||
|
||
procedure TCWSender.AbortSending;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FPending := '';
|
||
FAbortReq := True;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
FWake.SetEvent;
|
||
end;
|
||
|
||
procedure TCWSender.SetSpeed(WPM, Weight: Integer);
|
||
begin
|
||
if WPM < 5 then WPM := 5;
|
||
if WPM > 60 then WPM := 60;
|
||
if Weight < 33 then Weight := 33;
|
||
if Weight > 66 then Weight := 66;
|
||
FLock.Enter;
|
||
try
|
||
FWPM := WPM;
|
||
FWeight := Weight;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWSender.Busy: Boolean;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FBusy or (FPending <> '');
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWSender.TakeSentText: string;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FSent;
|
||
FSent := '';
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWSender.PendingText: string;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FUnsent + FPending;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWSender.Backspace: Boolean;
|
||
// Стираем с хвоста очереди: то, что уже звучит, вернуть нельзя.
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := False;
|
||
if FPending <> '' then
|
||
begin
|
||
SetLength(FPending, Length(FPending) - 1);
|
||
Result := True;
|
||
end
|
||
else if FUnsent <> '' then
|
||
begin
|
||
SetLength(FUnsent, Length(FUnsent) - 1);
|
||
FTrimTail := True;
|
||
Result := True;
|
||
end;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWSender.TakeText: string;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FPending;
|
||
FPending := '';
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWSender.Aborted: Boolean;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FAbortReq;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TCWSender.Key(Down: Boolean);
|
||
begin
|
||
if Assigned(FKeyEvent) then FKeyEvent(Down);
|
||
end;
|
||
|
||
function TCWSender.Hold(Deadline: QWord): Boolean;
|
||
var Now_: QWord; Slice: Integer;
|
||
begin
|
||
Result := True;
|
||
repeat
|
||
if Terminated or Aborted then Exit(False);
|
||
Now_ := GetTickCount64;
|
||
if Now_ >= Deadline then Exit(True);
|
||
// Дробим сон: обрыв передачи не должен ждать конца длинного тире.
|
||
Slice := Integer(Deadline - Now_);
|
||
if Slice > 5 then Slice := 5;
|
||
Sleep(Slice);
|
||
until False;
|
||
end;
|
||
|
||
procedure TCWSender.SendChar(Ch: Char; var Deadline: QWord);
|
||
// Deadline — момент, к которому знак должен закончиться; ведём его вперёд,
|
||
// чтобы длительности не «уползали» на ошибке каждого Sleep.
|
||
var
|
||
Code: string;
|
||
i, Dit, Mark, Gap, WPM, Weight: Integer;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
WPM := FWPM;
|
||
Weight := FWeight;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
Dit := DIT_MS_AT_1WPM div WPM;
|
||
// Вес: 50 = точка равна паузе. Выше — посылки длиннее за счёт пауз внутри
|
||
// знака (межзнаковые интервалы остаются стандартными, иначе «плывёт» темп).
|
||
Mark := (Dit * Weight) div 50;
|
||
Gap := (Dit * (100 - Weight)) div 50;
|
||
if Mark < 1 then Mark := 1;
|
||
if Gap < 1 then Gap := 1;
|
||
|
||
if Ch = ' ' then
|
||
begin
|
||
// Межсловный интервал = 7 точек; 3 из них уже отданы после прошлого знака.
|
||
Inc(Deadline, 4 * Dit);
|
||
Hold(Deadline);
|
||
Exit;
|
||
end;
|
||
|
||
Code := MorseCode(Ch);
|
||
if Code = '' then Exit;
|
||
for i := 1 to Length(Code) do
|
||
begin
|
||
Key(True);
|
||
if Code[i] = '-' then Inc(Deadline, 3 * Mark) else Inc(Deadline, Mark);
|
||
if not Hold(Deadline) then begin Key(False); Exit; end;
|
||
Key(False);
|
||
Inc(Deadline, Gap); // пауза между элементами знака
|
||
if not Hold(Deadline) then Exit;
|
||
end;
|
||
Inc(Deadline, 2 * Dit); // добор до 3 точек между знаками
|
||
Hold(Deadline);
|
||
end;
|
||
|
||
procedure TCWSender.Execute;
|
||
var
|
||
Text: string;
|
||
i, KeepLen: Integer;
|
||
Trim: Boolean;
|
||
Deadline: QWord;
|
||
begin
|
||
while not Terminated do
|
||
begin
|
||
FWake.WaitFor(200);
|
||
if Terminated then Break;
|
||
Text := TakeText;
|
||
if Text = '' then Continue;
|
||
|
||
FLock.Enter;
|
||
try
|
||
FBusy := True;
|
||
FAbortReq := False;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
try
|
||
Deadline := GetTickCount64;
|
||
i := 1;
|
||
while i <= Length(Text) do
|
||
begin
|
||
if Terminated or Aborted then Break;
|
||
// Backspace мог съесть хвост уже взятого текста — отсекаем.
|
||
FLock.Enter;
|
||
try
|
||
Trim := FTrimTail;
|
||
FTrimTail := False;
|
||
KeepLen := i - 1 + Length(FUnsent);
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
if Trim then
|
||
begin
|
||
if KeepLen < i then Break; // стёрли всё впереди
|
||
if Length(Text) > KeepLen then SetLength(Text, KeepLen);
|
||
if i > Length(Text) then Break;
|
||
end;
|
||
// Эхо в окно-терминал: знак пошёл в эфир; хвост очереди — для строки
|
||
// набора (там должно остаться только НЕ переданное).
|
||
FLock.Enter;
|
||
try
|
||
FSent := FSent + Text[i];
|
||
FUnsent := Copy(Text, i + 1, MaxInt);
|
||
if Length(FSent) > 4096 then FSent := Copy(FSent, Length(FSent) - 4095, 4096);
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
SendChar(Text[i], Deadline);
|
||
// Дописанное в очередь по ходу передачи подхватываем без паузы —
|
||
// иначе поток допередал бы «старый» текст и уснул на 200 мс.
|
||
if i = Length(Text) then Text := Text + TakeText;
|
||
Inc(i);
|
||
end;
|
||
finally
|
||
Key(False); // ключ всегда отпущен, что бы ни случилось
|
||
FLock.Enter;
|
||
try
|
||
FUnsent := '';
|
||
FTrimTail := False;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
FLock.Enter;
|
||
try
|
||
FBusy := False;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
end;
|
||
Key(False);
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Проигрыватель чужой манипуляции
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
constructor TCWElemPlayer.Create(AKeyEvent: TCWKeyEvent);
|
||
begin
|
||
FKeyEvent := AKeyEvent;
|
||
FLock := TCriticalSection.Create;
|
||
FWake := TEvent.Create(nil, False, False, '');
|
||
SetLength(FQ, CW_ELEM_QUEUE_MAX);
|
||
FreeOnTerminate := False;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
destructor TCWElemPlayer.Destroy;
|
||
begin
|
||
Terminate;
|
||
FWake.SetEvent;
|
||
WaitFor;
|
||
FWake.Free;
|
||
FLock.Free;
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TCWElemPlayer.Enqueue(Mark: Boolean; Ms: Integer);
|
||
var i: Integer;
|
||
begin
|
||
if Ms <= 0 then Exit;
|
||
FLock.Enter;
|
||
try
|
||
// Очередь переполнена — молча теряем новый элемент. Ронять уже принятую
|
||
// манипуляцию из-за захлебнувшегося клиента незачем.
|
||
if FCount >= Length(FQ) then Exit;
|
||
i := (FHead + FCount) mod Length(FQ);
|
||
FQ[i].Mark := Mark;
|
||
FQ[i].Ms := Ms;
|
||
Inc(FCount);
|
||
FAbortReq := False;
|
||
FBusy := True;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
FWake.SetEvent;
|
||
end;
|
||
|
||
function TCWElemPlayer.Take(out E: TCWElement): Boolean;
|
||
begin
|
||
Result := False;
|
||
FLock.Enter;
|
||
try
|
||
if FAbortReq or (FCount = 0) then Exit;
|
||
E := FQ[FHead];
|
||
FHead := (FHead + 1) mod Length(FQ);
|
||
Dec(FCount);
|
||
Result := True;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TCWElemPlayer.AbortPlay;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FAbortReq := True;
|
||
FHead := 0;
|
||
FCount := 0;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
FWake.SetEvent;
|
||
end;
|
||
|
||
function TCWElemPlayer.Aborted: Boolean;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FAbortReq;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWElemPlayer.Busy: Boolean;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FBusy;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TCWElemPlayer.Pending: Integer;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FCount;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TCWElemPlayer.Key(Down: Boolean);
|
||
begin
|
||
if Down = FDown then Exit; // лишних фронтов в эфир не шлём
|
||
FDown := Down;
|
||
if Assigned(FKeyEvent) then FKeyEvent(Down);
|
||
end;
|
||
|
||
function TCWElemPlayer.Hold(Deadline: QWord): Boolean;
|
||
// Тот же приём, что у TCWSender: абсолютный дедлайн и дробный сон, чтобы обрыв
|
||
// не ждал конца длинного тире.
|
||
var Now_: QWord; Slice: Integer;
|
||
begin
|
||
repeat
|
||
if Terminated or Aborted then Exit(False);
|
||
Now_ := GetTickCount64;
|
||
if Now_ >= Deadline then Exit(True);
|
||
Slice := Integer(Deadline - Now_);
|
||
if Slice > 5 then Slice := 5;
|
||
Sleep(Slice);
|
||
until False;
|
||
end;
|
||
|
||
procedure TCWElemPlayer.Execute;
|
||
var
|
||
E: TCWElement;
|
||
Deadline: QWord;
|
||
begin
|
||
Deadline := 0;
|
||
while not Terminated do
|
||
begin
|
||
FWake.WaitFor(200);
|
||
if Terminated then Break;
|
||
while Take(E) do
|
||
begin
|
||
// Первая пачка (или после простоя) начинается «сейчас»: догонять прошлое
|
||
// нечего, а старый дедлайн проиграл бы очередь одним махом.
|
||
if (Deadline = 0) or (GetTickCount64 > Deadline) then
|
||
Deadline := GetTickCount64;
|
||
Key(E.Mark);
|
||
Inc(Deadline, QWord(E.Ms));
|
||
if not Hold(Deadline) then Break;
|
||
end;
|
||
// Очередь кончилась (или её бросили) — ключ отпускаем всегда: несущая без
|
||
// хозяина в эфире не остаётся, а следующая пачка начнётся заново.
|
||
Key(False);
|
||
Deadline := 0;
|
||
FLock.Enter;
|
||
try
|
||
if FCount = 0 then FBusy := False;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
Key(False);
|
||
end;
|
||
|
||
end.
|