Files
ewsdr/EqualizerControl.pas
T

800 lines
25 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 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;
FRespDB: array of Double; // АЧХ по бинам (дБ), пересчитывается в Paint
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 GridBandFreq(Index: Integer): Double;
function InternalBandFreq(Index: Integer): Double;
procedure BuildResponse;
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;
// Кривая = точное повторение расчёта WDSP (wdsp/eq.c, eq_impulse) под
// геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц),
// eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) —
// SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов
// до Найквиста. Рисовать «колокольчики», как принято у графических EQ,
// нельзя: WDSP строит ломаную по точкам и обрывает всё за крайними.
RESP_RATE = 48000.0;
RESP_MID = 1024; // N/2
RESP_BIN_HZ = RESP_RATE / 2.0 / RESP_MID; // 23.4375 Hz
RESP_FLOOR = -2000.0; // 1e-100 в амплитуде — пол WDSP
// Legacy-раскладка 3-полосного EQ у самого WDSP (SetTXAGrphEQ) — её же
// отдаёт движок (WDSPEngine.PushTXEQProfile). Частоты фиксированы, ручка
// Low держит полку 150..400 Гц, High тянется до 6 кГц.
EQ3_F: array[1..3] of Double = (150.0, 1500.0, 6000.0);
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.GridBandFreq(Index: Integer): Double;
// Узел 10-полосной сетки профиля (то, что реально хранится и сохраняется).
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.InternalBandFreq(Index: Integer): Double;
// Частота, на которой ручка ДЕЙСТВУЕТ. В 3-полосном режиме это не наша сетка,
// а legacy-раскладка WDSP; FFreqs при этом не трогается, поэтому возврат к
// 10 полосам отдаёт пользователю его же кривую.
begin
if FBandCount = 3 then
Result := EQ3_F[EnsureRange(Index, 1, 3)]
else
Result := GridBandFreq(Index);
end;
procedure TEqualizerControl.BuildResponse;
// Порт eq_impulse (wdsp/eq.c) в дБ, без построения самого FIR. Точки F/G —
// узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и
// выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ
// бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как
// второй полосовой фильтр, и верхняя точка обязана лежать за краем TX-фильтра.
var
N, i, j, k, Lo, Hi: Integer;
Fp, Gp: array[0..11] of Double;
TF, TG, F, Frac, Preamp, MagDB, RefDB: Double;
begin
if Length(FRespDB) <> RESP_MID then SetLength(FRespDB, RESP_MID);
Preamp := FPreampGain;
if FBandCount = 3 then
begin
// Ровно то, что уходит в WDSP из PushTXEQProfile: 4 узла, полка на низах.
N := 4;
Fp[1] := 2.0 * 150.0 / RESP_RATE; Gp[1] := FGains[1];
Fp[2] := 2.0 * 400.0 / RESP_RATE; Gp[2] := FGains[1];
Fp[3] := 2.0 * 1500.0 / RESP_RATE; Gp[3] := FGains[2];
Fp[4] := 2.0 * 6000.0 / RESP_RATE; Gp[4] := FGains[3];
end
else
begin
N := EnsureRange(FBandCount, 1, 10);
for i := 1 to N do
begin
Fp[i] := EnsureRange(2.0 * InternalBandFreq(i) / RESP_RATE, 0.0, 1.0);
Gp[i] := FGains[i];
end;
end;
// WDSP сортирует точки по частоте (qsort) — порядок ручек значения не имеет.
for i := 1 to N - 1 do
for j := 1 to N - i do
if Fp[j] > Fp[j + 1] then
begin
TF := Fp[j]; Fp[j] := Fp[j + 1]; Fp[j + 1] := TF;
TG := Gp[j]; Gp[j] := Gp[j + 1]; Gp[j + 1] := TG;
end;
Fp[0] := 0.0; Gp[0] := Gp[1];
Fp[N + 1] := 1.0; Gp[N + 1] := Gp[N];
j := 0;
for i := 0 to RESP_MID - 1 do
begin
F := (i + 0.5) / RESP_MID;
while (j < N) and (F > Fp[j + 1]) do Inc(j);
if Fp[j + 1] > Fp[j] then Frac := (F - Fp[j]) / (Fp[j + 1] - Fp[j])
else Frac := 0.0;
FRespDB[i] := Frac * Gp[j + 1] + (1.0 - Frac) * Gp[j] + Preamp;
end;
// Скаты за крайними точками (ctfmode = 0, чётная ветка N). Множитель на бин
// — (f/f0)^4 в АМПЛИТУДЕ, в децибелах 80*log10(f/f0), и он накапливается.
Lo := Trunc(Fp[1] * RESP_MID - 0.5);
Hi := Trunc(Fp[N] * RESP_MID - 0.5);
if (Lo > 0) and (Lo < RESP_MID) then
begin
MagDB := FRespDB[Lo];
RefDB := 80.0 * Log10(Lo / RESP_MID);
for k := Lo - 1 downto 0 do
begin
if k > 0 then MagDB := MagDB + 80.0 * Log10(k / RESP_MID) - RefDB
else MagDB := RESP_FLOOR;
if MagDB < RESP_FLOOR then MagDB := RESP_FLOOR;
FRespDB[k] := MagDB;
end;
end;
if (Hi >= 0) and (Hi < RESP_MID - 1) then
begin
MagDB := FRespDB[Hi];
RefDB := 80.0 * Log10(Hi / RESP_MID);
for k := Hi + 1 to RESP_MID - 1 do
begin
MagDB := MagDB + RefDB - 80.0 * Log10(k / RESP_MID);
if MagDB < RESP_FLOOR then MagDB := RESP_FLOOR;
FRespDB[k] := MagDB;
end;
end;
end;
function TEqualizerControl.ResponseAtFreq(AFreq: Double): Double;
// Значение готовой АЧХ на частоте: линейная интерполяция между центрами бинов.
var
Pos: Double;
K: Integer;
begin
if Length(FRespDB) <> RESP_MID then BuildResponse;
Pos := AFreq / RESP_BIN_HZ - 0.5;
if Pos <= 0.0 then
Result := FRespDB[0]
else if Pos >= RESP_MID - 1 then
Result := FRespDB[RESP_MID - 1]
else
begin
K := Trunc(Pos);
Result := FRespDB[K] + (Pos - K) * (FRespDB[K + 1] - FRespDB[K]);
end;
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;
// Активные плитки подписаны рабочей частотой, погашенные — своим узлом
// сетки: иначе в 3-полосном режиме все семь показывали бы 6 кГц.
if i <= FBandCount then S := FormatFreq(InternalBandFreq(i))
else S := FormatFreq(GridBandFreq(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.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);
BuildResponse; // кривая считается заново на каждую отрисовку (1024 бина)
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);
// Клампа по ±15 дБ нет намеренно: за крайними точками WDSP обрывает АЧХ,
// и кривая обязана уходить в пол графика — иначе обрыв не виден.
GY := GainToY(G, 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(ResponseAtFreq(XToFreq(CurveL, R)), R);
Canvas.Pen.Color := FClrCurve;
Canvas.Pen.Width := 2;
for GX := CurveL + 1 to CurveR do
begin
F := XToFreq(GX, R);
GY := GainToY(ResponseAtFreq(F), 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);
// В 3-полосном режиме частоты задаёт WDSP (EQ3_F) — двигаем только гейн.
if FBandCount <> 3 then 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);
if FBandCount <> 3 then 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.