From e26434575ac697c3103b97a6184267730f19a497 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sun, 12 Jul 2026 11:05:00 +0300 Subject: [PATCH] =?UTF-8?q?fix(display):=20=D1=85=D0=B8=D1=80=D1=83=D1=80?= =?UTF-8?q?=D0=B3=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B0=D1=8F=20=D1=81=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20rate=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=E2=80=94=20=D0=BF=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B7=D0=B0=D0=BC=D0=B8=D1=80=D0=B0=D1=8E=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- RadioController.pas | 13 ++- WDSPEngine.pas | 245 +++++++++++++++++++++++++++++++------------- doc/SLICES_PLAN.md | 33 ++++++ 3 files changed, 215 insertions(+), 76 deletions(-) diff --git a/RadioController.pas b/RadioController.pas index 4c91bd1..12e78c8 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -3167,14 +3167,17 @@ begin FSpanHz := Hz; FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости - // 1. Останавливаем WDSP и меняем rate (Close + FlushQueue + Open). + // 1. Меняем rate главного тракта. Хирургически: движок гейтит и дропает + // только main-пакеты, DSP-поток/анализаторы/паны N живут без пауз — + // поэтому FWDSPReady НЕ сбрасываем (иначе OnDDCIQ перестал бы кормить + // паны на всё время пересборки и их водопады замирали). if FWDSPReady and Assigned(FDSPEngine) then - begin - FWDSPReady := False; FDSPEngine.ChangeSampleRate(Hz); - end; - // 2. Новый rate трансиверу (после остановки DSP — пакеты сразу с верным rate). + // 2. Новый rate трансиверу. Короткий хвост пакетов старого rate, дошедших + // до переключения радио, попадает в новый канал — транзиент в единицы мс + // (то же окно было и при старой схеме: FWDSPReady поднимался до того, как + // радио реально применяло DDC Specific). if Assigned(FNetwork) and FNetwork.Connected then FNetwork.ConfigureDDCs(1, Hz div 1000, FMainADCSrc, FDitherEnabled, FRandomEnabled); diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 59317b4..831b54f 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -284,6 +284,13 @@ type FQueueTail: Integer; // читает DSP поток FQueueSem: PRTLEvent; // сигнал: есть новые данные (RTLEvent) 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; FWaterfallPixels: array[0..SPECTRUM_PIXELS - 1] of Single; @@ -461,6 +468,8 @@ type procedure PushPanItemToDSP(PanId: Integer; const Item: TIQQueueItem); procedure ProcessRXBlock; procedure PushIQItemToDSP(const Item: TIQQueueItem); + procedure OpenMainRXChannel; // RXA_CHAN под текущие FBufSize/FSampleRate + procedure CloseMainRXChannel; // state=0 + EXT-блоки + CloseChannel(RXA_CHAN) procedure OpenAnalyzer; procedure CloseAnalyzer; procedure ApplyAnalyzerSettings; @@ -877,7 +886,22 @@ begin begin Item := @FEngine.FQueue[FEngine.FQueueTail]; 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 FEngine.PushPanItemToDSP(Item^.PanId, Item^); FEngine.FQueueTail := (FEngine.FQueueTail + 1) and (IQ_QUEUE_SIZE - 1); @@ -1130,6 +1154,10 @@ begin FDSPThread := nil; FDisplayThread := nil; + // Гейт главного тракта (хирургическая смена rate) + FMainGated := False; + FMainLock := TCriticalSection.Create; + // Мультислайсы FSliceLock := TCriticalSection.Create; FillChar(FSlices, SizeOf(FSlices), 0); @@ -1212,6 +1240,7 @@ begin RTLEventDestroy(FQueueSem); RTLEventDestroy(FTXMicSem); FSliceLock.Free; + FMainLock.Free; inherited; end; @@ -1515,38 +1544,11 @@ begin Exit; end; - OpenChannel( - RXA_CHAN, - FBufSize, - 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; + OpenMainRXChannel; + // TXA только что создан узким; забываем «широкую» геометрию прошлой + // сессии (RX-половину сбрасывает OpenMainRXChannel). 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). // out_size = FAudioBufSize * FTXSampleRate/FAudioRate = FTXOutBufSize (4096 @ 192k tx). // FTXOut уже имеет размер FTXOutBufSize*2. @@ -1649,56 +1651,153 @@ begin end; CloseAnalyzer; - destroy_anbEXT(RXA_CHAN); - destroy_nobEXT(RXA_CHAN); - CloseChannel(RXA_CHAN); + CloseMainRXChannel; CloseChannel(TXA_CHAN); FInitialized := False; 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); -// Меняет samplerate на лету: Close → сброс очереди → пересчёт буферов → Open. -// Панов не трогает: их rate свой; аккумуляторы сбрасываем (очередь общая, -// их пакеты умерли вместе с ней), анализаторы пересоздаст OpenAnalyzer. +// Смена rate главного тракта ХИРУРГИЧЕСКИ: DSP-поток, display-поток, +// анализаторы и паны N живут — закрываем/переоткрываем только RXA_CHAN и +// слайс-каналы пана 0. Пакеты панов идут через общую очередь без пауз: +// DSP-поток лишь дропает main-пакеты, пока стоит FMainGated. (Раньше здесь +// был полный Close/Open движка — паны замирали на всё время пересборки.) var - p: Integer; + i: Integer; begin if NewRate = FSampleRate then Exit; - // Останавливаем текущий канал - Close; + if not FInitialized then + 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 поток тратит секунды на разбор накопившихся пакетов. - FQueueHead := 0; - FQueueTail := 0; - FRXAccPos := 0; - for p := 1 to MAX_PANS - 1 do - if FPans[p].Active then - begin - FPans[p].AccPos := 0; - FPans[p].DispPos := 0; + // Гейт: DSP-поток перестаёт брать main-пакеты (дропает их из очереди); + // барьер Enter/Leave гарантирует, что текущий PushIQItemToDSP + // (fexchange0 на RXA_CHAN) завершён. + FMainGated := True; + FMainLock.Enter; + FMainLock.Leave; + try + try + // Слайсы пана 0 открыты с InRate = главный rate — закрываем на пересборку. + 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; - - // Обновляем параметры - 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); - SetLength(FDispBuf, DISPLAY_BLOCK_SIZE * 2); - FDispPos := 0; - AllocSliceBuffers; - - // Переинициализируем WDSP с новым rate - Open; - - // Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate. - if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate); + finally + FMainGated := False; + end; end; procedure TWDSPEngine.SetBeaconDecoder(D: TBeaconDecoder); @@ -2112,9 +2211,13 @@ begin end; procedure TWDSPEngine.CloseSliceChannel(Idx: Integer); +// dmode=0: fexchange0 слайс-каналов зовётся ТОЛЬКО под FSliceLock (ProcessSlices/ +// PushPanItemToDSP), и все вызовы CloseSliceChannel — под тем же локом, т.е. +// канал заведомо idle. dmode=1 здесь зря ждал флаш ~130мс на канал — при смене +// rate главного эта пауза (под локом) замораживала обработку панов. begin 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_nobEXT(FSlices[Idx].Chan); CloseChannel(FSlices[Idx].Chan); diff --git a/doc/SLICES_PLAN.md b/doc/SLICES_PLAN.md index 4a01984..eac8195 100644 --- a/doc/SLICES_PLAN.md +++ b/doc/SLICES_PLAN.md @@ -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.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).