diff --git a/EqualizerControl.pas b/EqualizerControl.pas new file mode 100644 index 0000000..1102e89 --- /dev/null +++ b/EqualizerControl.pas @@ -0,0 +1,686 @@ +unit EqualizerControl; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, Controls, Graphics, LCLType, Types, Math, + FlatButton, AppTheme; + +type + TEqualizerControl = class(TCustomControl) + private + FEnabledEQ: Boolean; + FBandCount: Integer; + FPreampGain: Double; + FGains: array[1..10] of Double; + FFreqs: array[1..10] of Double; + FDragging: Integer; // -1=preamp, 1..10=band, 0=none + FHover: Integer; + FOnChange: TNotifyEvent; + FClrBG: TColor; + FClrPanel: TColor; + FClrBorder: TColor; + FClrGrid: TColor; + FClrText: TColor; + FClrTextDim: TColor; + FClrAccent: TColor; + FClrCurve: TColor; + FBtnEQ: TFlatButton; + FBtnBands3: TFlatButton; + FBtnBands10: TFlatButton; + FButtonTheme: TAppTheme; + function GetBandGain(Index: Integer): Double; + function GetBandFreq(Index: Integer): Double; + procedure SetEnabledEQ(V: Boolean); + procedure SetBandCount(V: Integer); + procedure SetPreampGain(V: Double); + procedure SetBandGain(Index: Integer; V: Double); + procedure SetBandFreq(Index: Integer; V: Double); + function TopRect: TRect; + function PreampRect: TRect; + function GraphRect: TRect; + function TilesRect: TRect; + function GainToY(G: Double; const R: TRect): Integer; + function YToGain(Y: Integer; const R: TRect): Double; + function BandX(Index: Integer; const R: TRect): Integer; + function XToFreq(X: Integer; const R: TRect): Double; + function BandColor(Index: Integer): TColor; + function InternalBandFreq(Index: Integer): Double; + function ResponseAtFreq(AFreq: Double): Double; + function FormatFreq(AFreq: Double): string; + function HitTestControl(X, Y: Integer): Integer; + procedure DrawTiles; + procedure LayoutButtons; + procedure StyleButton(B: TFlatButton; Active: Boolean); + procedure UpdateButtonStates; + procedure OnEQButtonClick(Sender: TObject); + procedure OnBands3Click(Sender: TObject); + procedure OnBands10Click(Sender: TObject); + procedure Changed; + protected + procedure Paint; override; + procedure Resize; override; + procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; + procedure MouseMove(Shift: TShiftState; X, Y: Integer); override; + procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; + procedure MouseLeave; override; + function DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean; override; + function DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean; override; + public + constructor Create(AOwner: TComponent); override; + procedure SetThemeColors(ABG, APanel, ABorder, AGrid, AText, ATextDim, + AAccent, ACurve: TColor); + procedure SetAppTheme(const ATheme: TAppTheme); + procedure SetBand(Index: Integer; AFreq, AGain: Double); + property EQEnabled: Boolean read FEnabledEQ write SetEnabledEQ; + property BandCount: Integer read FBandCount write SetBandCount; + property PreampGain: Double read FPreampGain write SetPreampGain; + property BandGain[Index: Integer]: Double read GetBandGain write SetBandGain; + property BandFreq[Index: Integer]: Double read GetBandFreq write SetBandFreq; + property OnChange: TNotifyEvent read FOnChange write FOnChange; + end; + +implementation + +const + MIN_GAIN = -15.0; + MAX_GAIN = 15.0; + VIS_MIN_GAIN = -20.0; + VIS_MAX_GAIN = 20.0; + MIN_FREQ = 20.0; + MAX_FREQ = 20000.0; + TOP_H = 44; + BOTTOM_H = 82; + +constructor TEqualizerControl.Create(AOwner: TComponent); +var + i: Integer; +begin + inherited Create(AOwner); + Width := 720; + Height := 300; + FEnabledEQ := False; + FBandCount := 10; + FPreampGain := 0.0; + FDragging := 0; + FHover := 0; + for i := 1 to 10 do + begin + FGains[i] := 0.0; + FFreqs[i] := 0.0; + end; + FClrBG := TColor($001A1A1A); + FClrPanel := TColor($00202020); + FClrBorder := TColor($00303030); + FClrGrid := TColor($00383838); + FClrText := TColor($00E0E0E0); + FClrTextDim := TColor($00888888); + FClrAccent := TColor($0040FF80); + FClrCurve := TColor($0055DDEE); + FButtonTheme := DarkTheme; + Cursor := crHandPoint; + + FBtnEQ := TFlatButton.Create(Self); + FBtnEQ.Parent := Self; + FBtnEQ.Caption := 'Equalizer'; + FBtnEQ.OnClick := @OnEQButtonClick; + + FBtnBands3 := TFlatButton.Create(Self); + FBtnBands3.Parent := Self; + FBtnBands3.Caption := '3 Band'; + FBtnBands3.OnClick := @OnBands3Click; + + FBtnBands10 := TFlatButton.Create(Self); + FBtnBands10.Parent := Self; + FBtnBands10.Caption := '10 Band'; + FBtnBands10.OnClick := @OnBands10Click; + + LayoutButtons; + UpdateButtonStates; +end; + +procedure TEqualizerControl.SetThemeColors(ABG, APanel, ABorder, AGrid, + AText, ATextDim, AAccent, ACurve: TColor); +begin + FClrBG := ABG; + FClrPanel := APanel; + FClrBorder := ABorder; + FClrGrid := AGrid; + FClrText := AText; + FClrTextDim := ATextDim; + FClrAccent := AAccent; + FClrCurve := ACurve; + UpdateButtonStates; + Invalidate; +end; + +procedure TEqualizerControl.SetAppTheme(const ATheme: TAppTheme); +begin + FButtonTheme := ATheme; + UpdateButtonStates; + Invalidate; +end; + +function TEqualizerControl.GetBandGain(Index: Integer): Double; +begin + if (Index >= 1) and (Index <= 10) then Result := FGains[Index] + else Result := 0.0; +end; + +function TEqualizerControl.GetBandFreq(Index: Integer): Double; +begin + if (Index >= 1) and (Index <= 10) then Result := FFreqs[Index] + else Result := 0.0; +end; + +procedure TEqualizerControl.Changed; +begin + UpdateButtonStates; + Invalidate; + if Assigned(FOnChange) then FOnChange(Self); +end; + +procedure TEqualizerControl.SetEnabledEQ(V: Boolean); +begin + if FEnabledEQ = V then Exit; + FEnabledEQ := V; + Changed; +end; + +procedure TEqualizerControl.SetBandCount(V: Integer); +begin + if V <= 3 then V := 3 else V := 10; + if FBandCount = V then Exit; + FBandCount := V; + Changed; +end; + +procedure TEqualizerControl.SetPreampGain(V: Double); +begin + V := EnsureRange(V, MIN_GAIN, MAX_GAIN); + if Abs(FPreampGain - V) < 0.001 then Exit; + FPreampGain := V; + Changed; +end; + +procedure TEqualizerControl.SetBandGain(Index: Integer; V: Double); +begin + if (Index < 1) or (Index > 10) then Exit; + V := EnsureRange(V, MIN_GAIN, MAX_GAIN); + if Abs(FGains[Index] - V) < 0.001 then Exit; + FGains[Index] := V; + Changed; +end; + +procedure TEqualizerControl.SetBandFreq(Index: Integer; V: Double); +begin + if (Index < 1) or (Index > 10) then Exit; + V := EnsureRange(V, MIN_FREQ, MAX_FREQ); + if Abs(FFreqs[Index] - V) < 0.001 then Exit; + FFreqs[Index] := V; + Changed; +end; + +procedure TEqualizerControl.SetBand(Index: Integer; AFreq, AGain: Double); +begin + if (Index < 1) or (Index > 10) then Exit; + if AFreq <= 0.0 then + FFreqs[Index] := MIN_FREQ * Power(MAX_FREQ / MIN_FREQ, (Index - 1) / 9) + else + FFreqs[Index] := EnsureRange(AFreq, MIN_FREQ, MAX_FREQ); + FGains[Index] := EnsureRange(AGain, MIN_GAIN, MAX_GAIN); + Invalidate; +end; + +function TEqualizerControl.GraphRect: TRect; +var + R: TRect; +begin + R := TopRect; + Result := Rect(R.Left + 54, R.Top, R.Right, R.Bottom); +end; + +function TEqualizerControl.TopRect: TRect; +begin + Result := Rect(40, TOP_H, Width - 26, Height - BOTTOM_H - 10); +end; + +function TEqualizerControl.PreampRect: TRect; +var + R: TRect; +begin + R := TopRect; + Result := Rect(R.Left, R.Top, R.Left + 38, R.Bottom); +end; + +function TEqualizerControl.TilesRect: TRect; +begin + Result := Rect(40, Height - BOTTOM_H + 12, Width - 26, Height - 6); +end; + +function TEqualizerControl.GainToY(G: Double; const R: TRect): Integer; +begin + G := EnsureRange(G, VIS_MIN_GAIN, VIS_MAX_GAIN); + Result := R.Bottom - Round((G - VIS_MIN_GAIN) / (VIS_MAX_GAIN - VIS_MIN_GAIN) * (R.Bottom - R.Top)); +end; + +function TEqualizerControl.YToGain(Y: Integer; const R: TRect): Double; +begin + Result := VIS_MIN_GAIN + (R.Bottom - EnsureRange(Y, R.Top, R.Bottom)) / + Max(1, R.Bottom - R.Top) * (VIS_MAX_GAIN - VIS_MIN_GAIN); + Result := EnsureRange(Result, MIN_GAIN, MAX_GAIN); +end; + +function TEqualizerControl.BandX(Index: Integer; const R: TRect): Integer; +var + F: Double; +begin + F := EnsureRange(InternalBandFreq(Index), MIN_FREQ, MAX_FREQ); + Result := R.Left + Round(Ln(F / MIN_FREQ) / Ln(MAX_FREQ / MIN_FREQ) * (R.Right - R.Left)); +end; + +function TEqualizerControl.XToFreq(X: Integer; const R: TRect): Double; +var + P: Double; +begin + P := EnsureRange((X - R.Left) / Max(1, R.Right - R.Left), 0.0, 1.0); + Result := MIN_FREQ * Power(MAX_FREQ / MIN_FREQ, P); +end; + +function TEqualizerControl.BandColor(Index: Integer): TColor; +const + C: array[1..10] of TColor = ( + TColor($00E05A2A), TColor($00D99028), TColor($00C9B72C), TColor($006FBF45), + TColor($003DBE70), TColor($0035B7C4), TColor($003B88E0), TColor($006456E8), + TColor($00A653D6), TColor($00D34F9D)); +begin + if (Index >= 1) and (Index <= 10) then Result := C[Index] + else Result := FClrAccent; +end; + +function TEqualizerControl.InternalBandFreq(Index: Integer): Double; +begin + if (Index >= 1) and (Index <= 10) and (FFreqs[Index] > 0.0) then + Result := FFreqs[Index] + else + Result := MIN_FREQ * Power(MAX_FREQ / MIN_FREQ, EnsureRange((Index - 1) / 9, 0.0, 1.0)); +end; + +function TEqualizerControl.ResponseAtFreq(AFreq: Double): Double; +var + i, N: Integer; + D, W, Sum, LF, LBand: Double; +begin + Result := FPreampGain; + LF := Ln(EnsureRange(AFreq, MIN_FREQ, MAX_FREQ)); + Sum := 0.0; + N := FBandCount; + for i := 1 to N do + begin + LBand := Ln(InternalBandFreq(i)); + D := (LF - LBand) / 0.30; + W := Exp(-0.5 * D * D); + Sum := Sum + FGains[i] * W; + end; + Result := Result + Sum; + Result := EnsureRange(Result, -24.0, 24.0); +end; + +function TEqualizerControl.FormatFreq(AFreq: Double): string; +begin + if AFreq >= 1000.0 then + Result := FormatFloat('0.#', AFreq / 1000.0) + 'k' + else + Result := FormatFloat('0', AFreq); +end; + +function TEqualizerControl.HitTestControl(X, Y: Integer): Integer; +var + R: TRect; + i, BX, BY, N: Integer; +begin + Result := 0; + R := PreampRect; + if PtInRect(R, Point(X, Y)) then Exit(-1); + + R := GraphRect; + N := FBandCount; + for i := 1 to N do + begin + BX := BandX(i, R); + BY := GainToY(FGains[i], R); + if (Abs(X - BX) <= 12) and (Abs(Y - BY) <= 12) then + Exit(i); + end; +end; + +procedure TEqualizerControl.DrawTiles; +var + R, TR: TRect; + i, TileW, X: Integer; + C: TColor; + S: string; +begin + R := TilesRect; + TileW := Max(1, (R.Right - R.Left) div 10); + Canvas.Font.Size := 8; + Canvas.Brush.Style := bsSolid; + for i := 1 to 10 do + begin + X := R.Left + (i - 1) * TileW; + TR := Rect(X, R.Top, X + TileW - 2, R.Bottom); + C := BandColor(i); + if i <= FBandCount then Canvas.Brush.Color := TColor($00262626) + else Canvas.Brush.Color := TColor($001F1F1F); + Canvas.Pen.Color := FClrBorder; + Canvas.Rectangle(TR); + + Canvas.Pen.Color := C; + Canvas.Brush.Style := bsClear; + Canvas.Arc(TR.Left + 16, TR.Top + 6, TR.Left + 48, TR.Top + 30, + TR.Left + 18, TR.Top + 22, TR.Left + 46, TR.Top + 22); + Canvas.Brush.Style := bsSolid; + Canvas.Brush.Color := C; + Canvas.Ellipse(TR.Left + 30, TR.Top + 13, TR.Left + 38, TR.Top + 21); + + Canvas.Brush.Style := bsClear; + Canvas.Font.Color := FClrTextDim; + S := FormatFreq(InternalBandFreq(i)); + Canvas.TextOut(TR.Left + 6, TR.Top + 34, S); + Canvas.Font.Color := C; + Canvas.TextOut(TR.Left + 6, TR.Top + 49, FormatFloat('0.0', FGains[i])); + Canvas.Brush.Style := bsSolid; + end; + Canvas.Font.Size := 9; +end; + +procedure TEqualizerControl.LayoutButtons; +begin + if FBtnEQ <> nil then + FBtnEQ.SetBounds(18, 12, 94, 22); + if FBtnBands3 <> nil then + FBtnBands3.SetBounds(Width - 210, 12, 102, 24); + if FBtnBands10 <> nil then + FBtnBands10.SetBounds(Width - 100, 12, 82, 24); +end; + +procedure TEqualizerControl.StyleButton(B: TFlatButton; Active: Boolean); +begin + if B = nil then Exit; + B.Active := Active; + B.ClrNorm := FButtonTheme.BtnNorm; + B.ClrActive := FButtonTheme.BtnActive; + B.ClrHot := FButtonTheme.BtnHot; + if Active then + B.ClrBorder := FButtonTheme.BtnBorderActive + else + B.ClrBorder := FButtonTheme.BtnBorderNorm; + B.ClrText := FButtonTheme.BtnText; + B.ClrTextAct := FButtonTheme.BtnTextActive; + B.Font.Name := 'Courier New'; + B.Font.Size := 8; + B.Invalidate; +end; + +procedure TEqualizerControl.UpdateButtonStates; +begin + StyleButton(FBtnEQ, FEnabledEQ); + StyleButton(FBtnBands3, FBandCount = 3); + StyleButton(FBtnBands10, FBandCount = 10); +end; + +procedure TEqualizerControl.OnEQButtonClick(Sender: TObject); +begin + EQEnabled := not FEnabledEQ; +end; + +procedure TEqualizerControl.OnBands3Click(Sender: TObject); +begin + BandCount := 3; +end; + +procedure TEqualizerControl.OnBands10Click(Sender: TObject); +begin + BandCount := 10; +end; + +procedure TEqualizerControl.Paint; +var + R, PR: TRect; + i, X, Y, PrevX, PrevY, N, ZeroY, AxisY, PreX, CurveL, CurveR: Integer; + GX, GY, LastFillY, FillBottom: Integer; + FreqMarks: array[0..9] of Double; + S: string; + G, F: Double; +begin + Canvas.Brush.Color := FClrBG; + Canvas.FillRect(ClientRect); + Canvas.Font.Name := Font.Name; + Canvas.Font.Size := 9; + Canvas.Brush.Style := bsClear; + + Canvas.Brush.Style := bsClear; + Canvas.Font.Color := FClrTextDim; + Canvas.TextOut(126, 16, Format('Preamp %.1f dB', [FPreampGain])); + + R := GraphRect; + PR := PreampRect; + Canvas.Brush.Style := bsSolid; + Canvas.Brush.Color := FClrPanel; + Canvas.Pen.Color := FClrBorder; + Canvas.Rectangle(PR); + PreX := (PR.Left + PR.Right) div 2; + Canvas.Pen.Color := TColor($00424242); + Canvas.Pen.Width := 3; + Canvas.Line(PreX, PR.Top + 12, PreX, PR.Bottom - 22); + Canvas.Pen.Width := 1; + Y := GainToY(FPreampGain, PR); + Y := EnsureRange(Y, PR.Top + 12, PR.Bottom - 22); + Canvas.Brush.Color := TColor($00303030); + Canvas.Pen.Color := FClrAccent; + Canvas.Ellipse(PreX - 7, Y - 7, PreX + 8, Y + 8); + Canvas.Brush.Color := FClrAccent; + Canvas.Pen.Color := FClrAccent; + Canvas.Ellipse(PreX - 3, Y - 3, PreX + 4, Y + 4); + Canvas.Brush.Style := bsClear; + Canvas.Font.Size := 8; + Canvas.Font.Color := FClrTextDim; + Canvas.TextOut(PreX - Canvas.TextWidth('Pre') div 2, PR.Bottom - 16, 'Pre'); + Canvas.Font.Size := 9; + + Canvas.Brush.Style := bsSolid; + Canvas.Brush.Color := FClrPanel; + Canvas.Pen.Color := FClrBorder; + Canvas.Rectangle(R); + + FreqMarks[0] := 20; FreqMarks[1] := 50; FreqMarks[2] := 100; + FreqMarks[3] := 200; FreqMarks[4] := 500; FreqMarks[5] := 1000; + FreqMarks[6] := 2000; FreqMarks[7] := 5000; FreqMarks[8] := 10000; + FreqMarks[9] := 20000; + Canvas.Pen.Color := FClrGrid; + Canvas.Font.Size := 8; + for i := 0 to High(FreqMarks) do + begin + X := R.Left + Round(Ln(FreqMarks[i] / MIN_FREQ) / Ln(MAX_FREQ / MIN_FREQ) * + (R.Right - R.Left)); + Canvas.Line(X, R.Top + 1, X, R.Bottom - 1); + end; + + for i := -20 to 20 do + if (i mod 5) = 0 then + begin + Y := GainToY(i, R); + Canvas.Line(R.Left + 1, Y, R.Right - 1, Y); + Canvas.Brush.Style := bsClear; + Canvas.Font.Color := FClrTextDim; + if (i <> 0) and (Abs(i) <> 5) and (i >= -15) and (i <= 15) then + begin + if i > 0 then S := '+' + IntToStr(i) + 'dB' + else S := IntToStr(i) + 'dB'; + Canvas.TextOut(R.Left + 4, Y - (Canvas.TextHeight(S) * 3 div 5), S); + end; + end; + + ZeroY := GainToY(0, R); + Canvas.Pen.Color := FClrBorder; + Canvas.Line(R.Left + 1, ZeroY, R.Right - 1, ZeroY); + + Canvas.Brush.Color := TColor($00304438); + Canvas.Pen.Color := TColor($00304438); + FillBottom := ZeroY; + LastFillY := ZeroY; + CurveL := R.Left + 2; + CurveR := R.Right - 2; + for GX := CurveL to CurveR do + begin + F := XToFreq(GX, R); + G := ResponseAtFreq(F); + GY := GainToY(EnsureRange(G, MIN_GAIN, MAX_GAIN), R); + if GX = R.Left then + LastFillY := GY + else + Canvas.Polygon([Point(GX - 1, LastFillY), Point(GX, GY), + Point(GX, FillBottom), Point(GX - 1, FillBottom)]); + LastFillY := GY; + end; + + PrevX := CurveL; + PrevY := GainToY(EnsureRange(ResponseAtFreq(XToFreq(CurveL, R)), MIN_GAIN, MAX_GAIN), R); + Canvas.Pen.Color := FClrCurve; + Canvas.Pen.Width := 2; + for GX := CurveL + 1 to CurveR do + begin + F := XToFreq(GX, R); + GY := GainToY(EnsureRange(ResponseAtFreq(F), MIN_GAIN, MAX_GAIN), R); + Canvas.Line(PrevX, PrevY, GX, GY); + PrevX := GX; + PrevY := GY; + end; + Canvas.Pen.Width := 1; + + N := FBandCount; + for i := 1 to N do + begin + X := BandX(i, R); + Y := GainToY(FGains[i], R); + Canvas.Brush.Color := BandColor(i); + Canvas.Pen.Color := FClrBG; + Canvas.Ellipse(X - 5, Y - 5, X + 6, Y + 6); + if i = FHover then + begin + Canvas.Brush.Style := bsClear; + Canvas.Pen.Color := clWhite; + Canvas.Ellipse(X - 9, Y - 9, X + 10, Y + 10); + Canvas.Brush.Style := bsSolid; + end; + end; + + Canvas.Font.Color := FClrTextDim; + Canvas.Brush.Style := bsClear; + Canvas.Font.Size := 8; + AxisY := ZeroY + 4; + for i := 0 to High(FreqMarks) do + begin + X := R.Left + Round(Ln(FreqMarks[i] / MIN_FREQ) / Ln(MAX_FREQ / MIN_FREQ) * + (R.Right - R.Left)); + S := FormatFreq(FreqMarks[i]); + if i = 0 then + Canvas.TextOut(X, AxisY, S) + else if i = High(FreqMarks) then + Canvas.TextOut(X - Canvas.TextWidth(S), AxisY, S) + else + Canvas.TextOut(X - Canvas.TextWidth(S) div 2, AxisY, S); + end; + Canvas.Font.Size := 9; + + DrawTiles; +end; + +procedure TEqualizerControl.Resize; +begin + inherited Resize; + LayoutButtons; +end; + +procedure TEqualizerControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +var + H: Integer; +begin + inherited MouseDown(Button, Shift, X, Y); + if Button <> mbLeft then Exit; + H := HitTestControl(X, Y); + case H of + -1: + begin + FDragging := -1; + PreampGain := YToGain(Y, PreampRect); + end; + 1..10: + begin + FDragging := H; + BandGain[H] := YToGain(Y, GraphRect); + BandFreq[H] := XToFreq(X, GraphRect); + end; + end; +end; + +procedure TEqualizerControl.MouseMove(Shift: TShiftState; X, Y: Integer); +var + H: Integer; +begin + inherited MouseMove(Shift, X, Y); + if FDragging = -1 then + PreampGain := YToGain(Y, PreampRect) + else if FDragging > 0 then + begin + BandGain[FDragging] := YToGain(Y, GraphRect); + BandFreq[FDragging] := XToFreq(X, GraphRect); + end + else + begin + H := HitTestControl(X, Y); + if H < 0 then H := 0; + if FHover <> H then + begin + FHover := H; + Invalidate; + end; + end; +end; + +procedure TEqualizerControl.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +begin + inherited MouseUp(Button, Shift, X, Y); + FDragging := 0; +end; + +procedure TEqualizerControl.MouseLeave; +begin + inherited MouseLeave; + FHover := 0; + Invalidate; +end; + +function TEqualizerControl.DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean; +var + H: Integer; +begin + H := HitTestControl(MousePos.X, MousePos.Y); + if H = -1 then PreampGain := FPreampGain + 0.5 + else if H > 0 then BandGain[H] := FGains[H] + 0.5; + Result := H <> 0; +end; + +function TEqualizerControl.DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean; +var + H: Integer; +begin + H := HitTestControl(MousePos.X, MousePos.Y); + if H = -1 then PreampGain := FPreampGain - 0.5 + else if H > 0 then BandGain[H] := FGains[H] - 0.5; + Result := H <> 0; +end; + +end. diff --git a/SettingsForm.pas b/SettingsForm.pas index 909e2eb..27ee697 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -27,7 +27,7 @@ uses Classes, SysUtils, Math, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, Spin, - FlatButton, AudioOutput, AudioInput, AppTheme, Settings; + FlatButton, EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings; type // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон @@ -164,10 +164,7 @@ type FEdTXPHRStages: TSpinEdit; FEdTXPHRFreq: TFloatSpinEdit; // EQ - FChkTXEQOn: TCheckBox; - FCmbTXEQBands: TComboBox; - FEdTXEQGain: array[0..10] of TFloatSpinEdit; - FEdTXEQFreq: array[0..10] of TFloatSpinEdit; + FTXEQControl: TEqualizerControl; // AM FEdTXAMCarrier: TFloatSpinEdit; // FM @@ -1007,10 +1004,10 @@ const PAD = 18; LW = 150; CX = PAD + LW + 12; - STEP = 32; + STEP = 42; R1 = 42; SPW = 96; - CMB_W = 200; + GRP_W = 760; var Grp: TPanel; @@ -1090,7 +1087,7 @@ begin // ---- Mic options ---- // Mic source (Radio vs SoundCard) определяется автоматически по источнику PTT. Y := 86; - Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, 700, 188); + Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, GRP_W, 188); // Row 0: Mic jack selector | Boost (Mic In) или LineInGain (Line In) | Bias | PTT MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, 65); @@ -1124,7 +1121,7 @@ begin // ---- Tune (TUN button) ---- Inc(Y, 188 + 12); - Grp := MakeGroupPanel(FPageTransmit, 'Tune (TUN)', MARGIN, Y, 700, 100); + Grp := MakeGroupPanel(FPageTransmit, 'Tune (TUN)', MARGIN, Y, GRP_W, 100); MakeLbl(Grp, 'Tune level (%)', PAD, R1 + 5, LW); FEdTUNLevel := MkSpin(Grp, CX, R1, 0, 100, 10); @@ -1133,7 +1130,7 @@ begin // ---- TX bandpass filter ---- Inc(Y, 100 + 12); - Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, 700, 200); + Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, GRP_W, 220); MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW); FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 100); @@ -1157,8 +1154,8 @@ begin FCmbTXFiltWindow.ItemIndex := 1; // ---- Compressor / Leveler / ALC / Phase Rotator ---- - Inc(Y, 200 + 12); - Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, 700, 220); + Inc(Y, 220 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, GRP_W, 220); FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor'); MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80); @@ -1184,31 +1181,20 @@ begin // ---- TX EQ ---- Inc(Y, 220 + 12); - Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, 700, 320); + Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, GRP_W, 390); - FChkTXEQOn := MkChk(Grp, PAD, R1, 200, 'Enable EQ'); - MakeLbl(Grp, 'Bands', PAD + 230, R1 + 5, 60); - FCmbTXEQBands := MkCmb(Grp, PAD + 290, R1, 80); - FCmbTXEQBands.Items.AddStrings(['3','10']); - FCmbTXEQBands.ItemIndex := 1; - - // 11 строк: [0]=Preamp, [1..10]=полосы. Freq+Gain в одной строке. - for i := 0 to 10 do - begin - if i = 0 then - MakeLbl(Grp, 'Preamp (dB)', PAD, R1 + STEP + i * 22 + 3, 100) - else - MakeLbl(Grp, Format('Band %d', [i]), PAD, R1 + STEP + i * 22 + 3, 80); - FEdTXEQFreq[i] := MkFloat(Grp, PAD + 90, R1 + STEP + i * 22, 0.0, 0.0, 20000.0, 10.0); - FEdTXEQFreq[i].Width := 100; - MakeLbl(Grp, 'gain', PAD + 200, R1 + STEP + i * 22 + 3, 30); - FEdTXEQGain[i] := MkFloat(Grp, PAD + 230, R1 + STEP + i * 22, 0.0, -15.0, 15.0, 0.5); - FEdTXEQGain[i].Width := 80; - end; + FTXEQControl := TEqualizerControl.Create(Self); + FTXEQControl.Parent := Grp; + FTXEQControl.SetBounds(PAD, R1, GRP_W - 2 * PAD, 326); + FTXEQControl.Font.Name := UI_FONT; + FTXEQControl.Font.Size := 9; + FTXEQControl.SetThemeColors(CLR_PANEL, TColor($00202020), CLR_BORDER, + TColor($00383838), CLR_TEXT, CLR_TEXTDIM, CLR_ACCENT, TColor($0055DDEE)); + FTXEQControl.OnChange := OnTXAnyChange; // ---- AM / FM / CTCSS ---- - Inc(Y, 320 + 12); - Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, 700, 220); + Inc(Y, 390 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, GRP_W, 260); MakeLbl(Grp, 'AM carrier level', PAD, R1 + 5, LW); FEdTXAMCarrier := MkFloat(Grp, CX, R1, 0.5, 0.0, 1.0, 0.05); @@ -1231,8 +1217,8 @@ begin FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1); // ---- TX Display (отдельный analyzer для TX-спектра/водопада) ---- - Inc(Y, 220 + 12); - Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, 700, 290); + Inc(Y, 260 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, GRP_W, 318); MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW); FCmbTXFFT := MkCmb(Grp, CX, R1, 140); @@ -1269,8 +1255,8 @@ begin FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0); // ---- TX Grid (RefLevel / Range / GridStep — отдельные от RX) ---- - Inc(Y, 290 + 12); - Grp := MakeGroupPanel(FPageTransmit, 'TX Grid', MARGIN, Y, 700, 170); + Inc(Y, 318 + 12); + Grp := MakeGroupPanel(FPageTransmit, 'TX Grid', MARGIN, Y, GRP_W, 170); MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW); FEdTXRefLevel := TEdit.Create(Self); @@ -1330,13 +1316,16 @@ begin if FChkTXPHROn <> nil then FTX.PhaseRotOn := FChkTXPHROn.Checked; if FEdTXPHRStages <> nil then FTX.PhaseRotStages := FEdTXPHRStages.Value; if FEdTXPHRFreq <> nil then FTX.PhaseRotFreq := FEdTXPHRFreq.Value; - if FChkTXEQOn <> nil then FTX.EQOn := FChkTXEQOn.Checked; - if FCmbTXEQBands <> nil then - if FCmbTXEQBands.ItemIndex = 0 then FTX.EQNumBands := 3 else FTX.EQNumBands := 10; - for i := 0 to 10 do + if FTXEQControl <> nil then begin - if FEdTXEQGain[i] <> nil then FTX.EQGains[i] := FEdTXEQGain[i].Value; - if FEdTXEQFreq[i] <> nil then FTX.EQFreqs[i] := FEdTXEQFreq[i].Value; + FTX.EQOn := FTXEQControl.EQEnabled; + FTX.EQNumBands := FTXEQControl.BandCount; + FTX.EQGains[0] := FTXEQControl.PreampGain; + for i := 1 to 10 do + begin + FTX.EQGains[i] := FTXEQControl.BandGain[i]; + FTX.EQFreqs[i] := FTXEQControl.BandFreq[i]; + end; end; if FEdTXAMCarrier <> nil then FTX.AMCarrierLevel := FEdTXAMCarrier.Value; if FEdTXFMDev <> nil then FTX.FMDeviation := FEdTXFMDev.Value; @@ -1443,14 +1432,13 @@ begin if FChkTXPHROn <> nil then FChkTXPHROn.Checked := T.PhaseRotOn; if FEdTXPHRStages <> nil then FEdTXPHRStages.Value := T.PhaseRotStages; if FEdTXPHRFreq <> nil then FEdTXPHRFreq.Value := T.PhaseRotFreq; - if FChkTXEQOn <> nil then FChkTXEQOn.Checked := T.EQOn; - if FCmbTXEQBands <> nil then - if T.EQNumBands <= 3 then FCmbTXEQBands.ItemIndex := 0 - else FCmbTXEQBands.ItemIndex := 1; - for i := 0 to 10 do + if FTXEQControl <> nil then begin - if FEdTXEQGain[i] <> nil then FEdTXEQGain[i].Value := T.EQGains[i]; - if FEdTXEQFreq[i] <> nil then FEdTXEQFreq[i].Value := T.EQFreqs[i]; + FTXEQControl.EQEnabled := T.EQOn; + FTXEQControl.BandCount := T.EQNumBands; + FTXEQControl.PreampGain := T.EQGains[0]; + for i := 1 to 10 do + FTXEQControl.SetBand(i, T.EQFreqs[i], T.EQGains[i]); end; if FEdTXAMCarrier <> nil then FEdTXAMCarrier.Value := T.AMCarrierLevel; if FEdTXFMDev <> nil then FEdTXFMDev.Value := T.FMDeviation; @@ -1523,10 +1511,13 @@ end; procedure TSettingsForm.BuildPATab; const MARGIN = 22; - LBL_W = 46; + LBL_W = 54; ED_W = 72; - COL_W = LBL_W + ED_W + 8; + COL_W = LBL_W + ED_W + 14; COLS = 4; + VHF_LBL_W = 170; + VHF_COL_W = VHF_LBL_W + ED_W + 48; + VHF_COLS = 2; ROW_H = 32; GRP_PAD = 18; R1 = 42; @@ -1579,7 +1570,7 @@ begin Font.Color := CLR_TEXTDIM; end; - Grp := MakeGroupPanel(FPagePA, 'Power && Calibration', MARGIN, 86, 650, GRP_H); + Grp := MakeGroupPanel(FPagePA, 'Power && Calibration', MARGIN, 86, 700, GRP_H); // Max Power Lbl := TLabel.Create(Self); @@ -1635,33 +1626,35 @@ begin // ── VHF Calibration (Transverters) ────────────────────────────────────── Grp := MakeGroupPanel(FPagePA, 'VHF Calibration (Transverters)', - MARGIN, 86 + GRP_H + 14, 650, 2 * ROW_H + 62); + MARGIN, 86 + GRP_H + 14, 700, R1 + 4 * ROW_H + 44); Lbl := TLabel.Create(Self); Lbl.Parent := Grp; Lbl.Caption := 'Calibration per transverter slot, 38.8 ... 100.0'; - Lbl.SetBounds(GRP_PAD, 16, 400, 18); + Lbl.SetBounds(GRP_PAD, R1, 420, 18); Lbl.Font.Color := CLR_TEXTDIM; Lbl.Font.Name := UI_FONT; Lbl.Font.Size := 9; for i := 0 to CFG_XVTR_COUNT - 1 do begin - r := i div COLS; - c := i mod COLS; - X := GRP_PAD + c * (COL_W + 4); - Y := 38 + r * ROW_H; + r := i div VHF_COLS; + c := i mod VHF_COLS; + X := GRP_PAD + c * VHF_COL_W; + Y := R1 + 24 + r * ROW_H; Lbl := TLabel.Create(Self); Lbl.Parent := Grp; Lbl.Caption := 'VHF' + IntToStr(i) + ':'; - Lbl.SetBounds(X, Y + 5, LBL_W, 18); + Lbl.AutoSize := False; + Lbl.SetBounds(X, Y + 5, VHF_LBL_W, 18); Lbl.Font.Color := CLR_TEXTDIM; Lbl.Font.Name := UI_FONT; Lbl.Font.Size := 9; + Lbl.ShowHint := True; FVHFCalLabels[i] := Lbl; - FEdVHFCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnVHFCalEdit); + FEdVHFCal[i] := MakeFloatSpin(Grp, X + VHF_LBL_W + 8, Y, OnVHFCalEdit); FEdVHFCal[i].Tag := i; end; @@ -1673,8 +1666,8 @@ const GRP_PAD = 18; LBL_W = 110; ED_W = 200; - BTN_H = 22; - ROW_H = 28; + ROW_H = 42; + R1 = 42; var Grp: TPanel; Chk: TCheckBox; @@ -1694,12 +1687,12 @@ begin end; // ── ADC ─────────────────────────────────────────────────────────────────── - Grp := MakeGroupPanel(FPageAdvanced, 'ADC', MARGIN, 62, 420, 82); + Grp := MakeGroupPanel(FPageAdvanced, 'ADC', MARGIN, 86, 420, 128); Chk := TCheckBox.Create(Self); Chk.Parent := Grp; Chk.Caption := 'Dither Enabled'; - Chk.SetBounds(GRP_PAD, 24, 260, BTN_H); + Chk.SetBounds(GRP_PAD, R1, 260, 22); Chk.Font.Name := UI_FONT; Chk.Font.Size := 9; Chk.Font.Color := CLR_TEXT; @@ -1710,7 +1703,7 @@ begin Chk := TCheckBox.Create(Self); Chk.Parent := Grp; Chk.Caption := 'Random Enabled'; - Chk.SetBounds(GRP_PAD, 50, 260, BTN_H); + Chk.SetBounds(GRP_PAD, R1 + ROW_H, 260, 22); Chk.Font.Name := UI_FONT; Chk.Font.Size := 9; Chk.Font.Color := CLR_TEXT; @@ -1719,13 +1712,13 @@ begin FChkRandom := Chk; // ── Web Server ──────────────────────────────────────────────────────────── - Grp := MakeGroupPanel(FPageAdvanced, 'Web Server', MARGIN, 160, 420, 196); + Grp := MakeGroupPanel(FPageAdvanced, 'Web Server', MARGIN, 230, 500, 252); - Y := 24; + Y := R1; Chk := TCheckBox.Create(Self); Chk.Parent := Grp; Chk.Caption := 'Enabled'; - Chk.SetBounds(GRP_PAD, Y, 260, BTN_H); + Chk.SetBounds(GRP_PAD, Y, 260, 22); Chk.Font.Name := UI_FONT; Chk.Font.Size := 9; Chk.Font.Color := CLR_TEXT; @@ -1733,11 +1726,11 @@ begin Chk.OnChange := OnWebAnyChange; FChkWebEnabled := Chk; - Y := Y + ROW_H + 4; + Y := Y + ROW_H; MakeLbl(Grp, 'Port:', GRP_PAD, Y + 4, LBL_W); Spin := TSpinEdit.Create(Self); Spin.Parent := Grp; - Spin.SetBounds(GRP_PAD + LBL_W, Y, 90, BTN_H + 2); + Spin.SetBounds(GRP_PAD + LBL_W + 12, Y, 90, BTN_H); Spin.Color := CLR_INPUT; Spin.Font.Color := CLR_INPUT_TEXT; Spin.Font.Name := UI_FONT; @@ -1752,7 +1745,7 @@ begin MakeLbl(Grp, 'Interface:', GRP_PAD, Y + 4, LBL_W); Ed := TEdit.Create(Self); Ed.Parent := Grp; - Ed.SetBounds(GRP_PAD + LBL_W, Y, ED_W, BTN_H + 2); + Ed.SetBounds(GRP_PAD + LBL_W + 12, Y, ED_W, BTN_H); Ed.Color := CLR_INPUT; Ed.Font.Color := CLR_INPUT_TEXT; Ed.Font.Name := UI_FONT; @@ -1765,7 +1758,7 @@ begin MakeLbl(Grp, 'Username:', GRP_PAD, Y + 4, LBL_W); Ed := TEdit.Create(Self); Ed.Parent := Grp; - Ed.SetBounds(GRP_PAD + LBL_W, Y, ED_W, BTN_H + 2); + Ed.SetBounds(GRP_PAD + LBL_W + 12, Y, ED_W, BTN_H); Ed.Color := CLR_INPUT; Ed.Font.Color := CLR_INPUT_TEXT; Ed.Font.Name := UI_FONT; @@ -1778,7 +1771,7 @@ begin MakeLbl(Grp, 'Password:', GRP_PAD, Y + 4, LBL_W); Ed := TEdit.Create(Self); Ed.Parent := Grp; - Ed.SetBounds(GRP_PAD + LBL_W, Y, ED_W, BTN_H + 2); + Ed.SetBounds(GRP_PAD + LBL_W + 12, Y, ED_W, BTN_H); Ed.Color := CLR_INPUT; Ed.Font.Color := CLR_INPUT_TEXT; Ed.Font.Name := UI_FONT; @@ -1853,7 +1846,9 @@ procedure TSettingsForm.ApplyTheme(const T: TAppTheme); for i := 0 to C.ControlCount - 1 do begin Ctrl := C.Controls[i]; - if Ctrl is TFlatButton then + if Ctrl is TEqualizerControl then + TEqualizerControl(Ctrl).SetAppTheme(T) + else if Ctrl is TFlatButton then StyleBtn(TFlatButton(Ctrl)) else if Ctrl is TScrollBox then begin @@ -3219,6 +3214,7 @@ begin FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ' (' + X.Entries[i].ButtonText + '):' else FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ':'; + FVHFCalLabels[i].Hint := FVHFCalLabels[i].Caption; end; end; if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower; diff --git a/ewsdr.lpi b/ewsdr.lpi index 2e8f485..dffb9d6 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -123,6 +123,10 @@ + + + +