Files
ewsdr/Settings.pas
T
ew8bakandClaude Opus 4.7 96d7e0c759 TX: фиксированный sample rate, per-device grid, CTUN-aware overlay, alpha-blend filter bands
Декаплинг TX от RX sample rate (как в Thetis):
- WDSPEngine.pas: константа TX_SAMPLE_RATE=192000, поля FTXSampleRate/FTXOutBufSize.
  TXA OpenChannel теперь использует FTXSampleRate как out_rate, TX-анализатор
  через SetDisplaySampleRate(TX_DISP_ID, FTXSampleRate). ProcessTXBlock работает
  с FTXOutBufSize вместо FBufSize. Public TXSampleRate для MainForm.
- MainForm.SendDUCSpecificFromSettings: DUC0Rate теперь берётся из
  FDSPEngine.TXSampleRate, а не из RX-настроек — TX всегда 192k независимо от
  выбранного RX-rate (48/96/192/384k).

DUC Specific cache + keepalive resend (HPSDRNetwork.pas):
- FCachedDUCSpec/FCachedDUCValid параллельно DDC-кэшу.
- KeepAlive thread в первые 5s после Run=1 переотправляет и DDC, и DUC
  Specific — устраняет гонку, когда DUC настройки терялись железом.
- Disconnect сбрасывает оба valid-флага.

Per-device TX grid settings (Thetis-style):
- Settings.pas: TXSpecRefLevel/TXSpecRange/TXSpecGridStep в TTXSettings
  (defaults 0.0/80.0/10.0). LoadTX/SaveTX — ключи tx_spec_ref_level/range/grid_step.
- SettingsForm.pas: новая группа \"TX Grid\" во вкладке Transmit
  (RefLevel edit, Range edit, GridStep combo). CollectTXFromUI читает значения,
  LoadTXSettings заполняет UI.
- MainForm.pas: поля FTXSpecRefLevel/Range/GridStep, ApplySpecViewGridFromState
  пушит в FSpecView либо TX, либо RX параметры в зависимости от FTransmitting.
  ApplyGridParams (RX-путь) уважает FTransmitting и не перебивает TX-grid.
  OnTXSettingsChange при активном TX моментально применяет новый grid.

TX-aware spectrum/waterfall с CTUN-выравниванием (SpectrumView.pas):
- Поля FTXMode/FTXFreq/FTXSpanHz/FTXVfoIndex. TXSpanHz=192000 при создании.
- DrawSpectrum/DrawWaterfall в FTXMode мапят данные TX-анализатора (192k вокруг
  FTXFreq) в RX-координаты FCenterFreq/FSpanHz. Формула:
    freq = FCenterFreq - FSpanHz/2 + x*FSpanHz/W
    idx  = (freq-FTXFreq + TXSpanHz/2)/TXSpanHz * (SrcCount-1)
  Вне диапазона — спектр уходит в -200 dB; водопад копирует пиксель из
  предыдущей строки (FWfPixels[X+W]) — вместо чёрной полосы продолжает
  скроллиться старая RX-картинка.
- Эффект: при CTUN ON TX-сигнал рисуется на полосе фильтра (на VFO), а не в
  центре. При RX rate ≠ 192k частотная сетка совпадает между RX и TX
  автоматически (ruler всегда в RX-координатах).
- MainForm.SyncSpecViewFreq/ApplyMOX обновляют TXFreq/TXVfoIndex/TXMode при
  каждом PTT и смене VFO; учитывается split (FSplitTxB → TXVfoIndex=1).

Alpha-blended filter bands (визуальный feedback TX/RX):
- SpectrumView: PixelFormat:=pf32bit, процедура BlendBand через ScanLine[Y]
  внутри BeginUpdate/EndUpdate. Альфа-смешение по формуле
  (a*src + (255-a)*dst) / 255 для каждого канала BGR.
- В FTXMode полоса фильтра становится полупрозрачно красной. Если split
  (FTXVfoIndex<>FActiveVfo): на RX-частоте полоса зелёная, на TX-частоте —
  красная (CalcFilterBandX вычисляет границы для произвольной VFO-freq).
- При FTXMode=False — старое поведение (сплошная заливка SpecFilter).

Build: lazbuild --ws=qt6 ewsdr.lpi — clean, 0 errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 14:17:49 +03:00

