16 KiB
Мультислайсы (несколько приёмных фильтров, как во FlexRadio)
Статус: Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ (ветка feature/multislice,
коммит 1527b0f; проверено на эфире). Этап 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-метр), маркеры полосы фильтра слайса в обоих рендерерах (GLDrawSliceFilterMarkers+ CPU), drag-ретюн несущей, колесо над флагом, позиционирование СБОКУ от полосы фильтра с флипом (как у главного флага). Ctrl+ЛКМ по спектру = добавить слайс. Флаг главного (A) виден когда скрыта левая панель ЛИБО есть слайсы B+ (синхронизируется с правками с левой панели). - Перф — S-метр развязан с кэшем «хрома» флага (
FMeterDirty+ meter-onlyRefreshMeterчерез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 залочен на маяк.
Единая абстракция
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.
Чего не хватает:
- Per-DDC частота: сейчас
SetRunAndFreq(...; DDC0FreqHz; ...)знает только DDC0 (HPSDRNetwork.pas:224) — писать частотное слово каждого DDC в High Priority. - Роутинг
HandleDDCIQ(DDCIdx)→ соответствующий WDSP-канал слайса. - Учёт 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)