/********************************************************************** Audacity: A Digital Audio Editor Effect/ScienFilter.cpp Norm C Mitch Golden Vaughan Johnson (Preview) *******************************************************************//** \file ScienFilter.cpp \brief Implements EffectScienFilter, EffectScienFilterPanel. *//****************************************************************//** \class EffectScienFilter \brief An Effect that applies 'classical' IIR filters. Performs IIR filtering that emulates analog filters, specifically Butterworth, Chebyshev Type I and Type II. Highpass and lowpass filters are supported, as are filter orders from 1 to 10. The filter is applied using biquads *//****************************************************************//** \class EffectScienFilterPanel \brief EffectScienFilterPanel is used with EffectScienFilter and controls a graph for EffectScienFilter. *//*******************************************************************/ #include "ScienFilter.h" #include "LoadEffects.h" #include #include #include // for wxUSE_* macros #include #include #include #include #include #include #include #include #include #include #include "../AColor.h" #include "../AllThemeResources.h" #include "../PlatformCompatibility.h" #include "../Prefs.h" #include "../Project.h" #include "../Shuttle.h" #include "../ShuttleGui.h" #include "../Theme.h" #include "../WaveTrack.h" #include "../widgets/valnum.h" #include "../widgets/AudacityMessageBox.h" #include "../widgets/Ruler.h" #include "../widgets/WindowAccessible.h" #if !defined(M_PI) #define PI = 3.1415926535897932384626433832795 #else #define PI M_PI #endif #define square(a) ((a)*(a)) enum { ID_FilterPanel = 10000, ID_dBMax, ID_dBMin, ID_Type, ID_SubType, ID_Order, ID_Ripple, ID_Cutoff, ID_StopbandRipple }; enum kTypes { kButterworth, kChebyshevTypeI, kChebyshevTypeII, nTypes }; static const EnumValueSymbol kTypeStrings[nTypes] = { /*i18n-hint: Butterworth is the name of the person after whom the filter type is named.*/ { XO("Butterworth") }, /*i18n-hint: Chebyshev is the name of the person after whom the filter type is named.*/ { XO("Chebyshev Type I") }, /*i18n-hint: Chebyshev is the name of the person after whom the filter type is named.*/ { XO("Chebyshev Type II") } }; enum kSubTypes { kLowPass = Biquad::kLowPass, kHighPass = Biquad::kHighPass, nSubTypes = Biquad::nSubTypes }; static const EnumValueSymbol kSubTypeStrings[nSubTypes] = { // These are acceptable dual purpose internal/visible names { XO("Lowpass") }, { XO("Highpass") } }; static_assert(nSubTypes == WXSIZEOF(kSubTypeStrings), "size mismatch"); // Define keys, defaults, minimums, and maximums for the effect parameters // // Name Type Key Def Min Max Scale Param( Type, int, wxT("FilterType"), kButterworth, 0, nTypes - 1, 1 ); Param( Subtype, int, wxT("FilterSubtype"), kLowPass, 0, nSubTypes - 1, 1 ); Param( Order, int, wxT("Order"), 1, 1, 10, 1 ); Param( Cutoff, float, wxT("Cutoff"), 1000.0, 1.0, FLT_MAX, 1 ); Param( Passband, float, wxT("PassbandRipple"), 1.0, 0.0, 100.0, 1 ); Param( Stopband, float, wxT("StopbandRipple"), 30.0, 0.0, 100.0, 1 ); //---------------------------------------------------------------------------- // EffectScienFilter //---------------------------------------------------------------------------- const ComponentInterfaceSymbol EffectScienFilter::Symbol { XO("Classic Filters") }; #ifdef EXPERIMENTAL_SCIENCE_FILTERS // true argument means don't automatically enable this effect namespace{ BuiltinEffectsModule::Registration< EffectScienFilter > reg( true ); } #endif BEGIN_EVENT_TABLE(EffectScienFilter, wxEvtHandler) EVT_SIZE(EffectScienFilter::OnSize) EVT_SLIDER(ID_dBMax, EffectScienFilter::OnSliderDBMAX) EVT_SLIDER(ID_dBMin, EffectScienFilter::OnSliderDBMIN) EVT_CHOICE(ID_Order, EffectScienFilter::OnOrder) EVT_CHOICE(ID_Type, EffectScienFilter::OnFilterType) EVT_CHOICE(ID_SubType, EffectScienFilter::OnFilterSubtype) EVT_TEXT(ID_Cutoff, EffectScienFilter::OnCutoff) EVT_TEXT(ID_Ripple, EffectScienFilter::OnRipple) EVT_TEXT(ID_StopbandRipple, EffectScienFilter::OnStopbandRipple) END_EVENT_TABLE() EffectScienFilter::EffectScienFilter() { mOrder = DEF_Order; mFilterType = DEF_Type; mFilterSubtype = DEF_Subtype; mCutoff = DEF_Cutoff; mRipple = DEF_Passband; mStopbandRipple = DEF_Stopband; SetLinearEffectFlag(true); mOrderIndex = mOrder - 1; mdBMin = -30.0; mdBMax = 30.0; mLoFreq = 20; // Lowest frequency to display in response graph mNyquist = 44100.0 / 2.0; // only used during initialization, updated when effect is used } EffectScienFilter::~EffectScienFilter() { } // ComponentInterface implementation ComponentInterfaceSymbol EffectScienFilter::GetSymbol() { return Symbol; } TranslatableString EffectScienFilter::GetDescription() { /* i18n-hint: "infinite impulse response" */ return XO("Performs IIR filtering that emulates analog filters"); } ManualPageID EffectScienFilter::ManualPage() { return L"Classic_Filters"; } // EffectDefinitionInterface implementation EffectType EffectScienFilter::GetType() { return EffectTypeProcess; } // EffectClientInterface implementation unsigned EffectScienFilter::GetAudioInCount() { return 1; } unsigned EffectScienFilter::GetAudioOutCount() { return 1; } bool EffectScienFilter::ProcessInitialize(sampleCount WXUNUSED(totalLen), ChannelNames WXUNUSED(chanMap)) { for (int iPair = 0; iPair < (mOrder + 1) / 2; iPair++) mpBiquad[iPair].Reset(); return true; } size_t EffectScienFilter::ProcessBlock(float **inBlock, float **outBlock, size_t blockLen) { float *ibuf = inBlock[0]; for (int iPair = 0; iPair < (mOrder + 1) / 2; iPair++) { mpBiquad[iPair].Process(ibuf, outBlock[0], blockLen); ibuf = outBlock[0]; } return blockLen; } bool EffectScienFilter::DefineParams( ShuttleParams & S ){ S.SHUTTLE_ENUM_PARAM( mFilterType, Type, kTypeStrings, nTypes ); S.SHUTTLE_ENUM_PARAM( mFilterSubtype, Subtype, kSubTypeStrings, nSubTypes ); S.SHUTTLE_PARAM( mOrder, Order ); S.SHUTTLE_PARAM( mCutoff, Cutoff ); S.SHUTTLE_PARAM( mRipple, Passband ); S.SHUTTLE_PARAM( mStopbandRipple, Stopband ); return true; } bool EffectScienFilter::GetAutomationParameters(CommandParameters & parms) { parms.Write(KEY_Type, kTypeStrings[mFilterType].Internal()); parms.Write(KEY_Subtype, kSubTypeStrings[mFilterSubtype].Internal()); parms.Write(KEY_Order, mOrder); parms.WriteFloat(KEY_Cutoff, mCutoff); parms.WriteFloat(KEY_Passband, mRipple); parms.WriteFloat(KEY_Stopband, mStopbandRipple); return true; } bool EffectScienFilter::SetAutomationParameters(CommandParameters & parms) { ReadAndVerifyEnum(Type, kTypeStrings, nTypes); ReadAndVerifyEnum(Subtype, kSubTypeStrings, nSubTypes); ReadAndVerifyInt(Order); ReadAndVerifyFloat(Cutoff); ReadAndVerifyFloat(Passband); ReadAndVerifyFloat(Stopband); mFilterType = Type; mFilterSubtype = Subtype; mOrder = Order; mCutoff = Cutoff; mRipple = Passband; mStopbandRipple = Stopband; mOrderIndex = mOrder - 1; CalcFilter(); return true; } // Effect implementation bool EffectScienFilter::Startup() { wxString base = wxT("/SciFilter/"); // Migrate settings from 2.1.0 or before // Already migrated, so bail if (gPrefs->Exists(base + wxT("Migrated"))) { return true; } // Load the old "current" settings if (gPrefs->Exists(base)) { double dTemp; gPrefs->Read(base + wxT("Order"), &mOrder, 1); mOrder = wxMax (1, mOrder); mOrder = wxMin (MAX_Order, mOrder); gPrefs->Read(base + wxT("FilterType"), &mFilterType, 0); mFilterType = wxMax (0, mFilterType); mFilterType = wxMin (2, mFilterType); gPrefs->Read(base + wxT("FilterSubtype"), &mFilterSubtype, 0); mFilterSubtype = wxMax (0, mFilterSubtype); mFilterSubtype = wxMin (1, mFilterSubtype); gPrefs->Read(base + wxT("Cutoff"), &dTemp, 1000.0); mCutoff = (float)dTemp; mCutoff = wxMax (1, mCutoff); mCutoff = wxMin (100000, mCutoff); gPrefs->Read(base + wxT("Ripple"), &dTemp, 1.0); mRipple = dTemp; mRipple = wxMax (0, mRipple); mRipple = wxMin (100, mRipple); gPrefs->Read(base + wxT("StopbandRipple"), &dTemp, 30.0); mStopbandRipple = dTemp; mStopbandRipple = wxMax (0, mStopbandRipple); mStopbandRipple = wxMin (100, mStopbandRipple); SaveUserPreset(GetCurrentSettingsGroup()); // Do not migrate again gPrefs->Write(base + wxT("Migrated"), true); gPrefs->Flush(); } return true; } bool EffectScienFilter::Init() { int selcount = 0; double rate = 0.0; auto trackRange = inputTracks()->Selected< const WaveTrack >(); { auto t = *trackRange.begin(); mNyquist = (t ? t->GetRate() : mProjectRate) / 2.0; } for (auto t : trackRange) { if (selcount == 0) { rate = t->GetRate(); } else { if (t->GetRate() != rate) { Effect::MessageBox( XO( "To apply a filter, all selected tracks must have the same sample rate.") ); return false; } } selcount++; } return true; } void EffectScienFilter::PopulateOrExchange(ShuttleGui & S) { S.AddSpace(5); S.SetSizerProportion(1); S.StartMultiColumn(3, wxEXPAND); { S.SetStretchyCol(1); S.SetStretchyRow(0); // ------------------------------------------------------------------- // ROW 1: Freq response panel and sliders for vertical scale // ------------------------------------------------------------------- S.StartVerticalLay(); { mdBRuler = safenew RulerPanel( S.GetParent(), wxID_ANY, wxVERTICAL, wxSize{ 100, 100 }, // Ruler can't handle small sizes RulerPanel::Range{ 30.0, -120.0 }, Ruler::LinearDBFormat, XO("dB"), RulerPanel::Options{} .LabelEdges(true) ); S.SetBorder(1); S.AddSpace(1, 1); S.Prop(1) .Position(wxALIGN_RIGHT | wxTOP) .AddWindow(mdBRuler); S.AddSpace(1, 1); } S.EndVerticalLay(); mPanel = safenew EffectScienFilterPanel( S.GetParent(), wxID_ANY, this, mLoFreq, mNyquist ); S.SetBorder(5); S.Prop(1) .Position(wxEXPAND | wxRIGHT) .MinSize( { -1, -1 } ) .AddWindow(mPanel); S.StartVerticalLay(); { S.AddVariableText(XO("+ dB"), false, wxCENTER); mdBMaxSlider = S.Id(ID_dBMax) .Name(XO("Max dB")) .Style(wxSL_VERTICAL | wxSL_INVERSE) .AddSlider( {}, 10, 20, 0); #if wxUSE_ACCESSIBILITY mdBMaxSlider->SetAccessible(safenew SliderAx(mdBMaxSlider, XO("%d dB"))); #endif mdBMinSlider = S.Id(ID_dBMin) .Name(XO("Min dB")) .Style(wxSL_VERTICAL | wxSL_INVERSE) .AddSlider( {}, -10, -10, -120); #if wxUSE_ACCESSIBILITY mdBMinSlider->SetAccessible(safenew SliderAx(mdBMinSlider, XO("%d dB"))); #endif S.AddVariableText(XO("- dB"), false, wxCENTER); } S.EndVerticalLay(); // ------------------------------------------------------------------- // ROW 2: Frequency ruler // ------------------------------------------------------------------- S.AddSpace(1, 1); mfreqRuler = safenew RulerPanel( S.GetParent(), wxID_ANY, wxHORIZONTAL, wxSize{ 100, 100 }, // Ruler can't handle small sizes RulerPanel::Range{ mLoFreq, mNyquist }, Ruler::IntFormat, {}, RulerPanel::Options{} .Log(true) .Flip(true) .LabelEdges(true) ); S.Prop(1) .Position(wxEXPAND | wxALIGN_LEFT | wxRIGHT) .AddWindow(mfreqRuler); S.AddSpace(1, 1); // ------------------------------------------------------------------- // ROW 3 and 4: Type, Order, Ripple, Subtype, Cutoff // ------------------------------------------------------------------- S.AddSpace(1, 1); S.SetSizerProportion(0); S.StartMultiColumn(8, wxALIGN_CENTER); { wxASSERT(nTypes == WXSIZEOF(kTypeStrings)); mFilterTypeCtl = S.Id(ID_Type) .Focus() .Validator(&mFilterType) .MinSize( { -1, -1 } ) .AddChoice(XXO("&Filter Type:"), Msgids(kTypeStrings, nTypes) ); mFilterOrderCtl = S.Id(ID_Order) .Validator(&mOrderIndex) .MinSize( { -1, -1 } ) /*i18n-hint: 'Order' means the complexity of the filter, and is a number between 1 and 10.*/ .AddChoice(XXO("O&rder:"), []{ TranslatableStrings orders; for (int i = 1; i <= 10; i++) orders.emplace_back( Verbatim("%d").Format( i ) ); return orders; }() ); S.AddSpace(1, 1); mRippleCtlP = S.AddVariableText( XO("&Passband Ripple:"), false, wxALL | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); mRippleCtl = S.Id(ID_Ripple) .Name(XO("Passband Ripple (dB)")) .Validator>( 1, &mRipple, NumValidatorStyle::DEFAULT, MIN_Passband, MAX_Passband) .AddTextBox( {}, wxT(""), 10); mRippleCtlU = S.AddVariableText(XO("dB"), false, wxALL | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); mFilterSubTypeCtl = S.Id(ID_SubType) .Validator(&mFilterSubtype) .MinSize( { -1, -1 } ) .AddChoice(XXO("&Subtype:"), Msgids(kSubTypeStrings, nSubTypes) ); mCutoffCtl = S.Id(ID_Cutoff) .Name(XO("Cutoff (Hz)")) .Validator>( 1, &mCutoff, NumValidatorStyle::DEFAULT, MIN_Cutoff, mNyquist - 1) .AddTextBox(XXO("C&utoff:"), wxT(""), 10); S.AddUnits(XO("Hz")); mStopbandRippleCtlP = S.AddVariableText(XO("Minimum S&topband Attenuation:"), false, wxALL | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); mStopbandRippleCtl = S.Id(ID_StopbandRipple) .Name(XO("Minimum S&topband Attenuation (dB)")) .Validator>( 1, &mStopbandRipple, NumValidatorStyle::DEFAULT, MIN_Stopband, MAX_Stopband) .AddTextBox( {}, wxT(""), 10); mStopbandRippleCtlU = S.AddVariableText(XO("dB"), false, wxALL | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); } S.EndMultiColumn(); S.AddSpace(1, 1); } S.EndMultiColumn(); return; } // // Populate the window with relevant variables // bool EffectScienFilter::TransferDataToWindow() { mOrderIndex = mOrder - 1; if (!mUIParent->TransferDataToWindow()) { return false; } mdBMinSlider->SetValue((int) mdBMin); mdBMin = 0.0; // force refresh in TransferGraphLimitsFromWindow() mdBMaxSlider->SetValue((int) mdBMax); mdBMax = 0.0; // force refresh in TransferGraphLimitsFromWindow() EnableDisableRippleCtl(mFilterType); return TransferGraphLimitsFromWindow(); } bool EffectScienFilter::TransferDataFromWindow() { if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow()) { return false; } mOrder = mOrderIndex + 1; CalcFilter(); return true; } // EffectScienFilter implementation // // Retrieve data from the window // bool EffectScienFilter::TransferGraphLimitsFromWindow() { // Read the sliders and send to the panel wxString tip; bool rr = false; int dB = mdBMinSlider->GetValue(); if (dB != mdBMin) { rr = true; mdBMin = dB; tip.Printf(_("%d dB"), (int)mdBMin); mdBMinSlider->SetToolTip(tip); } dB = mdBMaxSlider->GetValue(); if (dB != mdBMax) { rr = true; mdBMax = dB; tip.Printf(_("%d dB"),(int)mdBMax); mdBMaxSlider->SetToolTip(tip); } if (rr) { mPanel->SetDbRange(mdBMin, mdBMax); } // Refresh ruler if values have changed if (rr) { int w1, w2, h; mdBRuler->ruler.GetMaxSize(&w1, &h); mdBRuler->ruler.SetRange(mdBMax, mdBMin); mdBRuler->ruler.GetMaxSize(&w2, &h); if( w1 != w2 ) // Reduces flicker { mdBRuler->SetSize(wxSize(w2,h)); mUIParent->Layout(); mfreqRuler->Refresh(false); } mdBRuler->Refresh(false); } mPanel->Refresh(false); return true; } void EffectScienFilter::CalcFilter() { switch (mFilterType) { case kButterworth: mpBiquad = Biquad::CalcButterworthFilter(mOrder, mNyquist, mCutoff, mFilterSubtype); break; case kChebyshevTypeI: mpBiquad = Biquad::CalcChebyshevType1Filter(mOrder, mNyquist, mCutoff, mRipple, mFilterSubtype); break; case kChebyshevTypeII: mpBiquad = Biquad::CalcChebyshevType2Filter(mOrder, mNyquist, mCutoff, mStopbandRipple, mFilterSubtype); break; } } float EffectScienFilter::FilterMagnAtFreq(float Freq) { float Magn; if (Freq >= mNyquist) Freq = mNyquist - 1; // prevent tan(PI/2) float FreqWarped = tan (PI * Freq/(2*mNyquist)); if (mCutoff >= mNyquist) mCutoff = mNyquist - 1; float CutoffWarped = tan (PI * mCutoff/(2*mNyquist)); float fOverflowThresh = pow (10.0, 12.0 / (2*mOrder)); // once we exceed 10^12 there's not much to be gained and overflow could happen switch (mFilterType) { case kButterworth: // Butterworth default: switch (mFilterSubtype) { case kLowPass: // lowpass default: if (FreqWarped/CutoffWarped > fOverflowThresh) // prevent pow() overflow Magn = 0; else Magn = sqrt (1 / (1 + pow (FreqWarped/CutoffWarped, 2*mOrder))); break; case kHighPass: // highpass if (FreqWarped/CutoffWarped > fOverflowThresh) Magn = 1; else Magn = sqrt (pow (FreqWarped/CutoffWarped, 2*mOrder) / (1 + pow (FreqWarped/CutoffWarped, 2*mOrder))); break; } break; case kChebyshevTypeI: // Chebyshev Type 1 double eps; eps = sqrt(pow (10.0, wxMax(0.001, mRipple)/10.0) - 1); double chebyPolyVal; switch (mFilterSubtype) { case 0: // lowpass default: chebyPolyVal = Biquad::ChebyPoly(mOrder, FreqWarped/CutoffWarped); Magn = sqrt (1 / (1 + square(eps) * square(chebyPolyVal))); break; case 1: chebyPolyVal = Biquad::ChebyPoly(mOrder, CutoffWarped/FreqWarped); Magn = sqrt (1 / (1 + square(eps) * square(chebyPolyVal))); break; } break; case kChebyshevTypeII: // Chebyshev Type 2 eps = 1 / sqrt(pow (10.0, wxMax(0.001, mStopbandRipple)/10.0) - 1); switch (mFilterSubtype) { case kLowPass: // lowpass default: chebyPolyVal = Biquad::ChebyPoly(mOrder, CutoffWarped/FreqWarped); Magn = sqrt (1 / (1 + 1 / (square(eps) * square(chebyPolyVal)))); break; case kHighPass: chebyPolyVal = Biquad::ChebyPoly(mOrder, FreqWarped/CutoffWarped); Magn = sqrt (1 / (1 + 1 / (square(eps) * square(chebyPolyVal)))); break; } break; } return Magn; } void EffectScienFilter::OnOrder(wxCommandEvent & WXUNUSED(evt)) { mOrderIndex = mFilterOrderCtl->GetSelection(); mOrder = mOrderIndex + 1; // 0..n-1 -> 1..n mPanel->Refresh(false); } void EffectScienFilter::OnFilterType(wxCommandEvent & WXUNUSED(evt)) { mFilterType = mFilterTypeCtl->GetSelection(); EnableDisableRippleCtl(mFilterType); mPanel->Refresh(false); } void EffectScienFilter::OnFilterSubtype(wxCommandEvent & WXUNUSED(evt)) { mFilterSubtype = mFilterSubTypeCtl->GetSelection(); mPanel->Refresh(false); } void EffectScienFilter::OnCutoff(wxCommandEvent & WXUNUSED(evt)) { if (!EnableApply(mUIParent->TransferDataFromWindow())) { return; } mPanel->Refresh(false); } void EffectScienFilter::OnRipple(wxCommandEvent & WXUNUSED(evt)) { if (!EnableApply(mUIParent->TransferDataFromWindow())) { return; } mPanel->Refresh(false); } void EffectScienFilter::OnStopbandRipple(wxCommandEvent & WXUNUSED(evt)) { if (!EnableApply(mUIParent->TransferDataFromWindow())) { return; } mPanel->Refresh(false); } void EffectScienFilter::OnSliderDBMIN(wxCommandEvent & WXUNUSED(evt)) { TransferGraphLimitsFromWindow(); } void EffectScienFilter::OnSliderDBMAX(wxCommandEvent & WXUNUSED(evt)) { TransferGraphLimitsFromWindow(); } void EffectScienFilter::OnSize(wxSizeEvent & evt) { // On Windows the Passband and Stopband boxes do not refresh properly // on a resize...no idea why. mUIParent->Refresh(); evt.Skip(); } void EffectScienFilter::EnableDisableRippleCtl(int FilterType) { bool ripple; bool stop; if (FilterType == kButterworth) // Butterworth { ripple = false; stop = false; } else if (FilterType == kChebyshevTypeI) // Chebyshev Type1 { ripple = true; stop = false; } else // Chebyshev Type2 { ripple = false; stop = true; } mRippleCtlP->Enable(ripple); mRippleCtl->Enable(ripple); mRippleCtlU->Enable(ripple); mStopbandRippleCtlP->Enable(stop); mStopbandRippleCtl->Enable(stop); mStopbandRippleCtlU->Enable(stop); } //---------------------------------------------------------------------------- // EffectScienFilterPanel //---------------------------------------------------------------------------- BEGIN_EVENT_TABLE(EffectScienFilterPanel, wxPanelWrapper) EVT_PAINT(EffectScienFilterPanel::OnPaint) EVT_SIZE(EffectScienFilterPanel::OnSize) END_EVENT_TABLE() EffectScienFilterPanel::EffectScienFilterPanel( wxWindow *parent, wxWindowID winid, EffectScienFilter *effect, double lo, double hi) : wxPanelWrapper(parent, winid, wxDefaultPosition, wxSize(400, 200)) { mEffect = effect; mParent = parent; mBitmap = NULL; mWidth = 0; mHeight = 0; mLoFreq = 0.0; mHiFreq = 0.0; mDbMin = 0.0; mDbMax = 0.0; SetFreqRange(lo, hi); } EffectScienFilterPanel::~EffectScienFilterPanel() { } void EffectScienFilterPanel::SetFreqRange(double lo, double hi) { mLoFreq = lo; mHiFreq = hi; Refresh(false); } void EffectScienFilterPanel::SetDbRange(double min, double max) { mDbMin = min; mDbMax = max; Refresh(false); } bool EffectScienFilterPanel::AcceptsFocus() const { return false; } bool EffectScienFilterPanel::AcceptsFocusFromKeyboard() const { return false; } void EffectScienFilterPanel::OnSize(wxSizeEvent & WXUNUSED(evt)) { Refresh(false); } void EffectScienFilterPanel::OnPaint(wxPaintEvent & WXUNUSED(evt)) { wxPaintDC dc(this); int width, height; GetSize(&width, &height); if (!mBitmap || mWidth != width || mHeight != height) { mWidth = width; mHeight = height; mBitmap = std::make_unique(mWidth, mHeight,24); } wxBrush bkgndBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE)); wxMemoryDC memDC; memDC.SelectObject(*mBitmap); wxRect bkgndRect; bkgndRect.x = 0; bkgndRect.y = 0; bkgndRect.width = mWidth; bkgndRect.height = mHeight; memDC.SetBrush(bkgndBrush); memDC.SetPen(*wxTRANSPARENT_PEN); memDC.DrawRectangle(bkgndRect); bkgndRect.y = mHeight; memDC.DrawRectangle(bkgndRect); wxRect border; border.x = 0; border.y = 0; border.width = mWidth; border.height = mHeight; memDC.SetBrush(*wxWHITE_BRUSH); memDC.SetPen(*wxBLACK_PEN); memDC.DrawRectangle(border); mEnvRect = border; mEnvRect.Deflate(2, 2); // Pure blue x-axis line memDC.SetPen(wxPen(theTheme.Colour(clrGraphLines), 1, wxPENSTYLE_SOLID)); int center = (int) (mEnvRect.height * mDbMax / (mDbMax - mDbMin) + 0.5); AColor::Line(memDC, mEnvRect.GetLeft(), mEnvRect.y + center, mEnvRect.GetRight(), mEnvRect.y + center); //Now draw the actual response that you will get. //mFilterFunc has a linear scale, window has a log one so we have to fiddle about memDC.SetPen(wxPen(theTheme.Colour(clrResponseLines), 3, wxPENSTYLE_SOLID)); double scale = (double) mEnvRect.height / (mDbMax - mDbMin); // pixels per dB double yF; // gain at this freq double loLog = log10(mLoFreq); double step = log10(mHiFreq) - loLog; step /= ((double) mEnvRect.width - 1.0); double freq; // actual freq corresponding to x position int x, y, xlast = 0, ylast = 0; for (int i = 0; i < mEnvRect.width; i++) { x = mEnvRect.x + i; freq = pow(10.0, loLog + i * step); //Hz yF = mEffect->FilterMagnAtFreq (freq); yF = LINEAR_TO_DB(yF); if (yF < mDbMin) { yF = mDbMin; } yF = center-scale * yF; if (yF > mEnvRect.height) { yF = (double) mEnvRect.height - 1.0; } if (yF < 0.0) { yF = 0.0; } y = (int) (yF + 0.5); if (i != 0 && (y < mEnvRect.height - 1 || ylast < mEnvRect.y + mEnvRect.height - 1)) { AColor::Line(memDC, xlast, ylast, x, mEnvRect.y + y); } xlast = x; ylast = mEnvRect.y + y; } memDC.SetPen(*wxBLACK_PEN); mEffect->mfreqRuler->ruler.DrawGrid(memDC, mEnvRect.height + 2, true, true, 0, 1); mEffect->mdBRuler->ruler.DrawGrid(memDC, mEnvRect.width + 2, true, true, 1, 2); dc.Blit(0, 0, mWidth, mHeight, &memDC, 0, 0, wxCOPY, FALSE); memDC.SelectObject(wxNullBitmap); }