mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(tci): этап 2 — бинарные потоки IQ/аудио, TX-аудио и запись линейного выхода
Реализованы все четыре потока §3.4 и рекордер:
• RX_AUDIO_STREAM — тап ДО громкости и мьюта (OnDemodAudioReady): скиммеру
и цифре нужен звук приёмника, а не то, что осталось после ручки;
• LINEOUT_STREAM — тап ПОСЛЕ (OnAudioReady), то есть что слышно;
• IQ_STREAM — тап сырого IQ в движке, ОДИН вызов на накопленный блок;
• TX_AUDIO_STREAM + TX_CHRONO — TRX:0,true,tci берёт модуляцию из потока
клиента (флаг TCIMicRequested впереди web в SetMOX), маркеры времени идут
из тика по часам, аудио клиента разворачивается в 48 кГц моно в тот же
ринг, что и web-микрофон;
• LINE_OUT_RECORDER_* — кольцо int16 на приёмник, WAV пишет отдельный поток.
Тапы аудио в контроллере многоадресные (AddAudioTap): слушают, ничего не
забирая, в отличие от OnAudioConsume, которым владеет web. Блоки нарезает и
раскладывает по кольцам клиентов сам DSP-поток, в сокет пишет поток клиента —
та же дисциплина, что у команд. Порядок локов везде FSliceLock → FStreamLock.
У очереди команд и кольца блоков разная политика переполнения: команду терять
нельзя, блок потока — можно (теряется самый старый).
Пересчёт частоты многоступенчатый (TCIStreams). Одноступенчатый FIR на верхнем
пресете Pluto (5760 кГц, коэффициент 120) упирался в потолок отводов и давал
завал 1.3 дБ в полосе при подавлении зеркала 16 дБ — то есть поток IQ с
мусором. Теперь коэффициент раскладывается на множители, спецификацию фильтра
каждой ступени задаёт ИТОГОВАЯ полоса, а свёртка идёт со сложением
симметричных пар: −83 дБ на любом коэффициенте, ≈10% ядра на 5.76 МГц.
Согласование частот с железом: из пресетов Pluto 576 и 960 кГц на 384 не
делятся, поэтому отдаём наибольшую ЗАКОННУЮ частоту, делящую источник нацело
(576/960 → 192 кГц). Ответ на IQ_SAMPLERATE называет достижимое, а не просьбу
клиента, и переобъявляется без запроса при смене rate и устройства.
Приёмный буфер соединения 4 → 32 КБ: блок TX-аудио это 64 байта заголовка плюс
data[16384], а кадр крупнее буфера не собирается никогда.
Настройки TCI переехали из Advanced на вкладку CAT, справа от TCP CAT Server:
это такой же канал внешнего управления трансивером.
Стенд (scratchpad, tcitest.pas): 112 проверок, все зелёные — включая сквозной
прогон через живой WDSP (синтетический IQ → блоки RX-аудио и IQ у настоящего
WS-клиента, и обратно TX-аудио клиента → блоки TX-IQ).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+783
-26
@@ -27,8 +27,11 @@ unit TCIAdapter;
|
||||
так синхронизация между несколькими клиентами остаётся честной, а поведение
|
||||
радио не выдумывается. Всё такое помечено «эхо» и перечислено в doc/TCI.md.
|
||||
|
||||
Потоки IQ/аудио (§3.4) — следующий этап: команды управления потоками
|
||||
принимаются и подтверждаются, но сами потоки не идут.
|
||||
Бинарные потоки (§3.4) живут в TCIStreams; здесь — только их подключение к
|
||||
контроллеру: тап RX-аудио (два вида: до громкости — «аудиопоток приёмника»,
|
||||
после неё — «линейный выход»), тап сырого IQ в движке, приём TX-аудио от
|
||||
клиента и маркеры TX_CHRONO. Данные перемалывает DSP-поток, поэтому вся
|
||||
работа с потоками — под FStreamLock и без единого ожидания.
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
@@ -41,10 +44,16 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, DateUtils, Math, SyncObjs,
|
||||
RadioController, RadioBackend, WDSPEngine, Settings,
|
||||
DXSpotStore, TCIProtocol, TCIServer;
|
||||
DXSpotStore, TCIProtocol, TCIServer, TCIStreams;
|
||||
|
||||
const
|
||||
TCI_MAX_RX = MAX_PANS; // приёмник TCI = панадаптер
|
||||
// Аудио на выходе движка всегда 48 кГц (TWDSPEngine.Create), от него и
|
||||
// считаются все прореживания и пересчёты потоков.
|
||||
TCI_AUDIO_ENGINE_RATE = 48000;
|
||||
// Потолок разворота одного блока TX-аудио в 48 кГц: 8192 отсчёта int16 на
|
||||
// 8 кГц дают ×6. Больше в блок не влезает по протоколу (data[16384]).
|
||||
TCI_TX_OUT_MAX = (TCI_STREAM_DATA_MAX div 2) * 6;
|
||||
// Захват параметра клиентом (§3.5): пока владелец его крутит, остальные
|
||||
// могут только слушать. Без этого два логгера перетягивают частоту друг у
|
||||
// друга бесконечно.
|
||||
@@ -120,6 +129,31 @@ type
|
||||
FHolds: array[0..TCI_HOLD_SLOTS-1] of TTCIHold;
|
||||
FHoldCount: Integer;
|
||||
|
||||
// ── Бинарные потоки (§3.4) ──
|
||||
// Список исходящих потоков и рекордеры живут под ОДНИМ локом: их читает
|
||||
// DSP-поток (тап аудио/IQ), а меняют потоки клиентов. Порядок захвата
|
||||
// всегда FSliceLock → FStreamLock: тап сначала выясняет, чей это слайс,
|
||||
// и только потом ищет подписчиков. Обратный порядок дал бы клин.
|
||||
FStreamLock: TCriticalSection;
|
||||
FStreams: array of TTCIStreamOut;
|
||||
FRec: array[0..TCI_MAX_RX-1] of TTCIRecorder;
|
||||
FTapsOn: Boolean; // тапы навешены на контроллер/движок
|
||||
|
||||
// ── TX-аудио от клиента (§3.4) ──
|
||||
FTxLock: TCriticalSection;
|
||||
FTxClient: TTCIClient; // кто модулирует (nil — никто)
|
||||
FTxInterp: TTCIInterpolator;
|
||||
FTxInRate: Integer; // частота дискретизации подачи клиента
|
||||
FTxRunning: Boolean; // маркеры TX_CHRONO идут
|
||||
FTxOwed: Double; // сколько сэмплов клиент нам «должен»
|
||||
FTxLastMs: QWord;
|
||||
// Рабочие буферы разбора TX-блока. Полем, а не на стеке: развёрнутый в
|
||||
// 48 кГц блок — это сотни килобайт, и класть их в стек потока клиента
|
||||
// (да ещё на каждый блок двадцать раз в секунду) незачем.
|
||||
FTxRaw: array of Single;
|
||||
FTxMono: array of Single;
|
||||
FTxOut: array of Double;
|
||||
|
||||
// ── Кэш для подавления повторов в уведомлениях ──
|
||||
FLastLimLo: Double; // последние разосланные VFO_LIMITS (поток контроллера)
|
||||
FLastLimHi: Double;
|
||||
@@ -134,6 +168,7 @@ type
|
||||
FsInt2: Integer;
|
||||
FsInt3: Integer;
|
||||
FsBool: Boolean;
|
||||
FsBool2: Boolean;
|
||||
FsStr: string;
|
||||
|
||||
// ── Sync-методы (поток контроллера) ──
|
||||
@@ -166,6 +201,30 @@ type
|
||||
procedure SyncCWSend;
|
||||
procedure SyncCWStop;
|
||||
procedure SyncFocus;
|
||||
procedure SyncTaps; // навесить/снять тапы аудио и IQ
|
||||
|
||||
// ── Бинарные потоки ──
|
||||
function FindStream(C: TTCIClient; K: TTCIStreamType;
|
||||
Rx: Integer): TTCIStreamOut; // под FStreamLock
|
||||
procedure StartStream(C: TTCIClient; K: TTCIStreamType; Rx: Integer);
|
||||
procedure StopStream(C: TTCIClient; K: TTCIStreamType; Rx: Integer);
|
||||
procedure DropClientStreams(C: TTCIClient);
|
||||
procedure RestartStreams(C: TTCIClient; K: TTCIStreamType);
|
||||
procedure DropDeadRxStreams; // приёмник исчез — гасим его потоки
|
||||
function EffIQRate(C: TTCIClient): Integer; // что реально отдадим
|
||||
procedure PushIQRate(Client: TTCIClient); // переобъявить её клиенту
|
||||
procedure StopAllStreams;
|
||||
procedure SetTaps(On_: Boolean); // поток контроллера
|
||||
function HasAudioStream(C: TTCIClient): Boolean;
|
||||
function StreamRxOf(PanId, SliceId: Integer): Integer; // −1 = не наш канал
|
||||
procedure OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
|
||||
const Left, Right: array of Single; Count: Integer);
|
||||
procedure OnIQTap(PanId: Integer; PI_, PQ_: PDouble; N, RateHz: Integer);
|
||||
procedure HandleBinary(Client: TTCIClient; Data: PByte; Len: Integer);
|
||||
procedure PushTxChrono; // тик: маркеры времени клиенту
|
||||
procedure CmdStream(Client: TTCIClient; const M: TTCIMessage);
|
||||
procedure CmdRecorder(Client: TTCIClient; const M: TTCIMessage);
|
||||
procedure ClearTxClient(C: TTCIClient); // клиент ушёл/перестал модулировать
|
||||
|
||||
// ── Помощники модели ──
|
||||
function CanInvoke: Boolean;
|
||||
@@ -307,8 +366,16 @@ begin
|
||||
FHoldLock := TCriticalSection.Create;
|
||||
FHoldCount := 0;
|
||||
|
||||
FStreamLock := TCriticalSection.Create;
|
||||
FTxLock := TCriticalSection.Create;
|
||||
FTapsOn := False;
|
||||
SetLength(FTxRaw, TCI_STREAM_DATA_MAX div 2); // худший случай: int16
|
||||
SetLength(FTxMono, TCI_STREAM_DATA_MAX div 2);
|
||||
SetLength(FTxOut, TCI_TX_OUT_MAX);
|
||||
|
||||
FServer := TTCIServer.Create;
|
||||
FServer.OnCommand := HandleCommand;
|
||||
FServer.OnBinary := HandleBinary;
|
||||
FServer.OnConnect := HandleConnect;
|
||||
FServer.OnDisconnect := HandleDisconnect;
|
||||
FServer.OnTick := HandleTick;
|
||||
@@ -326,18 +393,25 @@ end;
|
||||
destructor TTCIAdapter.Destroy;
|
||||
begin
|
||||
// Сначала отписка: контроллер живёт дольше адаптера, и Changed() после
|
||||
// нашей смерти позвал бы метод освобождённого объекта.
|
||||
// нашей смерти позвал бы метод освобождённого объекта. По той же причине
|
||||
// ПЕРВЫМИ снимаем тапы аудио/IQ — их зовёт DSP-поток, который переживёт нас.
|
||||
SetTaps(False);
|
||||
if FController <> nil then FController.RemoveStateListener(OnState);
|
||||
if FServer <> nil then
|
||||
begin
|
||||
FServer.Stop;
|
||||
FreeAndNil(FServer);
|
||||
end;
|
||||
// Потоки — уже после остановки сервера: их объекты ссылаются на клиентов.
|
||||
StopAllStreams;
|
||||
FreeAndNil(FTxInterp);
|
||||
FLock.Free;
|
||||
FEchoLock.Free;
|
||||
FDevLock.Free;
|
||||
FSliceLock.Free;
|
||||
FHoldLock.Free;
|
||||
FStreamLock.Free;
|
||||
FTxLock.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
@@ -359,6 +433,10 @@ begin
|
||||
|
||||
OldPort := FServer.Port;
|
||||
FServer.Stop;
|
||||
// Сервер остановлен — клиентов больше нет, значит и потоки чужие: снимаем
|
||||
// тапы, иначе DSP-поток продолжал бы носить аудио в никуда.
|
||||
StopAllStreams;
|
||||
SetTaps(False);
|
||||
if not T.Enabled then
|
||||
begin
|
||||
FCfg := T;
|
||||
@@ -369,11 +447,13 @@ begin
|
||||
if Result then
|
||||
begin
|
||||
FCfg := T;
|
||||
SetTaps(True);
|
||||
Exit;
|
||||
end;
|
||||
// Порт занят (или отобран правами) — поднимаем то, что работало.
|
||||
if FCfg.Enabled and TTCIServer.ValidSettings(OldPort, FCfg.BindAddr) then
|
||||
if FServer.Configure(OldPort, FCfg.BindAddr) then FServer.Start;
|
||||
if FServer.Configure(OldPort, FCfg.BindAddr) and FServer.Start then
|
||||
SetTaps(True);
|
||||
end;
|
||||
|
||||
function TTCIAdapter.CanInvoke: Boolean;
|
||||
@@ -577,6 +657,10 @@ procedure TTCIAdapter.PushChannelMap;
|
||||
// полную картину каждого живого приёмника — клиент перезаливает её целиком.
|
||||
var Rx, Ch, N: Integer;
|
||||
begin
|
||||
// Потоки пропавших приёмников гасим ВСЕГДА, даже если рассылать некому:
|
||||
// объект потока пережил бы свой пан и молча копил тишину, а клиент ждал бы
|
||||
// блоков, которых больше не будет.
|
||||
DropDeadRxStreams;
|
||||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||||
for Rx := 1 to RxCount - 1 do
|
||||
begin
|
||||
@@ -1168,9 +1252,13 @@ end;
|
||||
|
||||
procedure TTCIAdapter.HandleDisconnect(Client: TTCIClient);
|
||||
// Клиент ушёл — снимаем его захваты параметров, иначе следующий, кому достанется
|
||||
// тот же адрес объекта, унаследовал бы чужие права (§3.5).
|
||||
// тот же адрес объекта, унаследовал бы чужие права (§3.5). По той же причине
|
||||
// гасим его потоки: объект клиента вот-вот освободят, а на него смотрит
|
||||
// DSP-поток. Порядок обязателен — сначала потоки, потом возврат в сервер.
|
||||
begin
|
||||
DropHolds(Client);
|
||||
DropClientStreams(Client);
|
||||
ClearTxClient(Client);
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -1224,9 +1312,28 @@ end;
|
||||
|
||||
procedure TTCIAdapter.SyncSetTRX;
|
||||
begin
|
||||
// Источник модуляции ставим ДО SetMOX — именно он его и читает при выборе
|
||||
// микрофона. Выключение передачи флаг снимает всегда: следующий раз оператор
|
||||
// может нажать PTT сам, и тогда в эфир должен идти его микрофон.
|
||||
FController.TCIMicRequested := FsBool and FsBool2;
|
||||
FController.SetMOX(FsBool);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncTaps;
|
||||
// Поток контроллера: движок и список тапов трогаем только отсюда.
|
||||
begin
|
||||
if FsBool then
|
||||
begin
|
||||
FController.AddAudioTap(OnAudioTap);
|
||||
FController.SetIQTap(OnIQTap);
|
||||
end
|
||||
else
|
||||
begin
|
||||
FController.RemoveAudioTap(OnAudioTap);
|
||||
FController.SetIQTap(nil);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncSetTune;
|
||||
begin
|
||||
FController.SetTune(FsBool);
|
||||
@@ -1557,7 +1664,7 @@ procedure TTCIAdapter.DispatchCommand(Client: TTCIClient; const M: TTCIMessage);
|
||||
var
|
||||
Rx, Ch, V: Integer;
|
||||
Lo, Hi: Integer;
|
||||
B: Boolean;
|
||||
B, FromTCI: Boolean;
|
||||
D: Double;
|
||||
Name: string;
|
||||
begin
|
||||
@@ -1642,13 +1749,26 @@ begin
|
||||
// ── Передача ──
|
||||
if M.Name = 'TRX' then
|
||||
begin
|
||||
// arg3 (источник сигнала: tci/mic1/…) игнорируем: аудио по TCI ещё нет,
|
||||
// модуляция берётся из выбранного в программе входа.
|
||||
// arg3 — источник сигнала. Наш только 'tci': модуляция берётся из
|
||||
// аудиопотока этого клиента. Остальные значения (mic1/mic2/micpc/ecoder2)
|
||||
// называют физические входы ExpertSDR3, которых у нас нет, — они значат
|
||||
// «микрофон, выбранный в программе», то есть ровно то, что и без arg3.
|
||||
// Требование «включен аудиопоток по TCI» (§4.2) проверяем буквально:
|
||||
// без AUDIO_START модулировать нечем, и молча оставить оператора с
|
||||
// тишиной в эфире хуже, чем передавать с его микрофона.
|
||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('TRX', 0, 0), Client) then
|
||||
begin
|
||||
Name := LowerCase(Trim(TCIArg(M, 2)));
|
||||
FromTCI := B and (Name = 'tci') and HasAudioStream(Client);
|
||||
FTxLock.Enter;
|
||||
try
|
||||
if FromTCI then FTxClient := Client
|
||||
else if FTxClient = Client then FTxClient := nil;
|
||||
finally FTxLock.Leave; end;
|
||||
FLock.Enter;
|
||||
try
|
||||
FsBool := B;
|
||||
FsBool := B;
|
||||
FsBool2 := FromTCI;
|
||||
if CanInvoke then FController.Invoke(SyncSetTRX);
|
||||
finally FLock.Leave; end;
|
||||
end;
|
||||
@@ -2188,37 +2308,75 @@ begin
|
||||
// Это настройки КЛИЕНТА (§4.3), а не устройства: два логгера вправе просить
|
||||
// разную частоту дискретизации. Поэтому живут в его объекте, а не в адаптере
|
||||
// — иначе один клиент перенастраивал бы будущие потоки всем остальным.
|
||||
// Сами потоки — этап 2, значения только принимаются и подтверждаются.
|
||||
// Изменение параметра на ходу перезапускает уже идущие потоки этого клиента:
|
||||
// блок с новой частотой посреди старого потока клиенты разбирают как мусор.
|
||||
if M.Name = 'IQ_SAMPLERATE' then
|
||||
begin
|
||||
// Набор частот оговорён протоколом; чужое значение отвергаем, а в ответе
|
||||
// отдаём действующее — клиент увидит, что его не приняли.
|
||||
if TCITryArgInt(M, 0, V) and TCIValidIQRate(V) then Client.IQRate := V;
|
||||
Reply(Client, TCIBuild('iq_samplerate', [TCIIntStr(Client.IQRate)]));
|
||||
if TCITryArgInt(M, 0, V) and TCIValidIQRate(V) and (V <> Client.IQRate) then
|
||||
begin
|
||||
Client.IQRate := V;
|
||||
RestartStreams(Client, tstIQ);
|
||||
end;
|
||||
// ★В ответе — та частота, которую клиент РЕАЛЬНО получит, а не его
|
||||
// просьба: на 576 и 960 кГц Pluto просьба «384» невыполнима (не делится
|
||||
// нацело), и подтвердить её значило бы соврать. Сама просьба остаётся
|
||||
// сохранённой — на другом устройстве она может стать выполнимой.
|
||||
Reply(Client, TCIBuild('iq_samplerate', [TCIIntStr(EffIQRate(Client))]));
|
||||
Exit;
|
||||
end;
|
||||
if M.Name = 'AUDIO_SAMPLERATE' then
|
||||
begin
|
||||
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) then Client.AudioRate := V;
|
||||
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then
|
||||
begin
|
||||
Client.AudioRate := V;
|
||||
// Число сэмплов в блоке у ExpertSDR3 своё на каждую частоту (§4.3), и
|
||||
// клиент вправе на это рассчитывать, пока не задал своё явно.
|
||||
Client.AudioSamples := TCIDefaultAudioSamples(V);
|
||||
RestartStreams(Client, tstRXAudio);
|
||||
RestartStreams(Client, tstLineOut);
|
||||
end;
|
||||
Reply(Client, TCIBuild('audio_samplerate', [TCIIntStr(Client.AudioRate)]));
|
||||
Exit;
|
||||
end;
|
||||
if M.Name = 'AUDIO_STREAM_SAMPLES' then
|
||||
begin
|
||||
if TCITryArgInt(M, 0, V) then
|
||||
Client.AudioSamples := EnsureRange(V, 100, 2048);
|
||||
begin
|
||||
V := EnsureRange(V, TCI_AUDIO_SAMPLES_MIN, TCI_AUDIO_SAMPLES_MAX);
|
||||
if V <> Client.AudioSamples then
|
||||
begin
|
||||
Client.AudioSamples := V;
|
||||
RestartStreams(Client, tstRXAudio);
|
||||
RestartStreams(Client, tstLineOut);
|
||||
end;
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
if M.Name = 'AUDIO_STREAM_CHANNELS' then
|
||||
begin
|
||||
if TCITryArgInt(M, 0, V) then
|
||||
Client.AudioChannels := EnsureRange(V, 1, 2);
|
||||
begin
|
||||
V := EnsureRange(V, 1, 2);
|
||||
if V <> Client.AudioChannels then
|
||||
begin
|
||||
Client.AudioChannels := V;
|
||||
RestartStreams(Client, tstRXAudio);
|
||||
RestartStreams(Client, tstLineOut);
|
||||
end;
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
if M.Name = 'AUDIO_STREAM_SAMPLE_TYPE' then
|
||||
begin
|
||||
Name := LowerCase(Trim(TCIArg(M, 0)));
|
||||
if TCIValidSampleType(Name) then Client.AudioSampleType := Name;
|
||||
if TCIValidSampleType(Name) and (Name <> Client.AudioSampleType) then
|
||||
begin
|
||||
Client.AudioSampleType := Name;
|
||||
RestartStreams(Client, tstRXAudio);
|
||||
RestartStreams(Client, tstLineOut);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
if M.Name = 'TX_STREAM_AUDIO_BUFFERING' then
|
||||
@@ -2228,17 +2386,19 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Запуск потоков (§3.4) — этап 2. Молчать нельзя: клиент решил бы, что поток
|
||||
// пошёл, и ждал бы данных бесконечно. Отвечаем ошибкой на конкретную команду.
|
||||
// ── Запуск и остановка потоков (§3.4) ──
|
||||
if (M.Name = 'IQ_START') or (M.Name = 'IQ_STOP') or
|
||||
(M.Name = 'AUDIO_START') or (M.Name = 'AUDIO_STOP') or
|
||||
(M.Name = 'LINE_OUT_START') or (M.Name = 'LINE_OUT_STOP') or
|
||||
(M.Name = 'LINE_OUT_RECORDER_START') or
|
||||
(M.Name = 'LINE_OUT_START') or (M.Name = 'LINE_OUT_STOP') then
|
||||
begin
|
||||
CmdStream(Client, M);
|
||||
Exit;
|
||||
end;
|
||||
if (M.Name = 'LINE_OUT_RECORDER_START') or
|
||||
(M.Name = 'LINE_OUT_RECORDER_SAVE') or
|
||||
(M.Name = 'LINE_OUT_RECORDER_BREAK') then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name),
|
||||
'binary streams are not implemented']));
|
||||
CmdRecorder(Client, M);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
@@ -2291,11 +2451,594 @@ begin
|
||||
// измерители у ВСЕХ клиентов до перезапуска сервера.
|
||||
try
|
||||
FServer.EnumClients(PushSensors);
|
||||
PushTxChrono;
|
||||
except
|
||||
// молча: следующий тик через 20 мс попробует снова
|
||||
end;
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
Бинарные потоки (§3.4)
|
||||
|
||||
Кто в каком потоке исполнения:
|
||||
• START/STOP и параметры — поток клиента (список правится под FStreamLock);
|
||||
• подача данных (OnAudioTap/OnIQTap) — DSP-поток: он же нарезает блоки и
|
||||
кладёт их в кольцо клиента, а в сокет пишет поток самого клиента;
|
||||
• TX_CHRONO — тик-поток (пейсинг по часам);
|
||||
• TX-аудио от клиента — поток этого клиента (HandleBinary).
|
||||
Тапы навешиваются на контроллер и движок только из потока контроллера
|
||||
(SetTaps зовут ApplySettings и деструктор), потому что снятие тапа обязано
|
||||
дождаться выхода DSP-потока из вызова.
|
||||
═══════════════════════════════════════════════════════════════════════════ }
|
||||
|
||||
procedure TTCIAdapter.SetTaps(On_: Boolean);
|
||||
// Только поток контроллера (ApplySettings, Destroy): снятие тапа обязано
|
||||
// дождаться выхода DSP-потока из вызова, а Invoke сюда звать не из чего —
|
||||
// мы в нём и находимся.
|
||||
begin
|
||||
if (FTapsOn = On_) or (FController = nil) then Exit;
|
||||
FTapsOn := On_;
|
||||
FLock.Enter;
|
||||
try
|
||||
FsBool := On_;
|
||||
SyncTaps;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TTCIAdapter.FindStream(C: TTCIClient; K: TTCIStreamType;
|
||||
Rx: Integer): TTCIStreamOut;
|
||||
// Только под FStreamLock.
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := nil;
|
||||
for i := 0 to High(FStreams) do
|
||||
if (FStreams[i] <> nil) and FStreams[i].Matches(C, K, Rx) then
|
||||
Exit(FStreams[i]);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.StartStream(C: TTCIClient; K: TTCIStreamType; Rx: Integer);
|
||||
var
|
||||
S: TTCIStreamOut;
|
||||
SrcRate, WantRate, Chans, Block: Integer;
|
||||
ST: TTCISampleType;
|
||||
begin
|
||||
if C = nil then Exit;
|
||||
|
||||
// Параметры снимаем ДО лока: геттеры клиента берут его собственный лок, и
|
||||
// держать при этом FStreamLock значило бы связать два лока без нужды.
|
||||
if K = tstIQ then
|
||||
begin
|
||||
SrcRate := FController.IQTapRateHz(Rx);
|
||||
WantRate := C.IQRate;
|
||||
Chans := 2; // IQ комплексный по определению
|
||||
ST := tsyFloat32; // формат IQ в TCI не настраивается
|
||||
Block := TCIMaxBlockSamples(ST, Chans);
|
||||
end
|
||||
else
|
||||
begin
|
||||
SrcRate := TCI_AUDIO_ENGINE_RATE;
|
||||
WantRate := C.AudioRate;
|
||||
Chans := EnsureRange(C.AudioChannels, 1, 2);
|
||||
if not TCISampleTypeByName(C.AudioSampleType, ST) then ST := tsyFloat32;
|
||||
Block := C.AudioSamples;
|
||||
end;
|
||||
if SrcRate <= 0 then SrcRate := TCI_AUDIO_ENGINE_RATE;
|
||||
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
if FindStream(C, K, Rx) <> nil then Exit; // повторный START — не ошибка
|
||||
S := TTCIStreamOut.Create(C, K, Rx, SrcRate, WantRate, Chans, ST, Block);
|
||||
SetLength(FStreams, Length(FStreams) + 1);
|
||||
FStreams[High(FStreams)] := S;
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.StopStream(C: TTCIClient; K: TTCIStreamType; Rx: Integer);
|
||||
var i, j: Integer;
|
||||
begin
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
for i := 0 to High(FStreams) do
|
||||
if (FStreams[i] <> nil) and FStreams[i].Matches(C, K, Rx) then
|
||||
begin
|
||||
FStreams[i].Free;
|
||||
for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1];
|
||||
SetLength(FStreams, Length(FStreams) - 1);
|
||||
Exit;
|
||||
end;
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.DropClientStreams(C: TTCIClient);
|
||||
var i, j: Integer;
|
||||
begin
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
i := 0;
|
||||
while i <= High(FStreams) do
|
||||
if (FStreams[i] <> nil) and (FStreams[i].Client = C) then
|
||||
begin
|
||||
FStreams[i].Free;
|
||||
for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1];
|
||||
SetLength(FStreams, Length(FStreams) - 1);
|
||||
end
|
||||
else
|
||||
Inc(i);
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.RestartStreams(C: TTCIClient; K: TTCIStreamType);
|
||||
// Параметры потока сменились на ходу: пересоздаём то, что уже идёт, с новыми.
|
||||
// Пересобрать объект дешевле, чем учить его менять формат на лету, а клиент
|
||||
// всё равно обязан читать заголовок каждого блока.
|
||||
var
|
||||
Rx: Integer;
|
||||
Live: array[0..TCI_MAX_RX-1] of Boolean;
|
||||
begin
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
for Rx := 0 to TCI_MAX_RX - 1 do Live[Rx] := FindStream(C, K, Rx) <> nil;
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
for Rx := 0 to TCI_MAX_RX - 1 do
|
||||
if Live[Rx] then
|
||||
begin
|
||||
StopStream(C, K, Rx);
|
||||
StartStream(C, K, Rx);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.DropDeadRxStreams;
|
||||
// Поток контроллера (OnState): живость приёмника спрашиваем ДО лока — внутри
|
||||
// него ходит DSP-поток, и лезть оттуда в контроллер незачем.
|
||||
var
|
||||
Rx, i, j: Integer;
|
||||
Alive: array[0..TCI_MAX_RX-1] of Boolean;
|
||||
begin
|
||||
for Rx := 0 to TCI_MAX_RX - 1 do
|
||||
Alive[Rx] := ValidRx(Rx) and RxActive(Rx);
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
i := 0;
|
||||
while i <= High(FStreams) do
|
||||
if (FStreams[i].Rx >= 0) and (FStreams[i].Rx < TCI_MAX_RX) and
|
||||
not Alive[FStreams[i].Rx] then
|
||||
begin
|
||||
FStreams[i].Free;
|
||||
for j := i to High(FStreams) - 1 do FStreams[j] := FStreams[j + 1];
|
||||
SetLength(FStreams, Length(FStreams) - 1);
|
||||
end
|
||||
else
|
||||
Inc(i);
|
||||
for Rx := 0 to TCI_MAX_RX - 1 do
|
||||
if not Alive[Rx] then FreeAndNil(FRec[Rx]);
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.StopAllStreams;
|
||||
var i: Integer;
|
||||
begin
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
for i := 0 to High(FStreams) do FStreams[i].Free;
|
||||
SetLength(FStreams, 0);
|
||||
for i := 0 to TCI_MAX_RX - 1 do FreeAndNil(FRec[i]);
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
ClearTxClient(nil);
|
||||
end;
|
||||
|
||||
function TTCIAdapter.EffIQRate(C: TTCIClient): Integer;
|
||||
// Частота IQ, которую клиент получит на ГЛАВНОМ приёмнике. У доп. панов rate
|
||||
// свой, и настоящая частота каждого потока всегда стоит в заголовке блока —
|
||||
// но команда IQ_SAMPLERATE в протоколе одна на клиента, поэтому и отвечать на
|
||||
// неё можно только про один приёмник. Частоту берём из снимка: зовут отсюда
|
||||
// потоки клиентов.
|
||||
begin
|
||||
Result := TCIPickIQRate(DevSnap.SampleRate, C.IQRate);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.PushIQRate(Client: TTCIClient);
|
||||
begin
|
||||
if Client.Ready then
|
||||
Client.Send(TCIBuild('iq_samplerate', [TCIIntStr(EffIQRate(Client))]));
|
||||
end;
|
||||
|
||||
function TTCIAdapter.HasAudioStream(C: TTCIClient): Boolean;
|
||||
var Rx: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
for Rx := 0 to TCI_MAX_RX - 1 do
|
||||
if FindStream(C, tstRXAudio, Rx) <> nil then Exit(True);
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TTCIAdapter.StreamRxOf(PanId, SliceId: Integer): Integer;
|
||||
// Какому приёмнику TCI принадлежит это аудио. Главный тракт — приёмник 0.
|
||||
// У доп. пана в потоках участвует только канал A (первый слайс): «аудиопоток
|
||||
// приёмника» в протоколе один на приёмник, второго канала у него нет.
|
||||
// Зовётся из DSP-потока и берёт FSliceLock — ДО FStreamLock (порядок!).
|
||||
begin
|
||||
Result := -1;
|
||||
if PanId <= 0 then
|
||||
begin
|
||||
if SliceId = 0 then Result := 0; // слайсы главного пана в модель не входят
|
||||
Exit;
|
||||
end;
|
||||
if not ValidRx(PanId) then Exit;
|
||||
if SliceIdOf(PanId, 0) = SliceId then Result := PanId;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
|
||||
const Left, Right: array of Single; Count: Integer);
|
||||
// DSP-поток. Всё, что здесь можно, — перемолоть блок и разложить его по
|
||||
// кольцам клиентов; ждать нельзя ничего.
|
||||
var
|
||||
Rx, i: Integer;
|
||||
K: TTCIStreamType;
|
||||
begin
|
||||
if Count <= 0 then Exit;
|
||||
Rx := StreamRxOf(PanId, SliceId);
|
||||
if Rx < 0 then Exit;
|
||||
|
||||
if Kind = rakDemod then K := tstRXAudio else K := tstLineOut;
|
||||
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
for i := 0 to High(FStreams) do
|
||||
if (FStreams[i].Kind = K) and (FStreams[i].Rx = Rx) then
|
||||
FStreams[i].FeedAudio(Left, Right, Count);
|
||||
// Рекордер пишет ровно линейный выход — тот же источник, что и поток
|
||||
// LINEOUT (§4.3: «повторяет обычный аудио поток»).
|
||||
if (Kind = rakLineOut) and (Rx < TCI_MAX_RX) and (FRec[Rx] <> nil) then
|
||||
FRec[Rx].Feed(Left, Right, Count);
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.OnIQTap(PanId: Integer; PI_, PQ_: PDouble;
|
||||
N, RateHz: Integer);
|
||||
// DSP-поток, один вызов на накопленный блок. У пана этот вызов идёт под
|
||||
// FSliceLock движка, поэтому здесь тем более нельзя ждать.
|
||||
var i: Integer;
|
||||
begin
|
||||
if (N <= 0) or (RateHz <= 0) then Exit;
|
||||
if (PanId < 0) or (PanId >= TCI_MAX_RX) then Exit;
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
for i := 0 to High(FStreams) do
|
||||
if (FStreams[i].Kind = tstIQ) and (FStreams[i].Rx = PanId) then
|
||||
begin
|
||||
// Rate устройства могли сменить уже после START (смена sample rate,
|
||||
// другой rate DDC пана): пересчитываем прореживание на месте, иначе
|
||||
// клиент получал бы поток с враньём в заголовке.
|
||||
if FStreams[i].SrcRate <> RateHz then FStreams[i].SetSourceRate(RateHz);
|
||||
FStreams[i].FeedIQ(PI_, PQ_, N);
|
||||
end;
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.CmdStream(Client: TTCIClient; const M: TTCIMessage);
|
||||
var
|
||||
Rx: Integer;
|
||||
K: TTCIStreamType;
|
||||
Start: Boolean;
|
||||
begin
|
||||
// Номер приёмника обязателен и обязан существовать: молча завести поток
|
||||
// несуществующего пана значит навсегда оставить клиента без данных.
|
||||
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error',
|
||||
[LowerCase(M.Name), 'bad receiver']));
|
||||
Exit;
|
||||
end;
|
||||
|
||||
Start := False;
|
||||
K := tstRXAudio;
|
||||
if M.Name = 'IQ_START' then begin K := tstIQ; Start := True; end
|
||||
else if M.Name = 'IQ_STOP' then K := tstIQ
|
||||
else if M.Name = 'AUDIO_START' then begin K := tstRXAudio; Start := True; end
|
||||
else if M.Name = 'AUDIO_STOP' then K := tstRXAudio
|
||||
else if M.Name = 'LINE_OUT_START' then begin K := tstLineOut; Start := True; end
|
||||
else if M.Name = 'LINE_OUT_STOP' then K := tstLineOut;
|
||||
|
||||
if Start then
|
||||
begin
|
||||
// Пан существует, но не запущен — данных не будет вовсе. Честнее сказать
|
||||
// сразу, чем оставить клиента ждать блоков от мёртвого приёмника.
|
||||
if not RxActive(Rx) then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error',
|
||||
[LowerCase(M.Name), 'receiver is not running']));
|
||||
Exit;
|
||||
end;
|
||||
StartStream(Client, K, Rx);
|
||||
end
|
||||
else
|
||||
begin
|
||||
StopStream(Client, K, Rx);
|
||||
// Модулировать из потока, которого больше нет, нельзя (§4.2).
|
||||
if (K = tstRXAudio) and not HasAudioStream(Client) then ClearTxClient(Client);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.CmdRecorder(Client: TTCIClient; const M: TTCIMessage);
|
||||
var
|
||||
Rx, Sec, Rate: Integer;
|
||||
Path: string;
|
||||
Data: TTCIPcm;
|
||||
Old, New_: TTCIRecorder;
|
||||
begin
|
||||
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad receiver']));
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if M.Name = 'LINE_OUT_RECORDER_START' then
|
||||
begin
|
||||
if not TCITryArgInt(M, 1, Sec) then Sec := TCI_RECORD_MAX_SEC;
|
||||
Sec := EnsureRange(Sec, 1, TCI_RECORD_MAX_SEC);
|
||||
// Кольцо заводим ДО лока: на предельных 300 с это 57 МБ, и выделять их
|
||||
// под локом, которого ждёт DSP-поток, значит уронить звук на десятки мс.
|
||||
New_ := TTCIRecorder.Create(Rx, TCI_AUDIO_ENGINE_RATE, Sec);
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
// Рекордер один на приёмник, а не на клиента: пишет он то, что слышно
|
||||
// в аппарате, и второй такой же был бы просто копией памяти.
|
||||
Old := FRec[Rx];
|
||||
FRec[Rx] := New_;
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
Old.Free; // прежний — уже вне лока
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if M.Name = 'LINE_OUT_RECORDER_BREAK' then
|
||||
begin
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
Old := FRec[Rx];
|
||||
FRec[Rx] := nil;
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
Old.Free;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// LINE_OUT_RECORDER_SAVE
|
||||
Path := TCIRecordPath(TCIUnescape(TCIArg(M, 1)));
|
||||
if Path = '' then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'no file name']));
|
||||
Exit;
|
||||
end;
|
||||
// MP3 у нас кодировать нечем — молча подсунуть WAV с расширением .mp3 хуже,
|
||||
// чем сказать правду: клиент такой файл всё равно не откроет.
|
||||
if SameText(ExtractFileExt(Path), '.mp3') then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error',
|
||||
[LowerCase(M.Name), 'only wav is supported']));
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Забираем рекордер из таблицы (сохранение завершает запись, §4.3) и только
|
||||
// потом снимаем с него данные: копия кольца — это десятки мегабайт, и делать
|
||||
// её под локом DSP-потока нельзя.
|
||||
Data := nil;
|
||||
Rate := TCI_AUDIO_ENGINE_RATE;
|
||||
FStreamLock.Enter;
|
||||
try
|
||||
Old := FRec[Rx];
|
||||
FRec[Rx] := nil;
|
||||
finally
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
if Old <> nil then
|
||||
begin
|
||||
Data := Old.Take;
|
||||
Rate := Old.Rate;
|
||||
Old.Free;
|
||||
end;
|
||||
if Data = nil then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'nothing recorded']));
|
||||
Exit;
|
||||
end;
|
||||
// Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в
|
||||
// потоке клиента, который в это время не читает свой сокет.
|
||||
TTCIWavWriter.Create(Path, Data, Rate);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.ClearTxClient(C: TTCIClient);
|
||||
// C = nil — снять кого угодно (остановка сервера).
|
||||
var Drop: Boolean;
|
||||
begin
|
||||
Drop := False;
|
||||
FTxLock.Enter;
|
||||
try
|
||||
if (FTxClient <> nil) and ((C = nil) or (FTxClient = C)) then
|
||||
begin
|
||||
FTxClient := nil;
|
||||
FTxRunning := False;
|
||||
Drop := True;
|
||||
end;
|
||||
finally
|
||||
FTxLock.Leave;
|
||||
end;
|
||||
if not Drop then Exit;
|
||||
// Пишем прямо, без Invoke: это один Boolean, который SetMOX только читает, и
|
||||
// зовут нас откуда угодно — в том числе из деструктора адаптера, где ждать
|
||||
// поток контроллера уже некому. Передачу при этом НЕ трогаем: оператор мог
|
||||
// нажать PTT сам, и обрывать его эфир из-за ухода клиента нельзя.
|
||||
if FController <> nil then FController.TCIMicRequested := False;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.PushTxChrono;
|
||||
// Тик-поток. Маркер TX_CHRONO говорит клиенту «пришли столько-то отсчётов»
|
||||
// (§3.4). Пейсинг по часам: сколько времени прошло — столько и просим, плюс
|
||||
// разовая подушка TX_STREAM_AUDIO_BUFFERING на старте передачи. Ответа не
|
||||
// ждём: не успел клиент — в эфир уйдёт тишина, это его забота.
|
||||
var
|
||||
C: TTCIClient;
|
||||
Now_: QWord;
|
||||
Rate, Chans, Block: Integer;
|
||||
ST: TTCISampleType;
|
||||
H: TTCIStreamHeader;
|
||||
Active: Boolean;
|
||||
begin
|
||||
FTxLock.Enter;
|
||||
try
|
||||
C := FTxClient;
|
||||
finally
|
||||
FTxLock.Leave;
|
||||
end;
|
||||
if C = nil then Exit;
|
||||
|
||||
Active := FController.TCIMicActive;
|
||||
Now_ := GetTickCount64;
|
||||
Rate := C.AudioRate;
|
||||
Chans := EnsureRange(C.AudioChannels, 1, 2);
|
||||
Block := EnsureRange(C.AudioSamples, TCI_AUDIO_SAMPLES_MIN,
|
||||
TCI_AUDIO_SAMPLES_MAX);
|
||||
if not TCISampleTypeByName(C.AudioSampleType, ST) then ST := tsyFloat32;
|
||||
|
||||
FTxLock.Enter;
|
||||
try
|
||||
if not Active then
|
||||
begin
|
||||
FTxRunning := False;
|
||||
Exit;
|
||||
end;
|
||||
if not FTxRunning then
|
||||
begin
|
||||
FTxRunning := True;
|
||||
FTxLastMs := Now_;
|
||||
// Подушка: клиенту нужно время собрать первый блок, а тракт начнёт
|
||||
// забирать сэмплы сразу.
|
||||
FTxOwed := Rate * (C.TxBuffering / 1000.0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
FTxOwed := FTxOwed + Rate * ((Now_ - FTxLastMs) / 1000.0);
|
||||
FTxLastMs := Now_;
|
||||
// Клиент замолчал, а время идёт — потолок долга держим в один блок,
|
||||
// иначе после паузы на него обрушится пачка маркеров.
|
||||
if FTxOwed > 4 * Block then FTxOwed := 4 * Block;
|
||||
end;
|
||||
while FTxOwed >= Block do
|
||||
begin
|
||||
TCIFillHeader(H, tstTXChrono, 0, Rate, ST, Block, Chans);
|
||||
C.SendBin(H, nil, 0);
|
||||
FTxOwed := FTxOwed - Block;
|
||||
end;
|
||||
finally
|
||||
FTxLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.HandleBinary(Client: TTCIClient; Data: PByte; Len: Integer);
|
||||
// Поток клиента. Единственный бинарный кадр, который нам присылают, — блок
|
||||
// TX-аудио (§3.4). Всё прочее молча отбрасываем: отвечать ошибкой на каждый
|
||||
// чужой блок значит захлебнуться на клиенте, который шлёт их пачками.
|
||||
var
|
||||
H: TTCIStreamHeader;
|
||||
ST: TTCISampleType;
|
||||
P: PByte;
|
||||
N, i, k, Chans, Rate, Factor, Bytes: Integer;
|
||||
Mine: Boolean;
|
||||
begin
|
||||
if (Data = nil) or (Len <= SizeOf(H)) then Exit;
|
||||
Move(Data^, H, SizeOf(H));
|
||||
if H.StreamType <> LongWord(Ord(tstTXAudio)) then Exit;
|
||||
|
||||
FTxLock.Enter;
|
||||
try Mine := (FTxClient = Client); finally FTxLock.Leave; end;
|
||||
// Аудио от клиента, который не просил TRX:…,tci, — это не наша модуляция.
|
||||
// Принять его значит подмешать чужой звук в чужую же передачу.
|
||||
if not Mine then Exit;
|
||||
if not FController.TCIMicActive then Exit;
|
||||
|
||||
if H.Format > LongWord(Ord(tsyFloat32)) then Exit;
|
||||
ST := TTCISampleType(H.Format);
|
||||
|
||||
Chans := Integer(H.Channels);
|
||||
if Chans < 1 then Chans := 1;
|
||||
if Chans > 2 then Exit;
|
||||
Rate := Integer(H.SampleRate);
|
||||
if not TCIValidAudioRate(Rate) then Exit;
|
||||
// Тракт работает на 48 кГц; целое отношение — единственный случай, который
|
||||
// разрешает протокол (8/12/24/48), поэтому дробных пересчётов тут нет.
|
||||
if TCI_AUDIO_ENGINE_RATE mod Rate <> 0 then Exit;
|
||||
Factor := TCI_AUDIO_ENGINE_RATE div Rate;
|
||||
|
||||
Bytes := Len - SizeOf(H);
|
||||
P := Data;
|
||||
Inc(P, SizeOf(H));
|
||||
|
||||
FTxLock.Enter;
|
||||
try
|
||||
N := TCIUnpackSamples(P, Bytes, ST, FTxRaw);
|
||||
if N <= 0 then Exit;
|
||||
// Заголовок обещает length вещественных отсчётов — верим меньшему из двух:
|
||||
// клиент вправе прислать короткий хвост, но не длиннее уместившегося.
|
||||
if (H.DataLength > 0) and (Integer(H.DataLength) < N) then
|
||||
N := Integer(H.DataLength);
|
||||
|
||||
// В тракт идёт моно: TXA у нас один, а стерео от клиента — это его
|
||||
// собственный формат вывода, а не два независимых сигнала.
|
||||
if Chans = 2 then
|
||||
begin
|
||||
k := 0;
|
||||
i := 0;
|
||||
while i + 1 < N do
|
||||
begin
|
||||
FTxMono[k] := (FTxRaw[i] + FTxRaw[i + 1]) * 0.5;
|
||||
Inc(k);
|
||||
Inc(i, 2);
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
k := N;
|
||||
for i := 0 to N - 1 do FTxMono[i] := FTxRaw[i];
|
||||
end;
|
||||
if k <= 0 then Exit;
|
||||
|
||||
if (FTxInterp = nil) or (FTxInRate <> Rate) then
|
||||
begin
|
||||
FreeAndNil(FTxInterp);
|
||||
FTxInterp := TTCIInterpolator.Create(Factor);
|
||||
FTxInRate := Rate;
|
||||
end;
|
||||
N := FTxInterp.Process(FTxMono, k, FTxOut);
|
||||
finally
|
||||
FTxLock.Leave;
|
||||
end;
|
||||
if N > 0 then FController.PushTCIAudio(FTxOut, N);
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
Уведомления: изменение состояния контроллера → всем клиентам
|
||||
═══════════════════════════════════════════════════════════════════════════ }
|
||||
@@ -2316,6 +3059,9 @@ begin
|
||||
// сетевые потоки читают о плате.
|
||||
if Field in [rfDevice, rfConnected, rfDeviceList, rfXvtr, rfBand, rfSampleRate] then
|
||||
RefreshDev;
|
||||
// Пан могли убрать и без правки слайсов — тогда сигнатура карты не менялась,
|
||||
// а поток остался бы висеть на несуществующем приёмнике.
|
||||
if Field in [rfDevice, rfConnected] then DropDeadRxStreams;
|
||||
|
||||
MapChanged := False;
|
||||
if Field in [rfSliceFreq, rfSliceState, rfDevice, rfPanFreq, rfSampleRate,
|
||||
@@ -2401,9 +3147,20 @@ begin
|
||||
FServer.Broadcast(StrIf(0, 1));
|
||||
end;
|
||||
rfSampleRate:
|
||||
FServer.Broadcast(TCIBuild('if_limits',
|
||||
[TCIIntStr(-(FController.FSampleRate div 2)),
|
||||
TCIIntStr(FController.FSampleRate div 2)]));
|
||||
begin
|
||||
FServer.Broadcast(TCIBuild('if_limits',
|
||||
[TCIIntStr(-(FController.FSampleRate div 2)),
|
||||
TCIIntStr(FController.FSampleRate div 2)]));
|
||||
// Сменился rate устройства — сменилось и то, что мы можем отдать в
|
||||
// потоке IQ (у каждого клиента своё: просьбы разные). Молчать нельзя:
|
||||
// клиент, попросивший 384 кГц на HPSDR, после перехода на Pluto 576
|
||||
// получит 192 и должен об этом узнать, а не гадать по заголовкам.
|
||||
FServer.EnumClients(PushIQRate);
|
||||
end;
|
||||
rfDevice, rfConnected:
|
||||
// Смена устройства может утянуть за собой и rate (Pluto клампит чужой
|
||||
// rate к своему минимуму молча), поэтому переобъявляем и тут.
|
||||
FServer.EnumClients(PushIQRate);
|
||||
rfRunning:
|
||||
if FController.FRunning then FServer.Broadcast(TCIBuild('start'))
|
||||
else FServer.Broadcast(TCIBuild('stop'));
|
||||
|
||||
Reference in New Issue
Block a user