fix(display): хирургическая смена rate главного — паны не замирают

ChangeSampleRate больше не делает полный Close/Open движка (стоп DSP-потока,
разрушение всех анализаторов, сброс общей IQ-очереди): гейт FMainGated+FMainLock
дропает только main-пакеты, пересоздаются только RXA_CHAN и слайсы пана 0,
анализаторы панов живут. Контроллер не сбрасывает FWDSPReady на время смены
(OnDDCIQ продолжает кормить паны). Общий код RXA — Open/CloseMainRXChannel.
Бонус: SetChannelState dmode=1 на idle-каналах зря ждал флаш ~130мс → dmode=0
в CloseSliceChannel/CloseMainRXChannel (каналы всегда idle по локам).
Итог: смена 331→120мс, пауза аудио пан-слайса 137→40мс. Смоук ratetest ALL PASS,
подтверждено на экране.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 11:05:00 +03:00
co-authored by Claude Fable 5
parent a3a1ed0109
commit e26434575a
3 changed files with 215 additions and 76 deletions
+8 -5
View File
@@ -3167,14 +3167,17 @@ begin
FSpanHz := Hz; FSpanHz := Hz;
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
// 1. Останавливаем WDSP и меняем rate (Close + FlushQueue + Open). // 1. Меняем rate главного тракта. Хирургически: движок гейтит и дропает
// только main-пакеты, DSP-поток/анализаторы/паны N живут без пауз —
// поэтому FWDSPReady НЕ сбрасываем (иначе OnDDCIQ перестал бы кормить
// паны на всё время пересборки и их водопады замирали).
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(Hz); FDSPEngine.ChangeSampleRate(Hz);
end;
// 2. Новый rate трансиверу (после остановки DSP — пакеты сразу с верным rate). // 2. Новый rate трансиверу. Короткий хвост пакетов старого rate, дошедших
// до переключения радио, попадает в новый канал — транзиент в единицы мс
// (то же окно было и при старой схеме: FWDSPReady поднимался до того, как
// радио реально применяло DDC Specific).
if Assigned(FNetwork) and FNetwork.Connected then if Assigned(FNetwork) and FNetwork.Connected then
FNetwork.ConfigureDDCs(1, Hz div 1000, FMainADCSrc, FNetwork.ConfigureDDCs(1, Hz div 1000, FMainADCSrc,
FDitherEnabled, FRandomEnabled); FDitherEnabled, FRandomEnabled);
+174 -71
View File
@@ -284,6 +284,13 @@ type
FQueueTail: Integer; // читает DSP поток FQueueTail: Integer; // читает DSP поток
FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent) FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent)
FDSPRunning: Boolean; FDSPRunning: Boolean;
// Гейт главного тракта на время хирургической смены rate (ChangeSampleRate):
// True = DSP-поток дропает main-пакеты (PanId=0) не блокируясь, паны
// обрабатываются как обычно. FMainLock — барьер: DSP-поток держит его на
// время обработки main-пакета, ChangeSampleRate делает Enter/Leave после
// установки гейта, гарантируя что fexchange0 на RXA_CHAN завершён.
FMainGated: Boolean;
FMainLock: TCriticalSection;
FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single; FSpectrumPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single; FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single;
@@ -461,6 +468,8 @@ type
procedure PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem); procedure PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem);
procedure ProcessRXBlock; procedure ProcessRXBlock;
procedure PushIQItemToDSP(const Item: TIQQueueItem); procedure PushIQItemToDSP(const Item: TIQQueueItem);
procedure OpenMainRXChannel; // RXA_CHAN под текущие FBufSize/FSampleRate
procedure CloseMainRXChannel; // state=0 + EXT-блоки + CloseChannel(RXA_CHAN)
procedure OpenAnalyzer; procedure OpenAnalyzer;
procedure CloseAnalyzer; procedure CloseAnalyzer;
procedure ApplyAnalyzerSettings; procedure ApplyAnalyzerSettings;
@@ -877,7 +886,22 @@ begin
begin begin
Item := @FEngine.FQueue[FEngine.FQueueTail]; Item := @FEngine.FQueue[FEngine.FQueueTail];
if Item^.PanId = 0 then if Item^.PanId = 0 then
FEngine.PushIQItemToDSP(Item^) begin
// Гейт смены rate главного (ChangeSampleRate): main-пакеты дропаем
// НЕ блокируясь — пан-пакеты позади в очереди не должны ждать
// пересборку RXA. Повторная проверка под локом закрывает гонку
// с установкой гейта из UI-потока.
if not FEngine.FMainGated then
begin
FEngine.FMainLock.Enter;
try
if not FEngine.FMainGated then
FEngine.PushIQItemToDSP(Item^);
finally
FEngine.FMainLock.Leave;
end;
end;
end
else else
FEngine.PushPanItemToDSP(Item^.PanId, Item^); FEngine.PushPanItemToDSP(Item^.PanId, Item^);
FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1); FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1);
@@ -1130,6 +1154,10 @@ begin
FDSPThread := nil; FDSPThread := nil;
FDisplayThread := nil; FDisplayThread := nil;
// Гейт главного тракта (хирургическая смена rate)
FMainGated := False;
FMainLock := TCriticalSection.Create;
// Мультислайсы // Мультислайсы
FSliceLock := TCriticalSection.Create; FSliceLock := TCriticalSection.Create;
FillChar(FSlices, SizeOf(FSlices), 0); FillChar(FSlices, SizeOf(FSlices), 0);
@@ -1212,6 +1240,7 @@ begin
RTLEventDestroy(FQueueSem); RTLEventDestroy(FQueueSem);
RTLEventDestroy(FTXMicSem); RTLEventDestroy(FTXMicSem);
FSliceLock.Free; FSliceLock.Free;
FMainLock.Free;
inherited; inherited;
end; end;
@@ -1515,38 +1544,11 @@ begin
Exit; Exit;
end; end;
OpenChannel( OpenMainRXChannel;
RXA_CHAN, // TXA только что создан узким; забываем «широкую» геометрию прошлой
FBufSize, // сессии (RX-половину сбрасывает OpenMainRXChannel).
FAudioBufSize,
FSampleRate,
FAudioRate,
FAudioRate,
0,
0, // state=0: создать но не запускать
0.010, 0.010, 0.010, 0.010,
0
);
// Каналы только что созданы с dsp_rate = FAudioRate; забываем «широкую»
// геометрию прошлой сессии (Close/Open через ChangeSampleRate).
FRXDSPRate := FAudioRate;
FTXDSPSize := FAudioBufSize; FTXDSPSize := FAudioBufSize;
create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
SetRXABandpassWindow(RXA_CHAN, 1);
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
SetRXAShiftRun(RXA_CHAN, 1);
SetRXAShiftFreq(RXA_CHAN, 0.0);
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
SetAGC(FAGCMode, 50.0);
SetRXAPanelGain1(RXA_CHAN, FVolume);
SetRXAPanelSelect(RXA_CHAN, 3);
SetRXAPanelRun(RXA_CHAN, 1);
SetChannelState(RXA_CHAN, 1, 0);
// TXA: mic @ FAudioRate → IQ @ FTXSampleRate (фиксированный DUC rate, не зависит от RX). // TXA: mic @ FAudioRate → IQ @ FTXSampleRate (фиксированный DUC rate, не зависит от RX).
// out_size = FAudioBufSize * FTXSampleRate/FAudioRate = FTXOutBufSize (4096 @ 192k tx). // out_size = FAudioBufSize * FTXSampleRate/FAudioRate = FTXOutBufSize (4096 @ 192k tx).
// FTXOut уже имеет размер FTXOutBufSize*2. // FTXOut уже имеет размер FTXOutBufSize*2.
@@ -1649,56 +1651,153 @@ begin
end; end;
CloseAnalyzer; CloseAnalyzer;
destroy_anbEXT(RXA_CHAN); CloseMainRXChannel;
destroy_nobEXT(RXA_CHAN);
CloseChannel(RXA_CHAN);
CloseChannel(TXA_CHAN); CloseChannel(TXA_CHAN);
FInitialized := False; FInitialized := False;
end; end;
procedure TWDSPEngine.OpenMainRXChannel;
// Открывает RXA_CHAN под текущие FBufSize/FSampleRate и восстанавливает его
// состояние из полей движка. Канал создаётся «узким» (dsp_rate = FAudioRate);
// широкий WFM поднимает вызывающий через SetMode (ApplyDSPRate).
begin
OpenChannel(
RXA_CHAN,
FBufSize,
FAudioBufSize,
FSampleRate,
FAudioRate,
FAudioRate,
0,
0, // state=0: создать но не запускать
0.010, 0.010, 0.010, 0.010,
0
);
FRXDSPRate := FAudioRate;
create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
SetRXABandpassWindow(RXA_CHAN, 1);
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
SetRXAShiftRun(RXA_CHAN, 1);
SetRXAShiftFreq(RXA_CHAN, FShiftHz);
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
SetAGC(FAGCMode, 50.0);
SetRXAPanelGain1(RXA_CHAN, FVolume);
SetRXAPanelSelect(RXA_CHAN, 3);
SetRXAPanelRun(RXA_CHAN, 1);
SetChannelState(RXA_CHAN, 1, 0);
end;
procedure TWDSPEngine.CloseMainRXChannel;
// dmode=0: канал к этому моменту гарантированно idle (Close — DSP-поток уже
// мёртв; ChangeSampleRate — FMainGated+барьер), а dmode=1 на простаивающем
// канале зря ждёт флаш ~130мс.
begin
SetChannelState(RXA_CHAN, 0, 0);
destroy_anbEXT(RXA_CHAN);
destroy_nobEXT(RXA_CHAN);
CloseChannel(RXA_CHAN);
end;
procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer); procedure TWDSPEngine.ChangeSampleRate(NewRate: Integer);
// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open. // Смена rate главного тракта ХИРУРГИЧЕСКИ: DSP-поток, display-поток,
// Панов не трогает: их rate свой; аккумуляторы сбрасываем (очередь общая, // анализаторы и паны N живут — закрываем/переоткрываем только RXA_CHAN и
// их пакеты умерли вместе с ней), анализаторы пересоздаст OpenAnalyzer. // слайс-каналы пана 0. Пакеты панов идут через общую очередь без пауз:
// DSP-поток лишь дропает main-пакеты, пока стоит FMainGated. (Раньше здесь
// был полный Close/Open движка — паны замирали на всё время пересборки.)
var var
p: Integer; i: Integer;
begin begin
if NewRate = FSampleRate then Exit; if NewRate = FSampleRate then Exit;
// Останавливаем текущий канал if not FInitialized then
Close; begin
// Движок не открыт: только запоминаем параметры — Open создаст всё сам.
FSampleRate := NewRate;
FBufSize := FAudioBufSize * NewRate div FAudioRate;
SetLength(FRXIn, FBufSize * 2);
SetLength(FRXOut, FBufSize * 2);
SetLength(FSpecBuf, FBufSize * 2);
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
FRXAccPos := 0;
Exit;
end;
// Сбрасываем очередь IQ пакетов — старые пакеты не совместимы с новым rate. // Гейт: DSP-поток перестаёт брать main-пакеты (дропает их из очереди);
// Без этого DSP поток тратит секунды на разбор накопившихся пакетов. // барьер Enter/Leave гарантирует, что текущий PushIQItemToDSP
FQueueHead := 0; // (fexchange0 на RXA_CHAN) завершён.
FQueueTail := 0; FMainGated := True;
FRXAccPos := 0; FMainLock.Enter;
for p := 1 to MAX_PANS - 1 do FMainLock.Leave;
if FPans[p].Active then try
begin try
FPans[p].AccPos := 0; // Слайсы пана 0 открыты с InRate = главный rate — закрываем на пересборку.
FPans[p].DispPos := 0; FSliceLock.Enter;
try
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Opened and (FSlices[i].PanId = 0) then
CloseSliceChannel(i);
finally
FSliceLock.Leave;
end;
CloseMainRXChannel;
// Новый rate/буферы главного тракта.
FSampleRate := NewRate;
FBufSize := FAudioBufSize * NewRate div FAudioRate;
SetLength(FRXIn, FBufSize * 2);
SetLength(FRXOut, FBufSize * 2);
SetLength(FSpecBuf, FBufSize * 2);
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
FRXAccPos := 0;
FDispPos := 0;
AllocSliceBuffers;
// Переоткрываем главный канал и слайсы пана 0 (состояние из полей/записей).
OpenMainRXChannel;
if FMode = MODE_WFM then
begin
// Канал создан узким — SetMode поднимет dsp_rate под новый FSampleRate,
// SetFilter пересчитает полосу (как в Open).
SetMode(FMode);
SetFilter(FFilterLow, FFilterHigh);
end;
ApplyNoiseFilterState;
FSliceLock.Enter;
try
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and (not FSlices[i].Opened)
and (FSlices[i].PanId = 0) then
OpenSliceChannel(i);
finally
FSliceLock.Leave;
end;
// RX-анализатор не разрушался — переармируем под новый rate. FFT-история
// главного дисплея сбрасывается (при смене rate это неизбежно), паны
// не затронуты.
if FAnalyzerOpen then ApplyRXAnalyzerSettings;
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
except
on E: Exception do
begin
// Главный тракт в неопределённом состоянии — гасим FInitialized:
// ProcessRXBlock/слайсы/паны выходят по нему, контроллер увидит
// по Initialized и сбросит FWDSPReady.
FLastError := E.ClassName + ': ' + E.Message;
FInitialized := False;
end;
end; end;
finally
// Обновляем параметры FMainGated := False;
FSampleRate := NewRate; end;
FBufSize := FAudioBufSize * NewRate div FAudioRate;
// Перераспределяем буферы под новый размер
SetLength(FRXIn, FBufSize * 2);
SetLength(FRXOut, FBufSize * 2);
SetLength(FSpecBuf, FBufSize * 2);
SetLength(FRXAccI, FBufSize);
SetLength(FRXAccQ, FBufSize);
SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2);
FDispPos := 0;
AllocSliceBuffers;
// Переинициализируем WDSP с новым rate
Open;
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
end; end;
procedure TWDSPEngine.SetBeaconDecoder(D: TBeaconDecoder); procedure TWDSPEngine.SetBeaconDecoder(D: TBeaconDecoder);
@@ -2112,9 +2211,13 @@ begin
end; end;
procedure TWDSPEngine.CloseSliceChannel(Idx: Integer); procedure TWDSPEngine.CloseSliceChannel(Idx: Integer);
// dmode=0: fexchange0 слайс-каналов зовётся ТОЛЬКО под FSliceLock (ProcessSlices/
// PushPanItemToDSP), и все вызовы CloseSliceChannel — под тем же локом, т.е.
// канал заведомо idle. dmode=1 здесь зря ждал флаш ~130мс на канал — при смене
// rate главного эта пауза (под локом) замораживала обработку панов.
begin begin
if not FSlices[Idx].Opened then Exit; if not FSlices[Idx].Opened then Exit;
SetChannelState(FSlices[Idx].Chan, 0, 1); SetChannelState(FSlices[Idx].Chan, 0, 0);
destroy_anbEXT(FSlices[Idx].Chan); destroy_anbEXT(FSlices[Idx].Chan);
destroy_nobEXT(FSlices[Idx].Chan); destroy_nobEXT(FSlices[Idx].Chan);
CloseChannel(FSlices[Idx].Chan); CloseChannel(FSlices[Idx].Chan);
+33
View File
@@ -512,6 +512,39 @@ ChangeSampleRate).
(в коде НЕ делалось — сначала экранный тест). (в коде НЕ делалось — сначала экранный тест).
- Персист геометрии — вместе с общим персистом панов. - Персист геометрии — вместе с общим персистом панов.
**СТАТУС «хирургический ChangeSampleRate» (2026-07-12): ГОТОВО — смоук-тест
ALL PASS, юзер подтвердил на экране («всё работает»). Отладочные пробы
RATEPROBE удалены.** Наблюдение юзера: смена rate ГЛАВНОГО пана
коротко замораживала водопады панов N (обратное — нет). Причина была в движке:
`ChangeSampleRate` делал полный Close/Open (стоп DSP-потока, разрушение ВСЕХ
анализаторов вкл. пановых, закрытие всех слайс-каналов, сброс общей IQ-очереди).
Протокол чист: оба пути шлют одинаковый полный DDC Specific (`RebuildDDCSpecific`).
Сделано (WDSPEngine.pas + RadioController.pas):
- Гейт `FMainGated` + барьер `FMainLock`: DSP-поток при взведённом гейте дропает
main-пакеты (PanId=0) из общей очереди НЕ блокируясь, паны обрабатываются без
пауз; двойная проверка гейта (снаружи и под локом) закрывает гонку установки.
- `ChangeSampleRate` теперь хирургический: закрывает/переоткрывает ТОЛЬКО
RXA_CHAN + слайс-каналы пана 0 (их InRate = главный rate); DSP/display-потоки,
TXA, TX-поток, анализаторы и паны не трогаются. RX-анализатор переармируется
`ApplyRXAnalyzerSettings` (как при зуме) — история главного сбрасывается
(неизбежно), пановые НЕ. Ошибка реконфига → `FInitialized:=False` (контроллер
увидит по `Initialized`).
- Общий код RXA вынесен: `OpenMainRXChannel` (открытие+реставрация состояния из
полей движка, вкл. FShiftHz) / `CloseMainRXChannel` — используются Open/Close/
ChangeSampleRate.
- Контроллер `SetSampleRate` больше НЕ сбрасывает `FWDSPReady` на время смены —
иначе OnDDCIQ переставал кормить паны (это был второй источник паузы).
- ★Находка по пробам: `SetChannelState(chan,0,dmode=1)` на ПРОСТАИВАЮЩЕМ канале
зря ждёт флаш ~130мс. Каналы при закрытии всегда idle (слайсы — дисциплина
FSliceLock, RXA — гейт-барьер) → dmode=1→0 в `CloseSliceChannel` и
`CloseMainRXChannel`. Итог: смена rate 331→120мс, макс. пауза аудио слайса
пана 137→40мс (~2 блока, на глаз незаметно). Бонус: закрытие слайсов/панов и
SetPanRate тоже ускорились на ~130мс/канал.
- Смоук-тест scratchpad `ratetest.pas` (синтетический тон, фидер-поток, замер
межблочных пауз аудио пан-слайса вокруг смены 384к→192к): ALL PASS — пан без
паузы >41мс, main-аудио/слайс пана 0/слайс пана 1 живы и слышат тон после
смены. Оба бинарника собираются.
### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе ### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе
Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён). Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).