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.