audacia/src/effects/ScienFilter.cpp

1173 lines
36 KiB
C++

/**********************************************************************
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 <math.h>
#include <float.h>
#include <wx/brush.h>
#include <wx/dcmemory.h>
#include <wx/intl.h>
#include <wx/msgdlg.h>
#include <wx/settings.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 "../ShuttleGui.h"
#include "../Theme.h"
#include "../WaveTrack.h"
#include "../widgets/valnum.h"
#include "Equalization.h" // For SliderAx
#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,
kNumTypes
};
static const wxChar *kTypeStrings[] =
{
/*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,
kHighPass,
kNumSubTypes
};
static const wxChar *kSubTypeStrings[] =
{
XO("Lowpass"),
XO("Highpass")
};
// Define keys, defaults, minimums, and maximums for the effect parameters
//
// Name Type Key Def Min Max Scale
Param( Type, int, XO("FilterType"), kButterworth, 0, kNumTypes - 1, 1 );
Param( Subtype, int, XO("FilterSubtype"), kLowPass, 0, kNumSubTypes - 1, 1 );
Param( Order, int, XO("Order"), 1, 1, 10, 1 );
Param( Cutoff, float, XO("Cutoff"), 1000.0, 1.0, FLT_MAX, 1 );
Param( Passband, float, XO("PassbandRipple"), 1.0, 0.0, 100.0, 1 );
Param( Stopband, float, XO("StopbandRipple"), 30.0, 0.0, 100.0, 1 );
static const double s_fChebyCoeffs[MAX_Order][MAX_Order + 1] =
{
// For Chebyshev polynomials of the first kind (see http://en.wikipedia.org/wiki/Chebyshev_polynomial)
// Coeffs are in the order 0, 1, 2...9
{ 0, 1}, // order 1
{-1, 0, 2}, // order 2 etc.
{ 0, -3, 0, 4},
{ 1, 0, -8, 0, 8},
{ 0, 5, 0, -20, 0, 16},
{-1, 0, 18, 0, -48, 0, 32},
{ 0, -7, 0, 56, 0, -112, 0, 64},
{ 1, 0, -32, 0, 160, 0, -256, 0, 128},
{ 0, 9, 0, -120, 0, 432, 0, -576, 0, 256},
{-1, 0, 50, 0, -400, 0, 1120, 0, -1280, 0, 512}
};
//----------------------------------------------------------------------------
// EffectScienFilter
//----------------------------------------------------------------------------
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
mpBiquad = new BiquadStruct[MAX_Order / 2];
memset(mpBiquad, 0, sizeof(BiquadStruct) * MAX_Order / 2);
for (int i = 0; i < MAX_Order / 2; i++)
{
mpBiquad[i].fNumerCoeffs[0] = 1.0; // straight-through
}
}
EffectScienFilter::~EffectScienFilter()
{
delete [] mpBiquad;
}
// IdentInterface implementation
wxString EffectScienFilter::GetSymbol()
{
return CLASSICFILTERS_PLUGIN_SYMBOL;
}
wxString EffectScienFilter::GetDescription()
{
return XO("Performs IIR filtering that emulates analog filters");
}
// EffectIdentInterface implementation
EffectType EffectScienFilter::GetType()
{
return EffectTypeProcess;
}
// EffectClientInterface implementation
int EffectScienFilter::GetAudioInCount()
{
return 1;
}
int EffectScienFilter::GetAudioOutCount()
{
return 1;
}
bool EffectScienFilter::ProcessInitialize(sampleCount WXUNUSED(totalLen), ChannelNames WXUNUSED(chanMap))
{
for (int iPair = 0; iPair < (mOrder + 1) / 2; iPair++)
{
mpBiquad[iPair].fPrevIn = 0;
mpBiquad[iPair].fPrevPrevIn = 0;
mpBiquad[iPair].fPrevOut = 0;
mpBiquad[iPair].fPrevPrevOut = 0;
}
return true;
}
sampleCount EffectScienFilter::ProcessBlock(float **inBlock, float **outBlock, sampleCount blockLen)
{
float *ibuf = inBlock[0];
for (int iPair = 0; iPair < (mOrder + 1) / 2; iPair++)
{
mpBiquad[iPair].pfIn = ibuf;
mpBiquad[iPair].pfOut = outBlock[0];
Biquad_Process(&mpBiquad[iPair], blockLen);
ibuf = outBlock[0];
}
return blockLen;
}
bool EffectScienFilter::GetAutomationParameters(EffectAutomationParameters & parms)
{
parms.Write(KEY_Type, kTypeStrings[mFilterType]);
parms.Write(KEY_Subtype, kSubTypeStrings[mFilterSubtype]);
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(EffectAutomationParameters & parms)
{
ReadAndVerifyEnum(Type, wxArrayString(kNumTypes, kTypeStrings));
ReadAndVerifyEnum(Subtype, wxArrayString(kNumSubTypes, kSubTypeStrings));
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;
TrackListOfKindIterator iter(Track::Wave, mTracks);
WaveTrack *t = (WaveTrack *) iter.First();
mNyquist = (t ? t->GetRate() : GetActiveProject()->GetRate()) / 2.0;
while (t)
{
if (t->GetSelected() && t->GetKind() == Track::Wave)
{
if (selcount == 0)
{
rate = t->GetRate();
}
else
{
if (t->GetRate() != rate)
{
wxMessageBox(_("To apply a filter, all selected tracks must have the same sample rate."));
return false;
}
}
selcount++;
}
t = (WaveTrack *) iter.Next();
}
return true;
}
void EffectScienFilter::PopulateOrExchange(ShuttleGui & S)
{
wxWindow *const parent = S.GetParent();
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(parent, wxID_ANY);
mdBRuler->ruler.SetBounds(0, 0, 100, 100); // Ruler can't handle small sizes
mdBRuler->ruler.SetOrientation(wxVERTICAL);
mdBRuler->ruler.SetRange(30.0, -120.0);
mdBRuler->ruler.SetFormat(Ruler::LinearDBFormat);
mdBRuler->ruler.SetUnits(_("dB"));
mdBRuler->ruler.SetLabelEdges(true);
int w;
mdBRuler->ruler.GetMaxSize(&w, NULL);
mdBRuler->SetSize(wxSize(w, 150)); // height needed for wxGTK
S.SetBorder(1);
S.AddSpace(1, 1);
S.Prop(1);
S.AddWindow(mdBRuler, wxALIGN_RIGHT | wxTOP);
S.AddSpace(1, 1);
}
S.EndVerticalLay();
mPanel = safenew EffectScienFilterPanel(this, parent);
mPanel->SetFreqRange(mLoFreq, mNyquist);
S.SetBorder(5);
S.Prop(1);
S.AddWindow(mPanel, wxEXPAND | wxRIGHT);
S.SetSizeHints(-1, -1);
S.StartVerticalLay();
{
S.AddVariableText(_("+ dB"), false, wxCENTER);
S.SetStyle(wxSL_VERTICAL | wxSL_INVERSE);
mdBMaxSlider = S.Id(ID_dBMax).AddSlider(wxT(""), 10, 20, 0);
#if wxUSE_ACCESSIBILITY
mdBMaxSlider->SetName(_("Max dB"));
mdBMaxSlider->SetAccessible(safenew SliderAx(mdBMaxSlider, wxString(wxT("%d ")) + _("dB")));
#endif
S.SetStyle(wxSL_VERTICAL | wxSL_INVERSE);
mdBMinSlider = S.Id(ID_dBMin).AddSlider(wxT(""), -10, -10, -120);
S.AddVariableText(_("- dB"), false, wxCENTER);
#if wxUSE_ACCESSIBILITY
mdBMinSlider->SetName(_("Min dB"));
mdBMinSlider->SetAccessible(safenew SliderAx(mdBMinSlider, wxString(wxT("%d ")) + _("dB")));
#endif
}
S.EndVerticalLay();
// -------------------------------------------------------------------
// ROW 2: Frequency ruler
// -------------------------------------------------------------------
S.AddSpace(1, 1);
mfreqRuler = safenew RulerPanel(parent, wxID_ANY);
mfreqRuler->ruler.SetBounds(0, 0, 100, 100); // Ruler can't handle small sizes
mfreqRuler->ruler.SetOrientation(wxHORIZONTAL);
mfreqRuler->ruler.SetLog(true);
mfreqRuler->ruler.SetRange(mLoFreq, mNyquist);
mfreqRuler->ruler.SetFormat(Ruler::IntFormat);
mfreqRuler->ruler.SetUnits(wxT(""));
mfreqRuler->ruler.SetFlip(true);
mfreqRuler->ruler.SetLabelEdges(true);
int h;
mfreqRuler->ruler.GetMaxSize(NULL, &h);
mfreqRuler->SetMinSize(wxSize(-1, h));
S.Prop(1);
S.AddWindow(mfreqRuler, wxEXPAND | wxALIGN_LEFT | wxRIGHT);
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(kNumTypes == WXSIZEOF(kTypeStrings));
wxArrayString typeChoices;
for (int i = 0; i < kNumTypes; i++)
{
typeChoices.Add(wxGetTranslation(kTypeStrings[i]));
}
mFilterTypeCtl = S.Id(ID_Type).AddChoice(_("&Filter Type:"), wxT(""), &typeChoices);
mFilterTypeCtl->SetValidator(wxGenericValidator(&mFilterType));
S.SetSizeHints(-1, -1);
wxArrayString orders;
for (int i = 1; i <= 10; i++)
{
orders.Add(wxString::Format(wxT("%d"), i));
}
/*i18n-hint: 'Order' means the complexity of the filter, and is a number between 1 and 10.*/
mFilterOrderCtl = S.Id(ID_Order).AddChoice(_("O&rder:"), wxT(""), &orders);
mFilterOrderCtl->SetValidator(wxGenericValidator(&mOrderIndex));
S.SetSizeHints(-1, -1);
S.AddSpace(1, 1);
FloatingPointValidator<float> vldRipple(1, &mRipple);
vldRipple.SetRange(MIN_Passband, MAX_Passband);
mRippleCtlP = S.AddVariableText(_("&Passband Ripple:"), false, wxALL | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
mRippleCtl = S.Id(ID_Ripple).AddTextBox(wxT(""), wxT(""), 10);
mRippleCtl->SetName(_("Passband Ripple (dB)"));
mRippleCtl->SetValidator(vldRipple);
mRippleCtlU = S.AddVariableText(_("dB"), false, wxALL | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
wxASSERT(kNumSubTypes == WXSIZEOF(kSubTypeStrings));
wxArrayString subTypeChoices;
for (int i = 0; i < kNumSubTypes; i++)
{
subTypeChoices.Add(wxGetTranslation(kSubTypeStrings[i]));
}
mFilterSubTypeCtl = S.Id(ID_SubType).AddChoice(_("&Subtype:"), wxT(""), &subTypeChoices);
mFilterSubTypeCtl->SetValidator(wxGenericValidator(&mFilterSubtype));
S.SetSizeHints(-1, -1);
FloatingPointValidator<float> vldCutoff(1, &mCutoff);
vldCutoff.SetRange(MIN_Cutoff, mNyquist - 1);
mCutoffCtl = S.Id(ID_Cutoff).AddTextBox(_("C&utoff:"), wxT(""), 10);
mCutoffCtl->SetName(_("Cutoff (Hz)"));
mCutoffCtl->SetValidator(vldCutoff);
S.AddUnits(_("Hz"));
FloatingPointValidator<float> vldStopbandRipple(1, &mStopbandRipple);
vldStopbandRipple.SetRange(MIN_Stopband, MAX_Stopband);
mStopbandRippleCtlP = S.AddVariableText(_("Minimum S&topband Attenuation:"), false, wxALL | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
mStopbandRippleCtl = S.Id(ID_StopbandRipple).AddTextBox(wxT(""), wxT(""), 10);
mStopbandRippleCtl->SetName(_("Minimum S&topband Attenuation (dB)"));
mStopbandRippleCtl->SetValidator(vldStopbandRipple);
mStopbandRippleCtlU = S.AddVariableText(_("dB"), false, wxALL | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
}
S.EndMultiColumn();
S.AddSpace(1, 1);
}
S.EndMultiColumn();
mFilterTypeCtl->SetFocus();
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(wxString(wxT("%d ")) + _("dB"), (int)mdBMin);
mdBMinSlider->SetToolTip(tip);
}
dB = mdBMaxSlider->GetValue();
if (dB != mdBMax) {
rr = true;
mdBMax = dB;
tip.Printf(wxString(wxT("%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;
}
bool EffectScienFilter::CalcFilter()
{
// Set up the coefficients in all the biquads
float fNorm = mCutoff / mNyquist;
if (fNorm >= 0.9999)
fNorm = 0.9999F;
float fC = tan (PI * fNorm / 2);
float fDCPoleDistSqr = 1.0F;
float fZPoleX, fZPoleY;
float fZZeroX, fZZeroY;
float beta = cos (fNorm*PI);
switch (mFilterType)
{
case kButterworth: // Butterworth
if ((mOrder & 1) == 0)
{
// Even order
for (int iPair = 0; iPair < mOrder/2; iPair++)
{
float fSPoleX = fC * cos (PI - (iPair + 0.5) * PI / mOrder);
float fSPoleY = fC * sin (PI - (iPair + 0.5) * PI / mOrder);
BilinTransform (fSPoleX, fSPoleY, &fZPoleX, &fZPoleY);
mpBiquad[iPair].fNumerCoeffs [0] = 1;
if (mFilterSubtype == kLowPass) // LOWPASS
mpBiquad[iPair].fNumerCoeffs [1] = 2;
else
mpBiquad[iPair].fNumerCoeffs [1] = -2;
mpBiquad[iPair].fNumerCoeffs [2] = 1;
mpBiquad[iPair].fDenomCoeffs [0] = -2 * fZPoleX;
mpBiquad[iPair].fDenomCoeffs [1] = square(fZPoleX) + square(fZPoleY);
if (mFilterSubtype == kLowPass) // LOWPASS
fDCPoleDistSqr *= Calc2D_DistSqr (1, 0, fZPoleX, fZPoleY);
else
fDCPoleDistSqr *= Calc2D_DistSqr (-1, 0, fZPoleX, fZPoleY); // distance from Nyquist
}
}
else
{
// Odd order - first do the 1st-order section
float fSPoleX = -fC;
float fSPoleY = 0;
BilinTransform (fSPoleX, fSPoleY, &fZPoleX, &fZPoleY);
mpBiquad[0].fNumerCoeffs [0] = 1;
if (mFilterSubtype == kLowPass) // LOWPASS
mpBiquad[0].fNumerCoeffs [1] = 1;
else
mpBiquad[0].fNumerCoeffs [1] = -1;
mpBiquad[0].fNumerCoeffs [2] = 0;
mpBiquad[0].fDenomCoeffs [0] = -fZPoleX;
mpBiquad[0].fDenomCoeffs [1] = 0;
if (mFilterSubtype == kLowPass) // LOWPASS
fDCPoleDistSqr = 1 - fZPoleX;
else
fDCPoleDistSqr = fZPoleX + 1; // dist from Nyquist
for (int iPair = 1; iPair <= mOrder/2; iPair++)
{
float fSPoleX = fC * cos (PI - iPair * PI / mOrder);
float fSPoleY = fC * sin (PI - iPair * PI / mOrder);
BilinTransform (fSPoleX, fSPoleY, &fZPoleX, &fZPoleY);
mpBiquad[iPair].fNumerCoeffs [0] = 1;
if (mFilterSubtype == kLowPass) // LOWPASS
mpBiquad[iPair].fNumerCoeffs [1] = 2;
else
mpBiquad[iPair].fNumerCoeffs [1] = -2;
mpBiquad[iPair].fNumerCoeffs [2] = 1;
mpBiquad[iPair].fDenomCoeffs [0] = -2 * fZPoleX;
mpBiquad[iPair].fDenomCoeffs [1] = square(fZPoleX) + square(fZPoleY);
if (mFilterSubtype == kLowPass) // LOWPASS
fDCPoleDistSqr *= Calc2D_DistSqr (1, 0, fZPoleX, fZPoleY);
else
fDCPoleDistSqr *= Calc2D_DistSqr (-1, 0, fZPoleX, fZPoleY); // distance from Nyquist
}
}
mpBiquad[0].fNumerCoeffs [0] *= fDCPoleDistSqr / (1 << mOrder); // mult by DC dist from poles, divide by dist from zeroes
mpBiquad[0].fNumerCoeffs [1] *= fDCPoleDistSqr / (1 << mOrder);
mpBiquad[0].fNumerCoeffs [2] *= fDCPoleDistSqr / (1 << mOrder);
break;
case kChebyshevTypeI: // Chebyshev Type 1
double eps; eps = sqrt (pow (10.0, wxMax(0.001, mRipple) / 10.0) - 1);
double a; a = log (1 / eps + sqrt(1 / square(eps) + 1)) / mOrder;
// Assume even order to start
for (int iPair = 0; iPair < mOrder/2; iPair++)
{
float fSPoleX = -fC * sinh (a) * sin ((2*iPair + 1) * PI / (2 * mOrder));
float fSPoleY = fC * cosh (a) * cos ((2*iPair + 1) * PI / (2 * mOrder));
BilinTransform (fSPoleX, fSPoleY, &fZPoleX, &fZPoleY);
if (mFilterSubtype == kLowPass) // LOWPASS
{
fZZeroX = -1;
fDCPoleDistSqr = Calc2D_DistSqr (1, 0, fZPoleX, fZPoleY);
fDCPoleDistSqr /= 2*2; // dist from zero at Nyquist
}
else
{
// Highpass - do the digital LP->HP transform on the poles and zeroes
ComplexDiv (beta - fZPoleX, -fZPoleY, 1 - beta * fZPoleX, -beta * fZPoleY, &fZPoleX, &fZPoleY);
fZZeroX = 1;
fDCPoleDistSqr = Calc2D_DistSqr (-1, 0, fZPoleX, fZPoleY); // distance from Nyquist
fDCPoleDistSqr /= 2*2; // dist from zero at Nyquist
}
mpBiquad[iPair].fNumerCoeffs [0] = fDCPoleDistSqr;
mpBiquad[iPair].fNumerCoeffs [1] = -2 * fZZeroX * fDCPoleDistSqr;
mpBiquad[iPair].fNumerCoeffs [2] = fDCPoleDistSqr;
mpBiquad[iPair].fDenomCoeffs [0] = -2 * fZPoleX;
mpBiquad[iPair].fDenomCoeffs [1] = square(fZPoleX) + square(fZPoleY);
}
if ((mOrder & 1) == 0)
{
float fTemp = DB_TO_LINEAR(-wxMax(0.001, mRipple)); // at DC the response is down R dB (for even-order)
mpBiquad[0].fNumerCoeffs [0] *= fTemp;
mpBiquad[0].fNumerCoeffs [1] *= fTemp;
mpBiquad[0].fNumerCoeffs [2] *= fTemp;
}
else
{
// Odd order - now do the 1st-order section
float fSPoleX = -fC * sinh (a);
float fSPoleY = 0;
BilinTransform (fSPoleX, fSPoleY, &fZPoleX, &fZPoleY);
if (mFilterSubtype == kLowPass) // LOWPASS
{
fZZeroX = -1;
fDCPoleDistSqr = sqrt(Calc2D_DistSqr (1, 0, fZPoleX, fZPoleY));
fDCPoleDistSqr /= 2; // dist from zero at Nyquist
}
else
{
// Highpass - do the digital LP->HP transform on the poles and zeroes
ComplexDiv (beta - fZPoleX, -fZPoleY, 1 - beta * fZPoleX, -beta * fZPoleY, &fZPoleX, &fZPoleY);
fZZeroX = 1;
fDCPoleDistSqr = sqrt(Calc2D_DistSqr (-1, 0, fZPoleX, fZPoleY)); // distance from Nyquist
fDCPoleDistSqr /= 2; // dist from zero at Nyquist
}
mpBiquad[(mOrder-1)/2].fNumerCoeffs [0] = fDCPoleDistSqr;
mpBiquad[(mOrder-1)/2].fNumerCoeffs [1] = -fZZeroX * fDCPoleDistSqr;
mpBiquad[(mOrder-1)/2].fNumerCoeffs [2] = 0;
mpBiquad[(mOrder-1)/2].fDenomCoeffs [0] = -fZPoleX;
mpBiquad[(mOrder-1)/2].fDenomCoeffs [1] = 0;
}
break;
case kChebyshevTypeII: // Chebyshev Type 2
float fSZeroX, fSZeroY;
float fSPoleX, fSPoleY;
eps = DB_TO_LINEAR(-wxMax(0.001, mStopbandRipple));
a = log (1 / eps + sqrt(1 / square(eps) + 1)) / mOrder;
// Assume even order
for (int iPair = 0; iPair < mOrder/2; iPair++)
{
ComplexDiv (fC, 0, -sinh (a) * sin ((2*iPair + 1) * PI / (2 * mOrder)),
cosh (a) * cos ((2*iPair + 1) * PI / (2 * mOrder)),
&fSPoleX, &fSPoleY);
BilinTransform (fSPoleX, fSPoleY, &fZPoleX, &fZPoleY);
fSZeroX = 0;
fSZeroY = fC / cos (((2 * iPair) + 1) * PI / (2 * mOrder));
BilinTransform (fSZeroX, fSZeroY, &fZZeroX, &fZZeroY);
if (mFilterSubtype == kLowPass) // LOWPASS
{
fDCPoleDistSqr = Calc2D_DistSqr (1, 0, fZPoleX, fZPoleY);
fDCPoleDistSqr /= Calc2D_DistSqr (1, 0, fZZeroX, fZZeroY);
}
else
{
// Highpass - do the digital LP->HP transform on the poles and zeroes
ComplexDiv (beta - fZPoleX, -fZPoleY, 1 - beta * fZPoleX, -beta * fZPoleY, &fZPoleX, &fZPoleY);
ComplexDiv (beta - fZZeroX, -fZZeroY, 1 - beta * fZZeroX, -beta * fZZeroY, &fZZeroX, &fZZeroY);
fDCPoleDistSqr = Calc2D_DistSqr (-1, 0, fZPoleX, fZPoleY); // distance from Nyquist
fDCPoleDistSqr /= Calc2D_DistSqr (-1, 0, fZZeroX, fZZeroY);
}
mpBiquad[iPair].fNumerCoeffs [0] = fDCPoleDistSqr;
mpBiquad[iPair].fNumerCoeffs [1] = -2 * fZZeroX * fDCPoleDistSqr;
mpBiquad[iPair].fNumerCoeffs [2] = (square(fZZeroX) + square(fZZeroY)) * fDCPoleDistSqr;
mpBiquad[iPair].fDenomCoeffs [0] = -2 * fZPoleX;
mpBiquad[iPair].fDenomCoeffs [1] = square(fZPoleX) + square(fZPoleY);
}
// Now, if it's odd order, we have one more to do
if (mOrder & 1)
{
int iPair = (mOrder-1)/2; // we'll do it as a biquad, but it's just first-order
ComplexDiv (fC, 0, -sinh (a) * sin ((2*iPair + 1) * PI / (2 * mOrder)),
cosh (a) * cos ((2*iPair + 1) * PI / (2 * mOrder)),
&fSPoleX, &fSPoleY);
BilinTransform (fSPoleX, fSPoleY, &fZPoleX, &fZPoleY);
fZZeroX = -1; // in the s-plane, the zero is at infinity
fZZeroY = 0;
if (mFilterSubtype == kLowPass) // LOWPASS
{
fDCPoleDistSqr = sqrt(Calc2D_DistSqr (1, 0, fZPoleX, fZPoleY));
fDCPoleDistSqr /= 2;
}
else
{
// Highpass - do the digital LP->HP transform on the poles and zeroes
ComplexDiv (beta - fZPoleX, -fZPoleY, 1 - beta * fZPoleX, -fZPoleY, &fZPoleX, &fZPoleY);
fZZeroX = 1;
fDCPoleDistSqr = sqrt(Calc2D_DistSqr (-1, 0, fZPoleX, fZPoleY)); // distance from Nyquist
fDCPoleDistSqr /= 2;
}
mpBiquad[iPair].fNumerCoeffs [0] = fDCPoleDistSqr;
mpBiquad[iPair].fNumerCoeffs [1] = -fZZeroX * fDCPoleDistSqr;
mpBiquad[iPair].fNumerCoeffs [2] = 0;
mpBiquad[iPair].fDenomCoeffs [0] = -fZPoleX;
mpBiquad[iPair].fDenomCoeffs [1] = 0;
}
break;
}
return true;
}
double EffectScienFilter::ChebyPoly(int Order, double NormFreq) // NormFreq = 1 at the f0 point (where response is R dB down)
{
// Calc cosh (Order * acosh (NormFreq));
double x = 1;
double fSum = 0;
wxASSERT (Order >= MIN_Order && Order <= MAX_Order);
for (int i = 0; i <= Order; i++)
{
fSum += s_fChebyCoeffs [Order-1][i] * x;
x *= NormFreq;
}
return fSum;
}
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);
switch (mFilterSubtype)
{
case 0: // lowpass
default:
Magn = sqrt (1 / (1 + square(eps) * square(ChebyPoly(mOrder, FreqWarped/CutoffWarped))));
break;
case 1:
Magn = sqrt (1 / (1 + square(eps) * square(ChebyPoly(mOrder, CutoffWarped/FreqWarped))));
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:
Magn = sqrt (1 / (1 + 1 / (square(eps) * square(ChebyPoly(mOrder, CutoffWarped/FreqWarped)))));
break;
case kHighPass:
Magn = sqrt (1 / (1 + 1 / (square(eps) * square(ChebyPoly(mOrder, FreqWarped/CutoffWarped)))));
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(EffectScienFilter *effect, wxWindow *parent)
: wxPanelWrapper(parent, wxID_ANY, 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;
}
EffectScienFilterPanel::~EffectScienFilterPanel()
{
if (mBitmap)
{
delete mBitmap;
}
}
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)
{
if (mBitmap)
{
delete mBitmap;
}
mWidth = width;
mHeight = height;
mBitmap = new wxBitmap(mWidth, mHeight);
}
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, wxSOLID));
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, wxSOLID));
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);
}