feat(slices): независимый rate панадаптеров (селектор в шапке)

- Создание пана: rate наследуется от главного с клампом ≤768к
  (вне сетки → 384к); за сменой rate главного паны не следуют
- Клик по «NNN kHz» в шапке пана N → меню полной сетки железа
  (PanRateAllowed = Caps.RatePresets ∩ кратные 48 кГц; захардкоженный
  PAN_DDC_RATES удалён)
- WDSPEngine.SetPanRate: под FSliceLock пересбор аккумулятора/анализатора
  пана + переоткрытие слайс-каналов на новом rate (state сохраняется)
- RadioController.SetPanDDCRate: движок → реконфиг DDC (SetPanDDC) →
  кламп слайсов к новой полосе; FPanADC[] хранится (база под 3.4)
- Шапка: rate отдельным лейблом (у панов N кликабелен, акцент/рука)

Смоук-тест без железа ALL PASS (тон: спектр+аудио слайса живы через
384→192→768). Проверено юзером на экране.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 23:28:25 +03:00
co-authored by Claude Fable 5
parent 95b4f749be
commit e5921b8196
5 changed files with 264 additions and 23 deletions
+38
View File
@@ -509,6 +509,9 @@ type
function AddPan(PanId, RateHz: Integer): Boolean;
// Убирает пан вместе с его слайсами (аудио-выходы слайсов — забота вызывающего).
procedure RemovePan(PanId: Integer);
// Смена rate живого пана: пересбор аккумулятора/анализатора + переоткрытие
// WDSP-каналов его слайсов на новом входном rate (state слайсов сохраняется).
function SetPanRate(PanId, RateHz: Integer): Boolean;
function PanActive(PanId: Integer): Boolean;
// --- Мультислайсы (софт-fan-out) ---
@@ -2220,6 +2223,41 @@ begin
end;
end;
function TWDSPEngine.SetPanRate(PanId, RateHz: Integer): Boolean;
var s: Integer;
begin
Result := False;
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
// Те же требования кратности, что в AddPan.
if (RateHz < FAudioRate) or (RateHz mod FAudioRate <> 0) then Exit;
FSliceLock.Enter;
try
if not FPans[PanId].Active then Exit;
if FPans[PanId].Rate = RateHz then Exit(True);
// Анализатор держит старый rate — снести до пересбора аккумулятора.
DestroyPanAnalyzer(PanId);
FPans[PanId].Rate := RateHz;
FPans[PanId].BufSize := FAudioBufSize * (RateHz div FAudioRate);
SetLength(FPans[PanId].AccI, FPans[PanId].BufSize);
SetLength(FPans[PanId].AccQ, FPans[PanId].BufSize);
FPans[PanId].AccPos := 0;
FPans[PanId].DispPos := 0;
AllocSliceBuffers; // FSliceIn/Out под новый макс. блок
if FAnalyzerOpen then CreatePanAnalyzer(PanId);
// Слайсы пана: их каналы открыты с InRate/InSize пана — переоткрываем
// (OpenSliceChannel восстанавливает mode/filter/AGC/DSP из записи слайса).
for s := 0 to MAX_SLICES - 1 do
if FSlices[s].Active and (FSlices[s].PanId = PanId) then
begin
CloseSliceChannel(s);
OpenSliceChannel(s);
end;
Result := True;
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.PanActive(PanId: Integer): Boolean;
begin
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;