Files
ewsdr/Settings.pas
T
ew8bakandClaude Opus 4.7 ce97ee486c WIP: TX implementation — per-device settings, separate analyzer, sound-card mic
- Settings: TTXSettings record + LoadTX/SaveTX, JSON section "tx" per MAC
- WDSPEngine: separate TX_DISP_ID analyzer with own FFT/Window/Det/Avg
  parameters, ApplyTXAnalyzerSettings + setters
- WDSPEngine: full TX chain setters (Filter/Compressor/Leveler/ALC/
  PhaseRot/EQ/AMCarrier/FM/CTCSS) + ApplyTXChainSettings
- WDSPEngine: ProcessTXBlock now feeds TX analyzer in DISPLAY_BLOCK_SIZE
  chunks (was passing whole FTXOut → analyzer ignored most of it)
- WDSPEngine: ActiveDisplayID switches RX/TX in UpdateSpectrum
- WDSPEngine: RX audio muted while TX active to avoid local feedback
- WDSPEngine: default EQ frequencies in Create (zero freqs broke TX chain)
- WDSPEngine: PushTXMicSampleD + OnPullMicSamples for sound-card path
- AudioInput: Available counter for non-blocking pull
- MainForm: PullSoundCardMic, BuildMicLineSelectByte, SendDUCSpecific
  with mic bias/boost/line/PTT bits, OnTXSettingsChange
- MainForm: auto-switch MicSource on TX device select, TX diagnostic
  in StatusBar panel [5]
- SettingsForm: new Transmit nav page with Microphone, Filter, Dynamic
  processing, EQ, AM/FM/CTCSS, TX Display sections

NOT YET WORKING: sound-card mic produces ring activity but signal still
not visible on TX spectrum/waterfall — needs further investigation.

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

897 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;
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;
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);
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);
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.