Montag, 11. März 2013

FILTER DESIGN IN 10 SCHRITTEN

Aufgrund zahlreicher Anfragen werde ich diesmal nicht auf die MS Technik eingehen und stattdessen zeigen, wie man Filter einbindet.

Zu diesem Beispiel benutze ich die Biquad Filter Klasse von Nigel Redmon (http://www.earlevel.com/main/2012/11/26/biquad-c-source-code/), welche zu diesem Zeitpunkt gratis und frei verwendet werden darf. Du kannst dir die Dateien Biquad.cpp und Biquad.h dort herunterladen.
Ausserdem gibt es dort auch eine (englisch sprachige) Erklärung, wie der Filter funktioniert und die Parameter übergeben werden.

LOS GEHT'S!
Erstelle ein neues Projekt und nenne es "FILTERTest"
Ich benutze folgende Dateinamen: "FILTERTest.cpp" und "FILTERTest.h".

Wir beginnen mit der Datei "FILTERTest.h"

Zuerst müssen wir mal wissen, welche und wieviele Parameter wir benötigen.
Ein Blick in die Datei Biquad.cpp oder Biquad.h verrät uns:

• Type
• Frequency
• Quality
• Gain

1. Diese Parameter müssen deklariert werden und in den Enumerator angegeben werden:

enum {
  kType = 0, //--Index beginnend von 0!
  kFreq1,  //-- == 1
  kQ1,    //-- == 2
  kGain1,  //-- == 3
  kNumParameters  // -- Insgesamt 4 Parameter, 0 bis 3 :-)
};

Dies passiert VOR der Klassendeklaration.
Schnell noch Programme, Eingänge, Ausgänge und ID des Plugins angeben...


// TODO: Change to reflect your plugin
const int kNumPrograms = 0;
const int kNumInputs = 2;
const int kNumOutputs = 2;
const unsigned long kUniqueId = 'XXXX';

Dann geht es weiter mit unserer Klasse "FILTERTest". Das kenne wir schon aus unserem ersten Plugin:

class FILTERTest : public AudioEffectX {
public:
  FILTERTest(audioMasterCallback audioMaster);
  ~FILTERTest();
  
  virtual VstInt32 canDo(char *text);
  virtual bool getEffectName(char* name);
  
  virtual float getParameter(VstInt32 index);
  virtual void getParameterDisplay(VstInt32 index, char *text);
  virtual void getParameterLabel(VstInt32 index, char *label);
  virtual void getParameterName(VstInt32 index, char *text);
  virtual VstPlugCategory getPlugCategory();
  
  virtual bool getProductString(char* text);
  virtual void getProgramName(char *name);
  virtual bool getVendorString(char* text);
  virtual VstInt32 getVendorVersion();

  virtual void process(float **inputs, float **outputs, VstInt32 sampleFrames);  
  virtual void processReplacing(float **inputs, float **outputs, VstInt32 sampleFrames);
  
  virtual void setParameter(VstInt32 index, float value);
  virtual void setProgramName(char *name);
  
private:

float fFreq1, fQ1, fGain1; //-- Deklaration der Parameter: Fließkommazahl!
int fType;  //-- Deklaration des ausgewählten Filtertyps: Hier kommt lediglich ein Integer zum Einsatz.

float fMaxFreq,fMinFreq; //-- Biquad arbeitet mit "echten" Werten: fMax == 20000.0, fMin == 20.0 usw.
float fMaxGain,fMinGain; //-- fMax == 30.0, fMin == -30.0
float fMaxQ,fMinQ;      //-- fMax == 0.01, fMin == 10.0
int fMaxType ,fMinType; //-- fMax == 6, fMin == 0 (Integerzahl)
};


Das wärs mal fürs erste mit der FILTERTest.h Datei!

//--------------------------------------------------------------------
//--------------------------------------------------------------------
//--------------------------------------------------------------------



2. WIR MACHEN WEITER MIT DER DATEI FILTERTest.cpp!


Dazu kopieren wir die Dateien Biquad.h und Biquad.cpp in unser Projekt, falls ihr das noch nicht gemacht habt.

Öffnet dann die Datei FILTERTest.cpp, definiert und bindet (include) folgende Datein ein:

#ifndef __FilterTest_H
#include "FilterTest.h"
#endif

#include "Biquad.h"

#include <math.h>

Die Einbindungen sind selbsterklärend, oder? 


3. Danach folgt unser Klassenkonstruktor mit der Initialisierung ALLER Parameter und Variablen

AudioEffect* createEffectInstance(audioMasterCallback audioMaster) {
return new FILTERTest(audioMaster);
}

FILTERTest::FILTERTest(audioMasterCallback audioMaster)
: AudioEffectX(audioMaster, kNumPrograms, kNumParameters) {
  setNumInputs(kNumInputs);
  setNumOutputs(kNumOutputs);
  setUniqueID(kUniqueId);
  
  fType = 0.0; //--Filter Type Init
  fFreq1 = 1000.0; //--Frequency Init
  fQ1 = 0.707; //--Quality Init
  fGain1 = 0; //--Gain Init
  
  
  fMaxFreq = 22000; //--Maximum Frequency
  fMinFreq = 10; //--Minimum Frequency
  fMaxQ = 10; //--Maximum Q
  fMinQ = 0.01; //--Minimum Q
  fMaxGain = 30; //--Maximum Faim
  fMinGain = -30; //--Minimum Gain
  fMaxType = 6.0; //--Maximum Type
  fMinType = 0.0; //--Minimum Type
  
  
  
}

Im Deconstructor und canDo() bleibt alles beim alten.
Wir geben unserem Plugin einen Namen:

FILTERTest::~FILTERTest() {
}

VstInt32 FILTERTest::canDo(char *text) {
  return 0;
}

bool FILTERTest::getEffectName(char* name) {
  strncpy(name, "FILTERTest", kVstMaxProductStrLen);
  return true;
}

4. Nun folgen einige "Switch" Anweisungen. Zuerst "getParameter()"


float FILTERTest::getParameter(VstInt32 index) {

  float v = 0.0; //-- Für "return v"
  
  switch (index) {
 
  case kType:
  v = fType;
  break;
  case kFreq1:
  v = fFreq1; 
  break;
  case kQ1:
  v = fQ1; 
  break;
  case kGain1:
  v = fGain1; 
  break;
  }
  return v;
}

5. Danach folgt das Parameter Display. Hier ist etwas Schreibarbeit nötig:


void FILTERTest::getParameterDisplay(VstInt32 index, char *text) {
  
    switch (index) {
  case kType:
if (fType == 0) //-- Unser vorher deklarierter Integer "fType" von 0 bis 6
{
vst_strncpy(text, "Low Pass", kVstMaxParamStrLen);
}
if (fType == 1)
{
vst_strncpy(text, "High Pass", kVstMaxParamStrLen);
}
if (fType == 2)
{
vst_strncpy(text, "Band Pass", kVstMaxParamStrLen);
}
if (fType == 3)
{
vst_strncpy(text, "Notch", kVstMaxParamStrLen);
}
if (fType == 4)
{
vst_strncpy(text, "Peak", kVstMaxParamStrLen);
}
if (fType == 5)
{
vst_strncpy(text, "Low Shelf", kVstMaxParamStrLen);
}
if (fType == 6)
{
vst_strncpy(text, "High Shelf", kVstMaxParamStrLen);
}
  break;
  case kFreq1:
float2string (fFreq1, text, kVstMaxParamStrLen); //--Testweise benutzen wir mal float2string()
  break;
  case kQ1:
float2string (fQ1, text, kVstMaxParamStrLen);
  break;
  case kGain1:
float2string (fGain1, text, kVstMaxParamStrLen);
  break;
  }
}


6. Das Label für unser Display

void FILTERTest::getParameterLabel(VstInt32 index, char *text) {

  switch(index){
  
case kType:
vst_strncpy(text, "", kVstMaxParamStrLen);
break;
case kFreq1:
vst_strncpy(text, "Hz", kVstMaxParamStrLen);
break;
case kQ1:
vst_strncpy(text, "Q", kVstMaxParamStrLen);
break;
case kGain1:
vst_strncpy(text, "dB", kVstMaxParamStrLen);
break;
  
  }
}


7. Der Name des jeweiligen Parameters

void FILTERTest::getParameterName(VstInt32 index, char *text) {
  
switch(index){
case kType:
vst_strncpy(text, "Type", kVstMaxParamStrLen);
break;
case kFreq1:
vst_strncpy(text, "Freq", kVstMaxParamStrLen);
break;
case kQ1:
vst_strncpy(text, "Qual", kVstMaxParamStrLen);
break;
case kGain1:
vst_strncpy(text, "Gain", kVstMaxParamStrLen);
break;
}
}

8. Nun folgen noch einige notwendige Angaben zum Plugin

VstPlugCategory FILTERTest::getPlugCategory() {
  return kPlugCategEffect;
}

bool FILTERTest::getProductString(char* text) {
  strcpy(text, "FILTERTest");
  return true;
}

void FILTERTest::getProgramName(char *name) {

}

bool FILTERTest::getVendorString(char* text) {
  strcpy(text, "CISDSPFACTORY");
  return true;
}

VstInt32 FILTERTest::getVendorVersion() {

  return 1000;
}



9. Nun folgt "setParameter()"

Die Parameter müssen im Format 20 - 20000 für Frequenz, 0.01 - 10 für Quality und -30 bis +30 für Gain übergeben werden. Bei dem Parameter fGain entscheiden wir uns für eine lineare Übergabe.
Da wir bei einer linearen Übergabe der Parameter sehr viel Verlust an Daten im Bassbereich bei fFreq1 hätten, basteln wir uns einfach eine logarithmische Skala. Deshalb haben wir die vordefinierte Headerdatei <math.h> eingebunden!


void FILTERTest::setParameter(VstInt32 index, float value) {
   //--Formel für logarithmische Skala (für besseres auswählen der tiefen Frequenzen)
   //--"fFreq1 = exp(log(20) + fFreq1 * (log(20000) - log(20)));"
   switch (index) {
  case kType:
  fType = value * (fMaxType - fMinType) + fMinType; //--Int - Formel für Auswahl des Filters 0 - 6 (siehe enum "Biquad.h") - keine Logarithmik nötig!
  break;
  case kFreq1:
  fFreq1 = exp(log(fMinFreq) + value * (log(fMaxFreq)-log(fMinFreq)));  //--Log
  break;
  case kQ1:
  fQ1 = exp(log(fMinQ) + value * (log(fMaxQ)-log(fMinQ))); //--Log
  break;
  case kGain1:
  fGain1 = value * (fMaxGain - fMinGain) + fMinGain; //--Lin
  break;
  }
}


AUFGABE: Probiere die lineare Übergabe der Frequenzen aus und sehe den Unterschied!


Da wir im Moment kein Programm für unseren Ersetzungseffekt brauchen, lassen wir setProgramName() frei.

void FILTERTest::setProgramName(char *name) {

}



10. EINBINDEN DES FILTERS

Wir erzeugen einfach aus der Biquad Klasse zwei neue Filter. Warum zwei? 
Pro Kanal einen. Diese können nicht gemeinasm genutzt werden!! 

//--INIT FILTERS--WICHTIG: AUSSERHALB EINER PROZEDUR!
Biquad *FilterL = new Biquad();
Biquad *FilterR = new Biquad();


So. Fertig mit der aufwändigsten Arbeit. Nun machen wir uns an die Verarbeitung der Daten!
Ein Blick in die Datei Biquad.cpp erklärt uns wie wir unsere Daten zu übergeben haben.
Unser vorher erstellter Filter "FilterL" wird mittels "setBiquad()" aktiviert und wir können die Variablen unserer Parameter übergeben. 

Und unseren Loop kochen wir nun auch ein wenig anders, und zwar in einer "for" Schleife.
Natürlich ist das auch mit einer while Schleife möglich, aber wir ersparen uns ein wenig Schreibarbeit.
Der Filter wird nun berechnet. Dies passiert mit der Anweisung "process()" aus der Datei "Biquad.h" und hat nichts mit der Anweisung "process()" aus der vstsdk2.4 Klasse zu tun!

void FILTERTest::process(float **inputs, float **outputs, VstInt32 sampleFrames) {

FilterL->setBiquad(fType, fFreq1 / sampleRate, fQ1, fGain1);
FilterR->setBiquad(fType, fFreq1 / sampleRate, fQ1, fGain1);

//--OUTPUT-LOOP------------------------------//
//------------------------------------------//
for(int i=0; i < sampleFrames; i++) { // i = 0...127
outputs[0][i] = FilterL->process((inputs[0][i]));//inputs[0][i];
outputs[1][i] = FilterR->process((inputs[1][i]));//inputs[1][i];
}

}


Nun kopieren wir den gesamten process() Block und fügen ihn ein. Einzige Änderung: 
Aus process() wird processReplacing().


void FILTERTest::processReplacing(float **inputs, float **outputs, VstInt32 sampleFrames) {

FilterL->setBiquad(fType, fFreq1 / sampleRate, fQ1, fGain1);
FilterR->setBiquad(fType, fFreq1 / sampleRate, fQ1, fGain1);


//--OUTPUT-LOOP------------------------------//
//------------------------------------------//
for(int i=0; i < sampleFrames; i++) { // i = 0...127
outputs[0][i] = FilterL->process((inputs[0][i]));//inputs[0][i];
outputs[1][i] = FilterR->process((inputs[1][i]));//inputs[1][i];
}

}



So, das wars! So einfach ist das mit den Filterklassen. 
Danke fürs Lesen meines Blogs. Falls noch Fragen auftauchen, dann stellt sie!

Chris


PS: Auf der zu Beginn erwähnten Seite http://www.earlevel.com/main/2012/11/26/biquad-c-source-code/ findest du auch sehr viel Information zu Filterdesign und DSP Programmierung.