mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
800 lines
25 KiB
ObjectPascal
800 lines
25 KiB
ObjectPascal
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.
|