# Мультислайсы (несколько приёмных фильтров, как во FlexRadio) Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`, коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`, `3049f91`, `a3ac459`). Этап 3 (аппаратные DDC для слайса вне захваченной полосы) — не начат. Ниже: что вошло по факту + дизайн/остаток. ## Что реализовано (Этапы 1 + 2) - **WDSPEngine** — софтовый fan-out: `TSliceChan` + `FSlices[0..MAX_SLICES-1]` (`MAX_SLICES=6`), свои WDSP-каналы (`SLICE_CHAN_BASE=2`), `ProcessSlices` в DSP-потоке (свежая копия IQ на канал, свой NCO/mode/filter/AGC/NR/NB/SNB/ANF + S-метр → `OnSliceAudio`), `FSliceLock` сериализует add/remove/set vs DSP, переоткрытие после смены sample rate. - **RadioController** — `TCtrlSlice` (абс. `TargetHz`, буква, свой `TAudioOutput`/ звуковуха), `Add/Remove/SetSlice*`, `RefreshSliceShifts` держит слайсы на `TargetHz` при смене центра, гейт `SliceFitsCapture` (вне полосы = Этап 3). Буквы доп. слайсов начинаются с `'B'` (A = главный приёмник). - **UI** — по флагу `TVfoOverlay` на слайс (буква-бейдж, «×», fly-out MODE/DSP/AUD, vol/mute/split/TX, S-метр), маркеры полосы фильтра слайса в **обоих** рендерерах (GL `DrawSliceFilterMarkers` + CPU), drag-ретюн несущей, колесо над флагом. Ctrl+ЛКМ по спектру = добавить слайс. Флаг главного (A) виден когда скрыта левая панель ЛИБО есть слайсы B+ (синхронизируется с правками с левой панели). - **UI-полировка флагов** (`30a6616`, `3049f91`, `a3ac459`): - `MainForm.LayoutFlags` — единый packing всех флагов (A+B+) в один ряд: каждый рядом со своей полосой фильтра, но флаги НЕ наезжают (сосед слева толкает правого; обратный поджим при выходе за правый край). `PositionVfoOverlay`/ `PositionSliceFlags` — тонкие обёртки над ним. - Идентификация уехавшего флага — буквой слайса по центру его полосы фильтра сверху (`DrawBandLetter` CPU + `DrawBandLetterGL` GL с кэшем текстур букв); у главного A буква только когда есть слайсы. - Палитра слайсов `VfoOverlay.SliceColor(L)` (A зелёный … F фиолетовый, не красный — не путать с TX): один цвет для бейджа-буквы во флаге и для буквы на полосе. - Кнопки флага — общий `FlatButton.PaintFlatButton` (плоские, как левая панель; без «стеклянного» градиента), центровка бейджа через `TextRect`. - Флаг полностью тематизирован: структурные `CLR_*` выводятся из `TAppTheme` (`ApplyThemeColors`), тема раздаётся из `ApplyDarkTheme` + `WireOverlayEvents`. Статус-акценты (TX/SPLIT) фиксированы намеренно. - **Перф** — S-метр развязан с кэшем «хрома» флага (`FMeterDirty` + meter-only `RefreshMeter` через `EnsureRendered`); `InvalidateVfoOverlay` не дёргает глобальный перезалив оверлеев на тик метра. Остаток/риски (не сделано): персист слайсов в `Settings` (список + устройство); per-slice AGC-Top (сейчас общий с главным); полноценный мультислайс-TX (пока TX только главный); Этап 3 (HW DDC). Исходный план/дизайн — ниже без изменений. ## Предыстория (UI-каркас до мультислайса — исторично) > Ниже — состояние ДО реализации Этапа 2 (одиночный флаг, захардкоженная `'A'`). > Актуальное состояние см. в «Что реализовано» выше. Оставлено как контекст. `VfoOverlay.pas` переделан в **«флаг слайса»** в стиле SmartSDR (6 коммитов `4a92256..a04c592`, уже в `main`). Это ровно UI-управление одним слайсом из Этапа 2, только пока в единственном экземпляре: - Флаг-накладка поверх спектра с буквой слайса (`FSlice`, сейчас захардкожено `'A'`), fly-out пикерами **MODE / DSP / AUD**, слайдером громкости, mute, SPLIT, кликабельным **TX-бейджем** (выбор TX-слайса) и S-метром. - **Per-flag выбор аудио-устройства** уже есть: `SetAudioDevices` + `OnAudioDevice` (`VfoOverlay.pas:31,125,136`) — прокручиваемый список в AUD-fly-out. Это готовый UI под «своё звуковое устройство на слайс» из абстракции `TSlice.DeviceIndex`. - События наружу: `OnSelect/OnDSPChange/OnAGCChange/OnVolume/OnMute/OnAudioDevice/` `OnSplit/OnTxSelect/OnInvalidate` (`VfoOverlay.pas:131-139`). - Автопозиционирование: флаг флипается влево, если не влезает справа (`6ad5f29`). Привязка в приложении **пока одиночная**: один `FVfoOverlay` в `MainForm` (`MainForm.pas:125`), созданный/подключённый на `MainForm.pas:962-972`, привязан к VFO A (`UpdateVfo(FController.FVfoA)`, `MainForm.pas:3265`); хендлеры `OnVfoOverlay*` шлют изменения в главный тракт. `SpectrumView` держит один `FVfoOverlay` и рисует его в `DrawOverlay` (`SpectrumView.pas:72,220,1010`). Чтобы превратить это в мультислайс — нужно из одиночного флага сделать **коллекцию флагов (по одному на слайс)** и развести их события на конкретный `SliceId` вместо глобального VFO A. Детали — в обновлённом Этапе 2. ## Цель Несколько независимых приёмных «слайсов» на одном ADC/одном IQ-потоке: у каждого слайса своя частота, режим, полоса, AGC и **своё звуковое устройство** (как DAX/наушники/динамик во Flex). Панадаптер (спектр+водопад) остаётся один, слайсы рисуются маркерами поверх. ## Как это устроено у Flex и что из этого берём Слайс во Flex — это **не «ещё один фильтр» на одном приёмнике**, а полноценный независимый узкополосный приёмник (свой DDC в FPGA + демодулятор + фильтр + AGC + аудио). Общий у слайсов — только панадаптер (широкий FFT-захват). Один WDSP-канал = один демодулятор = один фильтр = один аудиопоток. Значит для «разный звук/режим/полоса на каждый слайс» нужен **отдельный WDSP-канал на слайс**. Способ получить узкополосный IQ для канала различается по железу — см. ниже. ## Ключевое различие бэкендов ``` openHPSDR (Protocol 2, V4.3, MAX_DDCS=80): ADC0 ─┬─ DDC (FPGA-тюнер) → IQ-поток порт 1035+n → WDSP-канал → фильтр/AGC → AudioOut[dev] ├─ DDC → ... └─ DDC → ... • Аппаратные DDC: слайс тюнится КУДА УГОДНО в полосе ADC (весь КВ), дёшево по CPU. • На Angelia/Orion/Saturn DDC0/DDC1 заняты PureSignal → слайсы с DDC2+ (в коде DDCBase=2). • RX2 = ВТОРОЙ ADC (ADC1), отдельная антенна/diversity — ОРТОГОНАЛЬНО слайсам, не нужно. Pluto (AD9363, libiio): один RX-тракт: один LO, один rf_bandwidth, один IQ-поток (притворяется одним DDC, пушится как TDDCIQPacket в тот же конвейер). • Аппаратных DDC НЕТ. Слайсы = СОФТОВЫЙ fan-out одного IQ-блока на N WDSP-каналов, каждый со своим SetRXAShiftFreq в пределах захваченной полосы вокруг LO. • Для QO-100 идеально: весь транспондер (~500 кГц) в захвате, LO залочен на маяк. ``` ## Единая абстракция ```pascal TSlice = record Id: Integer; TargetHz: Double; // абсолютная частота слайса Mode: TRxMode; // USB/LSB/CW/FM/... FilterLo, FilterHi: Integer; AGCMode: TWDSPAGCMode; Volume: Double; WDSPChannel: Integer; // канал WDSP (0..N); 0=главный, 1=TX заняты Source: (srcSoftShift, srcHardDDC); // как получаем узкий IQ DDCIndex: Integer; // если srcHardDDC — какой аппаратный DDC ShiftHz: Double; // если srcSoftShift — сдвиг = TargetHz - CapturedCenterHz AudioOut: TAudioOutput; // свой инстанс со своим DeviceIndex DeviceIndex: Integer; // выбранная звуковуха (или -1 = default) Enabled: Boolean; end; ``` Правило маршрутизации по частоте (одно на оба бэкенда): - слайс **внутри** текущего захваченного span → `srcSoftShift` (Pluto и HPSDR); - слайс **вне** захваченного span → `srcHardDDC` (только HPSDR; Pluto так не умеет). ## Порядок работ ### Этап 1 — Софтовый fan-out в WDSPEngine (backend-agnostic) ✅ РЕАЛИЗОВАН Даёт слайсы сразу на Pluto/QO-100 (единственный режим) и на HPSDR для слайсов внутри видимой полосы. Pluto уже пушит IQ через тот же DDC-конвейер, поэтому fan-out живёт в движке и не зависит от бэкенда. - `WDSPEngine.pas`: сейчас `RXA_CHAN=0` один канал, один `FRXOut`, один аудиовыход (`WDSPEngine.pas:96`, alloc буферов ~`:159`, OpenChannel ~`:1081`). - Добавить массив слайс-каналов: для каждого активного слайса `OpenChannel(chN, ...)` с той же геометрией, что RXA_CHAN, + `SetRXAShiftRun/SetRXAShiftFreq(chN, ShiftHz)` (механизм уже используется: `WDSPEngine.pas:1099-1100`), `SetRXAMode`, `RXASetPassband`, AGC, `SetRXAPanelGain1`. - В DSP-потоке после накопления IQ-блока (`FRXAccI/Q`, `FRXAccPos`) прогонять `fexchange0` по КАЖДОМУ активному каналу из ОДНОГО и того же входного буфера (входной буфер не портить in-place между каналами — держать копию, как `FSpecBuf`). - Выход каждого канала → свой callback `OnSliceAudio(SliceId, L, R, Count)`. - `AudioOutput.pas`: уже готов к мульти-устройству — `DeviceIndex`, `EnumOutputDevices`, `FindDeviceByName` (`AudioOutput.pas:137-151`). Создавать **отдельный `TAudioOutput` на слайс**, каждый со своим `DeviceIndex`. - `RadioController.pas`: `TSlice[]` (add/remove/retune/setmode/setdevice), роутинг `OnSliceAudio` → нужный `TAudioOutput`. Проверка Этапа 1: Pluto/QO-100 — 2 слайса (VFO + маяк-монитор), звук в две разные звуковухи, независимые режимы/полосы. Сборка: `lazbuild --ws=qt6 ewsdr.lpr`. ### Этап 2 — UI слайсов на водопаде + выбор устройства ✅ РЕАЛИЗОВАН (кроме персиста) Каркас флага слайса уже сделан (см. «Что уже готово»): один `TVfoOverlay` со всеми пикерами и per-flag выбором устройства. Остаётся размножить его на N слайсов и привязать к позиции по частоте. Выпадашка устройства и dropdown-паттерн **уже есть в самом флаге** (AUD fly-out) — отдельный `DeviceForm` не нужен. - **Коллекция флагов вместо одного.** Из `MainForm.FVfoOverlay` (одиночный, `MainForm.pas:125`) сделать `array of TVfoOverlay` / список, по флагу на слайс; `SpectrumView.FVfoOverlay` (`SpectrumView.pas:72`) — тоже коллекция, рисовать все в `DrawOverlay` (`SpectrumView.pas:1010`). Разместить: `SetSlice(Id, Letter)` в оверлей, события `OnVfoOverlay*` должны нести `SliceId` (сейчас бьют по VFO A напрямую — завести маппинг оверлей→слайс, роутить в нужный `TSlice`/WDSP-канал из Этапа 1). - **Позиционирование по частоте.** `PositionVfoOverlay` (`MainForm.pas:518`) сейчас ставит один флаг; расширить на маппинг `TargetHz → X` через `GetViewWindow`/ `SyncSpecViewFreq` (зум-работа). Слайс за краем видимой полосы — маркер у края (флип влево/вправо уже есть, `6ad5f29`). - **ЛКМ по водопаду** = поставить/двинуть активный слайс; выделение активного (подсветка буквы/рамки флага). Хит-тест мыши флага уже разведён (`HandleMouseDown/Move/Up`, `MainForm.pas:4424`) — добавить «клик мимо флага по спектру = ретюн активного слайса». - **TX-select** уже кликабелен (`OnTxSelect`, `08c71c4`) — при мультислайсе выбирать, какой слайс идёт в TX; снимать флаг с остальных. - Персист: список слайсов в `Settings.pas` (per-band или глобально — решить), включая `DeviceIndex`/имя устройства каждого флага. ### Этап 3 — Аппаратные DDC для openHPSDR (слайс куда угодно по КВ) ⏳ НЕ НАЧАТ Апгрейд поверх общего каркаса; нужен только чтобы ставить слайс ВНЕ захваченной полосы. Плита ~на 70% есть: - `ConfigureDDCs(NumDDCs, ...)` уже включает N DDC с per-DDC конфигом/ADC-source (`HPSDRNetwork.pas:220`, `DDCConfig[ddc*6+0]=ADCSource`). - Приём разведён по DDC: порты `1035+n` → `HandleDDCIQ(Buf,Len,DDCIdx)` (`HPSDRNetwork.pas:392`). - High Priority уже держит частоту на КАЖДЫЙ DDC (bytes 9-12=DDC0, 13-16=DDC1, …) `HPSDRProtocol.pas:271`. Чего не хватает: 1. Per-DDC частота: сейчас `SetRunAndFreq(...; DDC0FreqHz; ...)` знает только DDC0 (`HPSDRNetwork.pas:224`) — писать частотное слово каждого DDC в High Priority. 2. Роутинг `HandleDDCIQ(DDCIdx)` → соответствующий WDSP-канал слайса. 3. Учёт DDCBase=2 (PureSignal) и Ethernet-бюджета (реально 4–8 слайсов, не 80). `feature/rx2-support` — это про ВТОРОЙ ADC, не про слайсы; но её каркас (доп. WDSP-канал + доп. аудиовыход + роутинг по индексу) частично переиспользуем. ## Оговорки / риски - CPU софтовых слайсов ~линейно от их числа: каждый WDSP-канал жуёт полный rate и децимирует сам (аппаратный DDC дешевле — децимирует железо). Для QO-100 span мал, несколько слайсов — норма. - Не портить входной IQ-буфер между каналами (fexchange0 пишет in-place). - Pluto: слайсы только в пределах захваченной полосы; выход за край = ретюн LO (двигает всё, при QO-100 LO залочен на маяк — не трогать). - Пороговая аккуратность: старт/стоп WDSP-каналов на лету (см. заметку про задержки RXA стоп/старт в `WDSPEngine.pas:~230`). ## Связанные заметки памяти - project_pluto_integration, project_qo100_zoom_panel, project_qo100_decoder_lock - project_spectrum_zoom (GetViewWindow / оверлеи / маппинг мыши) - project_channels_feature (dropdown/форма-редактор как паттерн UI)