text: /**********************************************************************
  
    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 "../Audacity.h"
  #include "ScienFilter.h"
  #include "LoadEffects.h"
  
  #include <math.h>
  #include <float.h>
  
  #include <wx/setup.h> // for wxUSE_* macros
  
  #include <wx/brush.h>
  #include <wx/choice.h>
  #include <wx/dcclient.h>
  #include <wx/dcmemory.h>
  #include <wx/intl.h>
  #include <wx/settings.h>
  #include <wx/slider.h>
  #include <wx/stattext.h>
  #include <wx/utils.h>
  #include <wx/valgen.h>
  
  #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");
  }
  
  wxString EffectScienFilter::ManualPage()
  {
     return wxT("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<wxGenericValidator>(&mFilterType)
              .MinSize( { -1, -1 } )
              .AddChoice(XXO("&Filter Type:"),
                 Msgids(kTypeStrings, nTypes)
              );
  
           mFilterOrderCtl = S.Id(ID_Order)
              .Validator<wxGenericValidator>(&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<FloatingPointValidator<float>>(
                 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<wxGenericValidator>(&mFilterSubtype)
              .MinSize( { -1, -1 } )
              .AddChoice(XXO("&Subtype:"),
                 Msgids(kSubTypeStrings, nSubTypes)
              );
  
           mCutoffCtl = S.Id(ID_Cutoff)
              .Name(XO("Cutoff (Hz)"))
              .Validator<FloatingPointValidator<float>>(
                 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<FloatingPointValidator<float>>(
                 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<wxBitmap>(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);
  }
  