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 @@
+
+
+
+