This commit is contained in:
2026-03-08 22:34:18 +03:00
parent 811c96b36c
commit d347a26d6d
5 changed files with 519 additions and 546 deletions
+406 -376
View File
File diff suppressed because it is too large Load Diff
+48 -81
View File
@@ -145,6 +145,7 @@ type
FANFEnabled: Boolean; FANFEnabled: Boolean;
FMuted: Boolean; FMuted: Boolean;
FVolume: Double; FVolume: Double;
FLastError: string;
function ModeToWDSP(Mode: Integer): Integer; function ModeToWDSP(Mode: Integer): Integer;
procedure ApplyDefaultFilter; procedure ApplyDefaultFilter;
@@ -211,6 +212,7 @@ type
property Mode: Integer read FMode; property Mode: Integer read FMode;
property FilterLow: Integer read FFilterLow; property FilterLow: Integer read FFilterLow;
property FilterHigh: Integer read FFilterHigh; property FilterHigh: Integer read FFilterHigh;
property LastError: string read FLastError;
property SMeter: Double read FSMeter; property SMeter: Double read FSMeter;
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio; property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
@@ -356,62 +358,24 @@ var
begin begin
if FAnalyzerOpen then Exit; if FAnalyzerOpen then Exit;
// ---- Шаг 1: создать анализатор ----
// success = 0 означает УСПЕХ (WDSP Guide p.119)
// m_size = максимальный FFT размер (должен быть степенью 2, ≤ 262144)
// ОПТИМИЗАЦИЯ: 262144 → 16384. WDSP аллоцирует и инициализирует twiddle-таблицы
// для ВСЕХ степеней 2 вплоть до m_size при каждом вызове XCreateAnalyzer.
// Реальный рабочий FFT = 4096, запас ×4 = 16384 достаточен для будущих апгрейдов.
// Эффект: время открытия WDSP ~10 сек → ~1 сек.
// m_LO = 1 (нет spur elimination)
// m_stitch = 1 (один sub-span)
Success := -1; Success := -1;
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil); XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
if Success <> 0 then if Success <> 0 then Exit;
begin
// Анализатор не создан — работаем без спектра, краша не будет
Exit;
end;
// ---- Шаг 2: SetAnalyzer ---- FRAME_RATE := 20;
// Рассчитываем параметры по документации (WDSP Guide p.120-121) // max_w: WDSP аллоцирует ring-buffer и FFT планы на max_w сэмплов.
FRAME_RATE := 20; // 20 fps // Формула: fft_size + min(0.1*samplerate, 0.1*fft_size*fps)
// max_w = fft_size + min(KEEP_TIME*sample_rate, KEEP_TIME*fft_size*frame_rate) // = 4096 + min(19200, 8192) = 12288 → 1700ms инициализации!
// KEEP_TIME = 0.1 // Реально нам нужен только один FFT-кадр без усреднения по времени.
// min(0.1*192000, 0.1*4096*20) = min(19200, 8192) = 8192 // MaxW = sz = 4096 — минимально допустимое значение (должно быть ≥ sz).
MaxW := 4096 + 8192; // = 12288 // piHPSDR использует именно sz без добавки keep_time.
MaxW := 4096;
// ovrlp = max(0, ceil(fft_size - sample_rate/frame_rate))
// = max(0, ceil(4096 - 192000/20)) = max(0, 4096-9600) = 0
Ovrlp := 0; Ovrlp := 0;
FFlp[0] := 0; // LO на нижней стороне, нет spur elimination FFlp[0] := 0;
SetAnalyzer( SetAnalyzer(DISP_ID, 1, 1, 1, @FFlp[0], 4096, FAudioBufSize, 1, 14.0,
DISP_ID, Ovrlp, 0, 0.0, 0.0, SPECTRUM_PIXELS, 1, 0, 0.0, 0.0, MaxW);
1, // n_pixout: 1 (один выход — панадаптер и водопад одинаковые)
1, // n_fft: 1 (без spur elimination)
1, // typ: 1 = COMPLEX (I+Q)
@FFlp[0], // flp: [0] — LO ниже сигнала
4096, // sz: размер FFT
FAudioBufSize, // bf_sz: блок данных @ AudioRate = dsp_size (1024 @ 48kHz)
1, // win_type: 1 = 4-term Blackman-Harris
14.0, // pi: Kaiser beta (не используется при BH)
Ovrlp, // ovrlp: перекрытие = 0
0, // clp: обрезка краёв = 0
0.0, // fscLin: pan left = 0 (полный диапазон)
0.0, // fscHin: pan right = 0 (полный диапазон)
SPECTRUM_PIXELS, // n_pix: пикселей на выходе GetPixels
1, // n_stch: 1 sub-span (нет stitching)
0, // calset: нет калибровки
0.0, // fmin
0.0, // fmax
MaxW // max_w: рассчитан выше = 12288
);
// ---- Шаг 3: настройка детектора и усреднения ----
// DETECTOR_MODE_AVERAGE + LOG_RECURSIVE — плавный, как в piHPSDR/Thetis
// backmult ~0.45 = инерция ~2.2 кадра → плавно, без смазывания быстрых сигналов
// NumAverage = FRAME_RATE * 0.12 ≈ 2..3 при 20fps — достаточно для LOG_RECURSIVE
SetDisplayDetectorMode(DISP_ID, 0, DETECTOR_MODE_AVERAGE); SetDisplayDetectorMode(DISP_ID, 0, DETECTOR_MODE_AVERAGE);
SetDisplayAverageMode(DISP_ID, 0, AVERAGE_MODE_LOG_RECURSIVE); SetDisplayAverageMode(DISP_ID, 0, AVERAGE_MODE_LOG_RECURSIVE);
SetDisplayNumAverage(DISP_ID, 0, Max(2, Trunc(FRAME_RATE * 0.12))); SetDisplayNumAverage(DISP_ID, 0, Max(2, Trunc(FRAME_RATE * 0.12)));
@@ -434,66 +398,65 @@ end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function TWDSPEngine.Open: Boolean; function TWDSPEngine.Open: Boolean;
var
DllPath: string;
begin begin
Result := False; Result := False;
FLastError := '';
if FInitialized then Exit; if FInitialized then Exit;
// Проверяем что libwdsp загружена (WEAKEXTERNALSYMBOLS: nil если нет DLL)
if not Assigned(@OpenChannel) then if not Assigned(@OpenChannel) then
begin begin
// wdsp.dll / libwdsp.so не найдена — демо-режим
FInitialized := False; FInitialized := False;
DllPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + WDSP_LIB;
if FileExists(DllPath) then
FLastError := Format('Symbols from %s are not resolved. Check architecture (x64/x86) and dependencies.', [DllPath])
else
FLastError := Format('WDSP library not found: %s', [DllPath]);
Exit; Exit;
end; end;
try try
// ОПТИМИЗАЦИЯ: Анализатор создаём ПЕРВЫМ — до OpenChannel. // Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
// Это соответствует порядку piHPSDR и гарантирует что DISP_ID готов
// до того как DSP поток начнёт вызывать Spectrum0.
OpenAnalyzer; OpenAnalyzer;
// НЕ используем SetRXASpectrum — кормим анализатор вручную if not FAnalyzerOpen then
// через Spectrum0 с Double буфером, чтобы избежать SIGSEGV begin
// (fexchange0 работает с Single, а внутри WDSP Spectrum2 ждёт Double) FLastError := 'XCreateAnalyzer failed (WDSP init failed).';
Exit;
end;
// ---- RXA channel 0 ----
// ОПТИМИЗАЦИЯ: state=0 (не запускать сразу) — настраиваем все параметры
// вхолостую, только потом стартуем через SetChannelState.
// Канал не тратит CPU на обработку "мусорных" данных в процессе настройки.
OpenChannel( OpenChannel(
RXA_CHAN, RXA_CHAN,
FBufSize, // in_size @ FSampleRate (4096 @ 192kHz) FBufSize,
FAudioBufSize, // dsp_size @ AudioRate (1024 @ 48kHz) — как piHPSDR buffer_size FAudioBufSize,
FSampleRate, FSampleRate,
FAudioRate, // dsp_rate = 48kHz FAudioRate,
FAudioRate, // out_rate = 48kHz FAudioRate,
0, // atype: 0=RX 0,
0, // state: 0=hold (запустим после настройки всех параметров) 0, // state=0: создать но не запускать
0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010,
0 0
); );
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode)); SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
SetRXAShiftRun(RXA_CHAN, 1); // включаем NCO шифтер (нужен для CTUN) SetRXAShiftRun(RXA_CHAN, 1);
SetRXAShiftFreq(RXA_CHAN, 0.0); // по умолчанию сдвига нет SetRXAShiftFreq(RXA_CHAN, 0.0);
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh); RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
SetAGC(FAGCMode, 50.0); // настраиваем AGC с полными параметрами piHPSDR SetAGC(FAGCMode, 50.0);
SetRXAPanelGain1(RXA_CHAN, FVolume); SetRXAPanelGain1(RXA_CHAN, FVolume);
SetRXAPanelSelect(RXA_CHAN, 3); SetRXAPanelSelect(RXA_CHAN, 3);
SetRXAPanelRun(RXA_CHAN, 1); SetRXAPanelRun(RXA_CHAN, 1);
SetChannelState(RXA_CHAN, 1, 0);
// Запускаем RXA канал только после полной настройки
SetChannelState(RXA_CHAN, 1, 0); // run, no delay-wait
// ---- TXA channel 1 ----
OpenChannel( OpenChannel(
TXA_CHAN, TXA_CHAN,
FBufSize, FAudioBufSize,
FBufSize, FAudioBufSize,
FAudioRate, FAudioRate,
48000, FAudioRate,
FSampleRate, FAudioRate,
1, // atype: 1=TX 1,
0, // state: 0=hold 0,
0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010,
0 0
); );
@@ -519,9 +482,13 @@ begin
FDSPThread.Priority := tpHighest; // DSP критический поток FDSPThread.Priority := tpHighest; // DSP критический поток
except except
on E: Exception do
begin
FLastError := E.ClassName + ': ' + E.Message;
FInitialized := False; FInitialized := False;
Result := False; Result := False;
end; end;
end;
end; end;
procedure TWDSPEngine.Close; procedure TWDSPEngine.Close;
+5 -8
View File
@@ -1046,13 +1046,13 @@ var
i, n, Enc: Integer; i, n, Enc: Integer;
Pkt: array of Byte; Pkt: array of Byte;
begin begin
if not FRunning then begin AudioLog('EXIT: FRunning=false'); Exit; end; if not FRunning then Exit;
FClientLock.Enter; FClientLock.Enter;
HasWs := FClientCount > 0; HasWs := FClientCount > 0;
FClientLock.Leave; FClientLock.Leave;
if not HasWs then begin AudioLog('EXIT: FClientCount=0'); Exit; end; if not HasWs then Exit;
if (Samples = nil) or (Count <= 0) then begin AudioLog('EXIT: Samples nil/Count=0'); Exit; end; if (Samples = nil) or (Count <= 0) then Exit;
if not FOpusReady then begin AudioLog('EXIT: FOpusReady=false'); Exit; end; if not FOpusReady then Exit;
i := 0; i := 0;
while i < Count do while i < Count do
@@ -1073,10 +1073,7 @@ begin
Pkt[0] := WS_MSG_AUDIO; Pkt[0] := WS_MSG_AUDIO;
Move(FOpusOut[0], Pkt[1], Enc); Move(FOpusOut[0], Pkt[1], Enc);
BroadcastBinary(Pkt[0], Length(Pkt)); BroadcastBinary(Pkt[0], Length(Pkt));
AudioLog('SENT opus bytes=' + IntToStr(Enc) + ' clients=' + IntToStr(FClientCount)); end;
end
else
AudioLog('ENCODE FAILED Enc=' + IntToStr(Enc));
FOpusBufPos := 0; FOpusBufPos := 0;
end; end;
end; end;
-21
View File
@@ -6,7 +6,6 @@ unit WebUtils;
- SHA-1 (минимальная реализация для WebSocket handshake) - SHA-1 (минимальная реализация для WebSocket handshake)
- Base64 (кодирование) - Base64 (кодирование)
- JSON helpers (JsonGetStr, JsonGetFloat, JsonGetInt, JsonGetBool) - JSON helpers (JsonGetStr, JsonGetFloat, JsonGetInt, JsonGetBool)
- AudioLog (отладочное логирование аудио)
ИСПРАВЛЕНИЯ: ИСПРАВЛЕНИЯ:
- (Windows build fix) Порядок uses: стандартные RTL-юниты первыми, - (Windows build fix) Порядок uses: стандартные RTL-юниты первыми,
@@ -76,10 +75,6 @@ function JsonGetFloat(const Json, Key: string; Def: Double): Double;
function JsonGetInt(const Json, Key: string; Def: Integer): Integer; function JsonGetInt(const Json, Key: string; Def: Integer): Integer;
function JsonGetBool(const Json, Key: string; Def: Boolean): Boolean; function JsonGetBool(const Json, Key: string; Def: Boolean): Boolean;
{ ── Отладочное логирование аудио ─────────────────────────────────────────── }
procedure AudioLog(const S: string);
implementation implementation
{ ═══════════════════════════════════════════════════════════════════════════ { ═══════════════════════════════════════════════════════════════════════════
@@ -318,20 +313,4 @@ begin
else Result := (S = 'true') or (S = '1'); else Result := (S = 'true') or (S = '1');
end; end;
{ ═══════════════════════════════════════════════════════════════════════════
Отладочное логирование аудио
═══════════════════════════════════════════════════════════════════════════ }
procedure AudioLog(const S: string);
var F: TextFile;
begin
try
AssignFile(F, 'audio_debug.log');
if FileExists('audio_debug.log') then Append(F) else Rewrite(F);
WriteLn(F, FormatDateTime('hh:nn:ss.zzz', Now) + ' ' + S);
CloseFile(F);
except
end;
end;
end. end.
+3 -3
View File
@@ -285,7 +285,7 @@
"span_hz" : 1.9200000000000000E+005 "span_hz" : 1.9200000000000000E+005
}, },
"1" : { "1" : {
"vfo_a" : 3.6210000000000000E+006, "vfo_a" : 3.6870000000000000E+006,
"vfo_b" : 3.7500000000000000E+006, "vfo_b" : 3.7500000000000000E+006,
"mode" : 0, "mode" : 0,
"filter_idx" : 5, "filter_idx" : 5,
@@ -307,7 +307,7 @@
"span_hz" : 1.9200000000000000E+005 "span_hz" : 1.9200000000000000E+005
}, },
"3" : { "3" : {
"vfo_a" : 7.1620000000000000E+006, "vfo_a" : 7.1410000000000000E+006,
"vfo_b" : 7.1000000000000000E+006, "vfo_b" : 7.1000000000000000E+006,
"mode" : 0, "mode" : 0,
"filter_idx" : 5, "filter_idx" : 5,
@@ -329,7 +329,7 @@
"span_hz" : 1.9200000000000000E+005 "span_hz" : 1.9200000000000000E+005
}, },
"5" : { "5" : {
"vfo_a" : 1.4163000000000000E+007, "vfo_a" : 1.4189000000000000E+007,
"vfo_b" : 1.4200000000000000E+007, "vfo_b" : 1.4200000000000000E+007,
"mode" : 1, "mode" : 1,
"filter_idx" : 5, "filter_idx" : 5,