974 lines
26 KiB
C++
974 lines
26 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 "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");
|
|
}
|
|
|
|
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<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);
|
|
}
|