911 lines
35 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit Settings;
{
Settings.pas — JSON-конфигурация HPSDR SDR приложения.
Структура hpsdr_settings.json:
{ "AA:BB:CC:DD:EE:FF": { "global": {...}, "bands": {"5": {...}} } }
}
{$mode objfpc}{$H+}
interface
uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner;
const
SETTINGS_FILE = 'hpsdr_settings.json';
CFG_BAND_COUNT = 11;
CFG_BAND_DEFAULT_FREQ: array[0..CFG_BAND_COUNT-1] of Double = (
1900000, 3750000, 5357000, 7100000, 10125000,
14200000, 18120000, 21200000, 24940000, 28500000, 50150000);
CFG_BAND_DEFAULT_MODE: array[0..CFG_BAND_COUNT-1] of Integer = (
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1);
type
TBandSettings = record
VfoA: Double;
VfoB: Double;
Mode: Integer;
FilterIdx: Integer;
FilterBW: Integer;
AGCMode: Integer;
AGCTop: Integer;
CTun: Boolean;
SpanHz: Double;
end;
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
// (Тип TTXMicSource живёт в WDSPEngine — здесь храним как Integer,
// чтобы не тащить циклическую зависимость с WDSPEngine.)
TTXSettings = record
// Mic source select
MicSource: Integer; // 0=Radio, 1=SoundCard
// Radio mic options (DUC Specific byte 50 bits)
MicBoost: Boolean; // bit0: +20 dB
MicBias: Boolean; // bit4: электретный bias
MicLineIn: Boolean; // bit1: 1=Line in, 0=Mic in
MicPTTEnabled: Boolean; // bit2: Mic PTT enable
// Mic gain (dB, применяется в WDSP PanelGain1)
MicGainDB: Double; // -10..+30
// TX bandpass filter
FilterLow: Integer; // Hz, для SSB обычно 100
FilterHigh: Integer; // Hz, для SSB обычно 2800
FilterNC: Integer; // FIR length (1024/2048/4096)
FilterMP: Integer; // 0=Linear Phase, 1=Low Latency
FilterWindow: Integer; // 0=Rect..6=BH7
// Compressor
CompressorOn: Boolean;
CompressorGain: Double; // 0..20 dB
// Leveler (TX AGC)
LevelerOn: Boolean;
LevelerTop: Double; // 0..10 dB
LevelerDecay: Integer; // ms
// ALC (Auto Level Control)
ALCOn: Boolean;
ALCMaxGain: Double; // -10..+10 dB
ALCDecay: Integer; // ms
// Phase Rotator
PhaseRotOn: Boolean;
PhaseRotStages: Integer; // 4/6/8
PhaseRotFreq: Double; // Hz, обычно 338
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
EQOn: Boolean;
EQNumBands: Integer; // 3 или 10
EQGains: array[0..10] of Double; // dB, ±15
EQFreqs: array[0..10] of Double; // Hz
// AM modulator
AMCarrierLevel: Double; // 0..1
// FM modulator
FMDeviation: Double; // Hz, 2500..5000
FMLowCut: Integer; // AF filter low
FMHighCut: Integer; // AF filter high
FMEmphPosition: Integer; // 0=PreEQ, 1=PostEQ
// CTCSS
CTCSSOn: Boolean;
CTCSSFreq: Double; // Hz
// TX Display (отдельный analyzer для TX-спектра/водопада)
TXFFTSize: Integer; // 4096..262144
TXWindowType: Integer; // 0..6
TXSpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
TXSpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
TXSpecAvgTimeMS: Double;
TXWfDetector: Integer;
TXWfAvgMode: Integer;
TXWfAvgTimeMS: Double;
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах)
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
TXSpecRange: Double; // dB range, e.g. 80
TXSpecGridStep: Double; // grid step in dB, e.g. 10
end;
TGlobalSettings = record
Volume: Integer;
DriveLevel: Integer;
ActiveVfo: Integer;
NRMode: Integer;
NBMode: Integer;
SNBEnabled: Boolean;
ANFEnabled: Boolean;
AGCSlope: Integer;
AGCHangThreshold: Integer;
WfAGCEnabled: Boolean;
WfNFEnabled: Boolean;
LastBand: Integer;
SampleRate: Integer; // глобальный — один для всех диапазонов
WindowLeft: Integer;
WindowTop: Integer;
WindowWidth: Integer;
WindowHeight: Integer;
// --- Display settings ---
FFTSize: Integer; // 4096..262144
WindowType: Integer; // 0=Rect,1=BH4,2=Hann,3=FlatTop,4=Hamming,5=Kaiser,6=BH7
SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
SpecAvgTimeMS: Double; // averaging time in milliseconds
WfDetector: Integer; // waterfall detector
WfAvgMode: Integer; // waterfall average mode
WfAvgTimeMS: Double; // waterfall averaging time in milliseconds
WfManualHigh: Double; // manual waterfall high threshold
WfManualLow: Double; // manual waterfall low threshold
WfAGCOffset: Double; // AGC offset in dB
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
SpecGridStep: Double; // grid step in dB, e.g. 10
// --- Audio settings ---
AudioOutDevice: string; // PortAudio output device name ('' = default)
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
AudioInDevice: string; // PortAudio input device name for TX mic ('' = none)
// --- Visibility settings ---
ShowSpectrum: Boolean; // True = draw spectrum on main form
ShowWaterfall: Boolean; // True = draw waterfall on main form
// --- Display FPS ---
DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
// --- Theme ---
LightTheme: Boolean; // False = dark (default), True = light
// --- PA (Power Amplifier) settings ---
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
// --- CAT (Computer Aided Transceiver) settings ---
// До 4 последовательных портов (Linux: /dev/ttyS0, Windows: COM1 и т.д.)
CATSerialEnabled: array[0..3] of Boolean;
CATSerialPort: array[0..3] of string; // имя порта ОС
CATSerialBaud: array[0..3] of Integer; // скорость: 1200..115200
CATSerialDataBits: array[0..3] of Integer; // 7 или 8
CATSerialStopBits: array[0..3] of Integer; // 1 или 2
CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even
// TCP CAT сервер (один, многоклиентский)
CATTcpEnabled: Boolean;
CATTcpPort: Integer; // порт, по умолчанию 19090
end;
TSettingsManager = class
private
FFilePath: string;
FRoot: TJSONObject;
function EnsureObj(P: TJSONObject; const K: string): TJSONObject;
function GetDevObj(const M: string): TJSONObject;
function GetGlobalObj(D: TJSONObject): TJSONObject;
function GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject;
function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
function JD(O: TJSONObject; const K: string; Def: Double): Double;
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
function JS(O: TJSONObject; const K: string; const Def: string): string;
procedure JW(O: TJSONObject; const K: string; V: Integer); overload;
procedure JW(O: TJSONObject; const K: string; V: Double); overload;
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
procedure JWS(O: TJSONObject; const K: string; const V: string);
public
constructor Create(const FilePath: string = SETTINGS_FILE);
destructor Destroy; override;
procedure Load;
procedure Save;
function LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean;
procedure SaveGlobal(const MAC: array of Byte; const G: TGlobalSettings);
procedure SaveBand(const MAC: array of Byte; BandIdx: Integer;
const B: TBandSettings);
function LoadBand(const MAC: array of Byte; BandIdx: Integer;
out B: TBandSettings): Boolean;
// TX settings per-device (JSON-секция "tx" под MAC).
// LoadTX возвращает True если устройство уже зарегистрировано в JSON;
// в любом случае T заполняется (дефолтами или сохранёнными значениями).
function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean;
procedure SaveTX(const MAC: array of Byte; const T: TTXSettings);
class function MacToStr(const MAC: array of Byte): string;
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
class procedure DefaultGlobal(out G: TGlobalSettings);
class procedure DefaultTX(out T: TTXSettings);
// Размер окна — не привязан к MAC, хранится в корне JSON
procedure SaveWindowBounds(L, T, W, H: Integer);
procedure LoadWindowBounds(out L, T, W, H: Integer);
procedure SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);
function LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean;
// CAT settings — не привязаны к MAC, хранятся в корне JSON (доступны до подключения)
procedure SaveCATSettings(const G: TGlobalSettings);
procedure LoadCATSettings(var G: TGlobalSettings);
// Тема — глобальная настройка, не привязана к устройству
procedure SaveTheme(ALightTheme: Boolean);
function LoadTheme: Boolean;
end;
implementation
function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer;
Backmult, DefaultMS: Double): Double;
var
FPSv: Integer;
begin
if AvgMode <= 0 then
Exit(0.0);
FPSv := EnsureRange(FPS, 1, 100);
if (Backmult > 0.0) and (Backmult < 1.0) then
Result := -1000.0 / (FPSv * Ln(Backmult))
else
Result := (1000.0 * Max(1, AvgCount)) / FPSv;
if IsNan(Result) or IsInfinite(Result) or (Result <= 0.0) then
Result := DefaultMS;
Result := EnsureRange(Result, 10.0, 5000.0);
end;
class function TSettingsManager.MacToStr(const MAC: array of Byte): string;
begin
Result := Format('%02X:%02X:%02X:%02X:%02X:%02X',
[MAC[0],MAC[1],MAC[2],MAC[3],MAC[4],MAC[5]]);
end;
class procedure TSettingsManager.DefaultBand(BandIdx: Integer; out B: TBandSettings);
begin
FillChar(B, SizeOf(B), 0);
B.VfoA := CFG_BAND_DEFAULT_FREQ[BandIdx];
B.VfoB := CFG_BAND_DEFAULT_FREQ[BandIdx];
B.Mode := CFG_BAND_DEFAULT_MODE[BandIdx];
B.FilterIdx := 5;
B.FilterBW := 2700;
B.AGCMode := 1;
B.AGCTop := 90;
B.CTun := False;
B.SpanHz := 192000;
end;
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
var i: Integer;
begin
FillChar(G, SizeOf(G), 0);
G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0;
G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False;
G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5;
G.WfAGCEnabled := True;
G.WfNFEnabled := False;
G.SampleRate := 192000;
G.PAMaxPower := 100.0;
for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0;
// Display defaults
G.FFTSize := 131072;
G.WindowType := 2; // Hann
G.SpecDetector := 0; // Peak
G.SpecAvgMode := 3; // Log Recursive
G.SpecAvgTimeMS := 30.0;
G.WfDetector := 0; // Peak
G.WfAvgMode := 3; // Log Recursive
G.WfAvgTimeMS := 120.0;
G.WfManualHigh := -80.0;
G.WfManualLow := -130.0;
G.WfAGCOffset := 0.0;
G.SpecRefLevel := -20.0;
G.SpecRange := 110.0;
G.SpecGridStep := 10.0;
G.AudioOutDevice := '';
G.AudioSampleRate := 48000;
G.AudioInDevice := '';
G.ShowSpectrum := True;
G.ShowWaterfall := True;
G.DisplayFPS := 60;
// CAT defaults
for i := 0 to 3 do begin
G.CATSerialEnabled[i] := False;
{$IFDEF MSWINDOWS}
G.CATSerialPort[i] := 'COM' + IntToStr(i+1);
{$ELSE}
G.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i);
{$ENDIF}
G.CATSerialBaud[i] := 9600;
G.CATSerialDataBits[i] := 8;
G.CATSerialStopBits[i] := 1;
G.CATSerialParity[i] := 0; // None
end;
G.CATTcpEnabled := False;
G.CATTcpPort := 19090;
end;
constructor TSettingsManager.Create(const FilePath: string);
begin
inherited Create;
FFilePath := FilePath;
FRoot := TJSONObject.Create;
end;
destructor TSettingsManager.Destroy;
begin
FRoot.Free;
inherited;
end;
procedure TSettingsManager.Load;
var F: TFileStream; P: TJSONParser; D: TJSONData;
begin
if not FileExists(FFilePath) then Exit;
try
F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone);
try
P := TJSONParser.Create(F, [joUTF8]);
try
D := P.Parse;
if (D <> nil) and (D is TJSONObject) then
begin FRoot.Free; FRoot := TJSONObject(D); end
else
D.Free;
finally P.Free; end;
finally F.Free; end;
except
FreeAndNil(FRoot);
FRoot := TJSONObject.Create;
end;
end;
procedure TSettingsManager.Save;
var
S: string;
F: TFileStream;
Buf: TBytes;
begin
try
S := FRoot.FormatJSON([], 2);
Buf := TEncoding.UTF8.GetBytes(S);
F := TFileStream.Create(FFilePath, fmCreate);
try
if Length(Buf) > 0 then
F.WriteBuffer(Buf[0], Length(Buf));
finally F.Free; end;
except end;
end;
function TSettingsManager.EnsureObj(P: TJSONObject; const K: string): TJSONObject;
var D: TJSONData; Idx: Integer;
begin
D := P.Find(K);
if (D <> nil) and (D is TJSONObject) then
Result := TJSONObject(D)
else
begin
if D <> nil then begin Idx := P.IndexOfName(K); if Idx >= 0 then P.Delete(Idx); end;
Result := TJSONObject.Create;
P.Add(K, Result);
end;
end;
function TSettingsManager.GetDevObj(const M: string): TJSONObject;
begin Result := EnsureObj(FRoot, M); end;
function TSettingsManager.GetGlobalObj(D: TJSONObject): TJSONObject;
begin Result := EnsureObj(D, 'global'); end;
function TSettingsManager.GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject;
begin Result := EnsureObj(EnsureObj(D, 'bands'), IntToStr(Idx)); end;
function TSettingsManager.JI(O: TJSONObject; const K: string; Def: Integer): Integer;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsInteger; except Result:=Def; end else Result:=Def; end;
function TSettingsManager.JD(O: TJSONObject; const K: string; Def: Double): Double;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsFloat; except Result:=Def; end else Result:=Def; end;
function TSettingsManager.JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsBoolean; except Result:=Def; end else Result:=Def; end;
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Integer);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Double);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Boolean);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
function TSettingsManager.JS(O: TJSONObject; const K: string; const Def: string): string;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsString; except Result:=Def; end else Result:=Def; end;
procedure TSettingsManager.JWS(O: TJSONObject; const K: string; const V: string);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
function TSettingsManager.LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean;
var MacStr: string; DevObj,GObj,BObj: TJSONObject; i: Integer;
begin
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
DevObj := GetDevObj(MacStr);
GObj := GetGlobalObj(DevObj);
G.Volume := JI(GObj,'volume',70);
G.DriveLevel := JI(GObj,'drive_level',50);
G.ActiveVfo := JI(GObj,'active_vfo',0);
G.NRMode := JI(GObj,'nr_mode',Ord(JB(GObj,'nr_enabled',False)));
G.NBMode := JI(GObj,'nb_mode',Ord(JB(GObj,'nb_enabled',False)));
G.SNBEnabled := JB(GObj,'snb_enabled',False);
G.ANFEnabled := JB(GObj,'anf_enabled',False);
G.AGCSlope := JI(GObj,'agc_slope',0);
G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100);
G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',True);
G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False);
G.LastBand := JI(GObj,'last_band',5);
G.SampleRate := JI(GObj,'sample_rate',192000);
// Display settings
G.FFTSize := JI(GObj,'fft_size',131072);
G.WindowType := JI(GObj,'win_type',2);
G.SpecDetector := JI(GObj,'spec_detector',0);
G.SpecAvgMode := JI(GObj,'spec_avg_mode',3);
if GObj.Find('spec_avg_time_ms') <> nil then
G.SpecAvgTimeMS := JD(GObj,'spec_avg_time_ms',30.0)
else if (GObj.Find('spec_avg_count') <> nil) or
(GObj.Find('spec_backmult') <> nil) then
begin
G.SpecAvgTimeMS := LegacyAverageTimeMS(
JI(GObj,'display_fps',60),
G.SpecAvgMode,
JI(GObj,'spec_avg_count',1),
JD(GObj,'spec_backmult',0.0),
30.0);
end
else
G.SpecAvgTimeMS := 30.0;
G.WfDetector := JI(GObj,'wf_detector',0);
G.WfAvgMode := JI(GObj,'wf_avg_mode',3);
if GObj.Find('wf_avg_time_ms') <> nil then
G.WfAvgTimeMS := JD(GObj,'wf_avg_time_ms',120.0)
else if (GObj.Find('wf_avg_count') <> nil) or
(GObj.Find('wf_backmult') <> nil) then
begin
G.WfAvgTimeMS := LegacyAverageTimeMS(
JI(GObj,'display_fps',60),
G.WfAvgMode,
JI(GObj,'wf_avg_count',2),
JD(GObj,'wf_backmult',0.25),
120.0);
end
else
G.WfAvgTimeMS := 120.0;
G.WfManualHigh := JD(GObj,'wf_manual_high',-80.0);
G.WfManualLow := JD(GObj,'wf_manual_low',-130.0);
G.WfAGCOffset := JD(GObj,'wf_agc_offset',0.0);
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
G.SpecRange := JD(GObj,'spec_range',110.0);
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
// Audio settings
G.AudioOutDevice := JS(GObj,'audio_out_device','');
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
G.AudioInDevice := JS(GObj,'audio_in_device','');
// Visibility settings
G.ShowSpectrum := JB(GObj,'show_spectrum',True);
G.ShowWaterfall := JB(GObj,'show_waterfall',True);
G.DisplayFPS := JI(GObj,'display_fps',60);
G.LightTheme := JB(GObj,'light_theme',False);
// PA settings
G.PAMaxPower := JD(GObj,'pa_max_power',100.0);
for i := 0 to CFG_BAND_COUNT-1 do
G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i),100.0);
// CAT serial settings
for i := 0 to 3 do begin
G.CATSerialEnabled[i] := JB(GObj,'cat_serial_en_'+IntToStr(i), False);
{$IFDEF MSWINDOWS}
G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), 'COM'+IntToStr(i+1));
{$ELSE}
G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), '/dev/ttyS'+IntToStr(i));
{$ENDIF}
G.CATSerialBaud[i] := JI(GObj,'cat_serial_baud_'+IntToStr(i), 9600);
G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8);
G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1);
G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0);
end;
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
for i := 0 to CFG_BAND_COUNT-1 do
begin
DefaultBand(i, Bands[i]);
BObj := GetBandObj(DevObj, i);
Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA);
Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB);
Bands[i].Mode := JI(BObj,'mode', Bands[i].Mode);
Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx);
Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW);
Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode);
Bands[i].AGCTop := JI(BObj,'agc_top', Bands[i].AGCTop);
Bands[i].CTun := JB(BObj,'ctun', Bands[i].CTun);
Bands[i].SpanHz := JD(BObj,'span_hz', Bands[i].SpanHz);
end;
end;
procedure TSettingsManager.SaveGlobal(const MAC: array of Byte;
const G: TGlobalSettings);
var O: TJSONObject; i: Integer;
begin
O := GetGlobalObj(GetDevObj(MacToStr(MAC)));
JW(O,'volume',G.Volume); JW(O,'drive_level',G.DriveLevel);
JW(O,'active_vfo',G.ActiveVfo);
JW(O,'nr_mode',G.NRMode); JW(O,'nb_mode',G.NBMode);
JW(O,'nr_enabled',G.NRMode <> 0); JW(O,'nb_enabled',G.NBMode <> 0);
JW(O,'snb_enabled',G.SNBEnabled);
JW(O,'anf_enabled',G.ANFEnabled);
JW(O,'agc_slope',G.AGCSlope);
JW(O,'agc_hang_threshold',G.AGCHangThreshold);
JW(O,'wf_agc_enabled',G.WfAGCEnabled);
JW(O,'wf_nf_enabled',G.WfNFEnabled);
JW(O,'last_band',G.LastBand);
JW(O,'sample_rate',G.SampleRate);
// Display settings
JW(O,'fft_size',G.FFTSize);
JW(O,'win_type',G.WindowType);
JW(O,'spec_detector',G.SpecDetector);
JW(O,'spec_avg_mode',G.SpecAvgMode);
JW(O,'spec_avg_time_ms',G.SpecAvgTimeMS);
JW(O,'wf_detector',G.WfDetector);
JW(O,'wf_avg_mode',G.WfAvgMode);
JW(O,'wf_avg_time_ms',G.WfAvgTimeMS);
JW(O,'wf_manual_high',G.WfManualHigh);
JW(O,'wf_manual_low',G.WfManualLow);
JW(O,'wf_agc_offset',G.WfAGCOffset);
JW(O,'spec_ref_level',G.SpecRefLevel);
JW(O,'spec_range',G.SpecRange);
JW(O,'spec_grid_step',G.SpecGridStep);
// Audio settings
JWS(O,'audio_out_device',G.AudioOutDevice);
JW(O,'audio_sample_rate',G.AudioSampleRate);
JWS(O,'audio_in_device',G.AudioInDevice);
// Visibility settings
JW(O,'show_spectrum',G.ShowSpectrum);
JW(O,'show_waterfall',G.ShowWaterfall);
JW(O,'display_fps',G.DisplayFPS);
JW(O,'light_theme',G.LightTheme);
// PA settings
JW(O,'pa_max_power',G.PAMaxPower);
for i := 0 to CFG_BAND_COUNT-1 do
JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]);
// CAT serial settings
for i := 0 to 3 do begin
JW(O,'cat_serial_en_'+IntToStr(i), G.CATSerialEnabled[i]);
JWS(O,'cat_serial_port_'+IntToStr(i),G.CATSerialPort[i]);
JW(O,'cat_serial_baud_'+IntToStr(i), G.CATSerialBaud[i]);
JW(O,'cat_serial_dbits_'+IntToStr(i),G.CATSerialDataBits[i]);
JW(O,'cat_serial_sbits_'+IntToStr(i),G.CATSerialStopBits[i]);
JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]);
end;
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
JW(O,'cat_tcp_port',G.CATTcpPort);
end;
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
const B: TBandSettings);
var O: TJSONObject;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
O := GetBandObj(GetDevObj(MacToStr(MAC)), BandIdx);
JW(O,'vfo_a',B.VfoA); JW(O,'vfo_b',B.VfoB);
JW(O,'mode',B.Mode);
JW(O,'filter_idx',B.FilterIdx); JW(O,'filter_bw',B.FilterBW);
JW(O,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop);
JW(O,'ctun',B.CTun); JW(O,'span_hz',B.SpanHz);
end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
out B: TBandSettings): Boolean;
var MacStr: string; O: TJSONObject;
begin
DefaultBand(BandIdx, B);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
O := GetBandObj(GetDevObj(MacStr), BandIdx);
B.VfoA := JD(O,'vfo_a', B.VfoA);
B.VfoB := JD(O,'vfo_b', B.VfoB);
B.Mode := JI(O,'mode', B.Mode);
B.FilterIdx := JI(O,'filter_idx', B.FilterIdx);
B.FilterBW := JI(O,'filter_bw', B.FilterBW);
B.AGCMode := JI(O,'agc_mode', B.AGCMode);
B.AGCTop := JI(O,'agc_top', B.AGCTop);
B.CTun := JB(O,'ctun', B.CTun);
B.SpanHz := JD(O,'span_hz', B.SpanHz);
end;
procedure TSettingsManager.SaveWindowBounds(L, T, W, H: Integer);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'window');
JW(O, 'left', L);
JW(O, 'top', T);
JW(O, 'width', W);
JW(O, 'height', H);
end;
procedure TSettingsManager.LoadWindowBounds(out L, T, W, H: Integer);
var O: TJSONObject;
begin
L := 80; T := 80; W := 1400; H := 900;
if FRoot.Find('window') = nil then Exit;
O := EnsureObj(FRoot, 'window');
L := JI(O, 'left', 80);
T := JI(O, 'top', 80);
W := JI(O, 'width', 1400);
H := JI(O, 'height', 900);
end;
procedure TSettingsManager.SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'startup');
JW(O, 'vfo_a', VfoA);
JW(O, 'vfo_b', VfoB);
JW(O, 'sample_rate', SampleRate);
end;
function TSettingsManager.LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean;
var O: TJSONObject;
begin
Result := FRoot.Find('startup') <> nil;
if not Result then Exit;
O := EnsureObj(FRoot, 'startup');
VfoA := JD(O, 'vfo_a', 14200000);
VfoB := JD(O, 'vfo_b', 7100000);
SampleRate := JI(O, 'sample_rate', 192000);
end;
procedure TSettingsManager.SaveCATSettings(const G: TGlobalSettings);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(FRoot, 'cat');
JW(O, 'tcp_en', G.CATTcpEnabled);
JW(O, 'tcp_port', G.CATTcpPort);
for i := 0 to 3 do
begin
JW(O, 'ser_en_' +IntToStr(i), G.CATSerialEnabled[i]);
JWS(O, 'ser_port_' +IntToStr(i), G.CATSerialPort[i]);
JW(O, 'ser_baud_' +IntToStr(i), G.CATSerialBaud[i]);
JW(O, 'ser_dbits_' +IntToStr(i), G.CATSerialDataBits[i]);
JW(O, 'ser_sbits_' +IntToStr(i), G.CATSerialStopBits[i]);
JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]);
end;
end;
procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings);
var O: TJSONObject; i: Integer;
begin
if FRoot.Find('cat') = nil then Exit;
O := EnsureObj(FRoot, 'cat');
G.CATTcpEnabled := JB(O, 'tcp_en', False);
G.CATTcpPort := JI(O, 'tcp_port', 19090);
for i := 0 to 3 do
begin
G.CATSerialEnabled[i] := JB(O, 'ser_en_' +IntToStr(i), False);
{$IFDEF MSWINDOWS}
G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), 'COM'+IntToStr(i+1));
{$ELSE}
G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), '/dev/ttyS'+IntToStr(i));
{$ENDIF}
G.CATSerialBaud[i] := JI(O, 'ser_baud_' +IntToStr(i), 9600);
G.CATSerialDataBits[i] := JI(O, 'ser_dbits_' +IntToStr(i), 8);
G.CATSerialStopBits[i] := JI(O, 'ser_sbits_' +IntToStr(i), 1);
G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0);
end;
end;
// ---------------------------------------------------------------------------
// TX settings (per-device, JSON-секция "tx" под MAC)
// ---------------------------------------------------------------------------
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
var
i: Integer;
const
// 10-полосный голосовой EQ (Hz). [0]=preamp slot, [1..10]=bands.
// Подобрано под голос, а не под музыку: больше точек в 100..3500 Hz.
DefaultEQFreqs: array[0..10] of Double = (
0, // [0] preamp slot — частота не используется
100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500);
begin
FillChar(T, SizeOf(T), 0);
// Mic source — Radio (HW). 0=Radio, 1=SoundCard (синхронно с TTXMicSource в WDSPEngine).
T.MicSource := 0;
T.MicBoost := False;
T.MicBias := True;
T.MicLineIn := False;
T.MicPTTEnabled := False;
T.MicGainDB := 10.0;
// SSB-фильтр по умолчанию
T.FilterLow := 100;
T.FilterHigh := 2800;
T.FilterNC := 1024;
T.FilterMP := 1; // Low Latency (как в piHPSDR)
T.FilterWindow := 1; // BH4 / Blackman-Harris
// Compressor — выкл по умолчанию
T.CompressorOn := False;
T.CompressorGain:= 5.0;
// Leveler — вкл, типичные значения piHPSDR/Thetis
T.LevelerOn := True;
T.LevelerTop := 5.0;
T.LevelerDecay := 500;
// ALC — вкл всегда (защита от перегруза DAC)
T.ALCOn := True;
T.ALCMaxGain := 0.0;
T.ALCDecay := 10;
// Phase Rotator — выкл
T.PhaseRotOn := False;
T.PhaseRotStages:= 8;
T.PhaseRotFreq := 338.0;
// EQ — выкл, нулевые гейны
T.EQOn := False;
T.EQNumBands := 10;
for i := 0 to 10 do
begin
T.EQGains[i] := 0.0;
T.EQFreqs[i] := DefaultEQFreqs[i];
end;
// AM
T.AMCarrierLevel := 0.5;
// FM
T.FMDeviation := 5000.0;
T.FMLowCut := 300;
T.FMHighCut := 3000;
T.FMEmphPosition := 1; // PostEQ
// CTCSS
T.CTCSSOn := False;
T.CTCSSFreq := 100.0;
// TX Display defaults — те же что у RX (см. DefaultGlobal)
T.TXFFTSize := 16384; // меньше чем RX — TX-полоса узкая, FFT не нужен большой
T.TXWindowType := 2; // Hann
T.TXSpecDetector := 0; // Peak
T.TXSpecAvgMode := 3; // Log Recursive
T.TXSpecAvgTimeMS := 30.0;
T.TXWfDetector := 0;
T.TXWfAvgMode := 3;
T.TXWfAvgTimeMS := 120.0;
// TX Grid — потолок 0 dBm (TX-уровни выше RX), 80 dB динамики, шаг 10 dB
T.TXSpecRefLevel := 0.0;
T.TXSpecRange := 80.0;
T.TXSpecGridStep := 10.0;
end;
function TSettingsManager.LoadTX(const MAC: array of Byte;
out T: TTXSettings): Boolean;
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
begin
DefaultTX(T);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
// Если секции "tx" ещё нет — оставляем дефолты, не материализуем пустую секцию
if DevObj.Find('tx') = nil then Exit;
O := EnsureObj(DevObj, 'tx');
T.MicSource := JI(O,'mic_source', T.MicSource);
T.MicBoost := JB(O,'mic_boost', T.MicBoost);
T.MicBias := JB(O,'mic_bias', T.MicBias);
T.MicLineIn := JB(O,'mic_line_in', T.MicLineIn);
T.MicPTTEnabled := JB(O,'mic_ptt_enabled', T.MicPTTEnabled);
T.MicGainDB := JD(O,'mic_gain_db', T.MicGainDB);
T.FilterLow := JI(O,'filter_low', T.FilterLow);
T.FilterHigh := JI(O,'filter_high', T.FilterHigh);
T.FilterNC := JI(O,'filter_nc', T.FilterNC);
T.FilterMP := JI(O,'filter_mp', T.FilterMP);
T.FilterWindow := JI(O,'filter_window', T.FilterWindow);
T.CompressorOn := JB(O,'compressor_on', T.CompressorOn);
T.CompressorGain := JD(O,'compressor_gain', T.CompressorGain);
T.LevelerOn := JB(O,'leveler_on', T.LevelerOn);
T.LevelerTop := JD(O,'leveler_top', T.LevelerTop);
T.LevelerDecay := JI(O,'leveler_decay', T.LevelerDecay);
T.ALCOn := JB(O,'alc_on', T.ALCOn);
T.ALCMaxGain := JD(O,'alc_max_gain', T.ALCMaxGain);
T.ALCDecay := JI(O,'alc_decay', T.ALCDecay);
T.PhaseRotOn := JB(O,'phase_rot_on', T.PhaseRotOn);
T.PhaseRotStages := JI(O,'phase_rot_stages',T.PhaseRotStages);
T.PhaseRotFreq := JD(O,'phase_rot_freq', T.PhaseRotFreq);
T.EQOn := JB(O,'eq_on', T.EQOn);
T.EQNumBands := JI(O,'eq_num_bands', T.EQNumBands);
for i := 0 to 10 do
begin
T.EQGains[i] := JD(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
T.EQFreqs[i] := JD(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
end;
T.AMCarrierLevel := JD(O,'am_carrier_level',T.AMCarrierLevel);
T.FMDeviation := JD(O,'fm_deviation', T.FMDeviation);
T.FMLowCut := JI(O,'fm_low_cut', T.FMLowCut);
T.FMHighCut := JI(O,'fm_high_cut', T.FMHighCut);
T.FMEmphPosition := JI(O,'fm_emph_position',T.FMEmphPosition);
T.CTCSSOn := JB(O,'ctcss_on', T.CTCSSOn);
T.CTCSSFreq := JD(O,'ctcss_freq', T.CTCSSFreq);
T.TXFFTSize := JI(O,'tx_fft_size', T.TXFFTSize);
T.TXWindowType := JI(O,'tx_win_type', T.TXWindowType);
T.TXSpecDetector := JI(O,'tx_spec_detector', T.TXSpecDetector);
T.TXSpecAvgMode := JI(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
T.TXSpecAvgTimeMS := JD(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
T.TXWfDetector := JI(O,'tx_wf_detector', T.TXWfDetector);
T.TXWfAvgMode := JI(O,'tx_wf_avg_mode', T.TXWfAvgMode);
T.TXWfAvgTimeMS := JD(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
T.TXSpecRefLevel := JD(O,'tx_spec_ref_level', T.TXSpecRefLevel);
T.TXSpecRange := JD(O,'tx_spec_range', T.TXSpecRange);
T.TXSpecGridStep := JD(O,'tx_spec_grid_step', T.TXSpecGridStep);
end;
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
const T: TTXSettings);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx');
JW(O,'mic_source', T.MicSource);
JW(O,'mic_boost', T.MicBoost);
JW(O,'mic_bias', T.MicBias);
JW(O,'mic_line_in', T.MicLineIn);
JW(O,'mic_ptt_enabled', T.MicPTTEnabled);
JW(O,'mic_gain_db', T.MicGainDB);
JW(O,'filter_low', T.FilterLow);
JW(O,'filter_high', T.FilterHigh);
JW(O,'filter_nc', T.FilterNC);
JW(O,'filter_mp', T.FilterMP);
JW(O,'filter_window', T.FilterWindow);
JW(O,'compressor_on', T.CompressorOn);
JW(O,'compressor_gain', T.CompressorGain);
JW(O,'leveler_on', T.LevelerOn);
JW(O,'leveler_top', T.LevelerTop);
JW(O,'leveler_decay', T.LevelerDecay);
JW(O,'alc_on', T.ALCOn);
JW(O,'alc_max_gain', T.ALCMaxGain);
JW(O,'alc_decay', T.ALCDecay);
JW(O,'phase_rot_on', T.PhaseRotOn);
JW(O,'phase_rot_stages',T.PhaseRotStages);
JW(O,'phase_rot_freq', T.PhaseRotFreq);
JW(O,'eq_on', T.EQOn);
JW(O,'eq_num_bands', T.EQNumBands);
for i := 0 to 10 do
begin
JW(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
JW(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
end;
JW(O,'am_carrier_level',T.AMCarrierLevel);
JW(O,'fm_deviation', T.FMDeviation);
JW(O,'fm_low_cut', T.FMLowCut);
JW(O,'fm_high_cut', T.FMHighCut);
JW(O,'fm_emph_position',T.FMEmphPosition);
JW(O,'ctcss_on', T.CTCSSOn);
JW(O,'ctcss_freq', T.CTCSSFreq);
JW(O,'tx_fft_size', T.TXFFTSize);
JW(O,'tx_win_type', T.TXWindowType);
JW(O,'tx_spec_detector', T.TXSpecDetector);
JW(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
JW(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
JW(O,'tx_wf_detector', T.TXWfDetector);
JW(O,'tx_wf_avg_mode', T.TXWfAvgMode);
JW(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
JW(O,'tx_spec_ref_level', T.TXSpecRefLevel);
JW(O,'tx_spec_range', T.TXSpecRange);
JW(O,'tx_spec_grid_step', T.TXSpecGridStep);
end;
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'preferences');
JW(O, 'light_theme', ALightTheme);
end;
function TSettingsManager.LoadTheme: Boolean;
var O: TJSONObject;
begin
Result := False;
if FRoot.Find('preferences') = nil then Exit;
O := EnsureObj(FRoot, 'preferences');
Result := JB(O, 'light_theme', False);
end;
end.