4 Commits

Author SHA1 Message Date
2cfc49bac3 feat: all filter types 2024-01-17 08:56:59 +07:00
29ceabca74 wip: State Variable Filter (FILTER LFO WORKS NOW!!!) 2024-01-17 08:56:58 +07:00
4fd0cb279d wip: moog filter mentioned 2024-01-17 08:56:58 +07:00
61c0d3b787 wip: state-variable filter doc 2024-01-17 08:56:58 +07:00
17 changed files with 363 additions and 114 deletions

214
docs/StateVariableFilter.md Normal file
View File

@@ -0,0 +1,214 @@
https://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/
The frequency control coefficient, f, is defined as
![Alt text](image.png)
where Fs is the sample rate and Fc is the filters corner frequency you want to set. The q coefficient is defined as
![Alt text](image-1.png)
where Q normally ranges from 0.5 to inifinity (where the filter oscillates).
The main drawback of the digital state variable is that it becomes unstable at higher frequencies. It depends on the Q setting, but basically the upper bound of stability is about where f reaches 1, which is at one-sixth of the sample rate (8 kHz at 48 kHz). The only way around this is to oversample. A simple way to double the filters sample rate (and thereby double the filters frequency range) is to run the filter twice with the same input sample, and discard one output sample.
example with double-sampling
```
input = input buffer;
output = output buffer;
fs = sampling frequency;
fc = cutoff frequency normally something like:
440.0*pow(2.0, (midi_note - 69.0)/12.0);
res = resonance 0 to 1;
drive = internal distortion 0 to 0.1
freq = 2.0*sin(PI*MIN(0.25, fc/(fs*2))); // the fs*2 is because it's double sampled
damp = MIN(2.0*(1.0 - pow(res, 0.25)), MIN(2.0, 2.0/freq - freq*0.5));
notch = notch output
low = low pass output
high = high pass output
band = band pass output
peak = peaking output = low - high
--
double sampled svf loop:
for (i=0; i<numSamples; i++)
{
in = input[i];
notch = in - damp*band;
low = low + freq*band;
high = notch - low;
band = freq*high + band - drive*band*band*band;
out = 0.5*(notch or low or high or band or peak);
notch = in - damp*band;
low = low + freq*band;
high = notch - low;
band = freq*high + band - drive*band*band*band;
out += 0.5*(same out as above);
output[i] = out;
}
```
Also that could work as it's a voltage-controlled filter
https://www.musicdsp.org/en/latest/Filters/24-moog-vcf.html
```
//Init
cutoff = cutoff freq in Hz
fs = sampling frequency //(e.g. 44100Hz)
res = resonance [0 - 1] //(minimum - maximum)
f = 2 * cutoff / fs; //[0 - 1]
k = 3.6*f - 1.6*f*f -1; //(Empirical tunning)
p = (k+1)*0.5;
scale = e^((1-p)*1.386249;
r = res*scale;
y4 = output;
y1=y2=y3=y4=oldx=oldy1=oldy2=oldy3=0;
//Loop
//--Inverted feed back for corner peaking
x = input - r*y4;
//Four cascaded onepole filters (bilinear transform)
y1=x*p + oldx*p - k*y1;
y2=y1*p+oldy1*p - k*y2;
y3=y2*p+oldy2*p - k*y3;
y4=y3*p+oldy3*p - k*y4;
//Clipper band limited sigmoid
y4 = y4 - (y4^3)/6;
oldx = x;
oldy1 = y1;
oldy2 = y2;
oldy3 = y3;
```
```
#pragma once
namespace DistoCore
{
template<class T>
class MoogFilter
{
public:
MoogFilter();
~MoogFilter() {};
T getSampleRate() const { return sampleRate; }
void setSampleRate(T fs) { sampleRate = fs; calc(); }
T getResonance() const { return resonance; }
void setResonance(T filterRezo) { resonance = filterRezo; calc(); }
T getCutoff() const { return cutoff; }
T getCutoffHz() const { return cutoff * sampleRate * 0.5; }
void setCutoff(T filterCutoff) { cutoff = filterCutoff; calc(); }
void init();
void calc();
T process(T input);
// filter an input sample using normalized params
T filter(T input, T cutoff, T resonance);
protected:
// cutoff and resonance [0 - 1]
T cutoff;
T resonance;
T sampleRate;
T fs;
T y1,y2,y3,y4;
T oldx;
T oldy1,oldy2,oldy3;
T x;
T r;
T p;
T k;
};
/**
* Construct Moog-filter.
*/
template<class T>
MoogFilter<T>::MoogFilter()
: sampleRate(T(44100.0))
, cutoff(T(1.0))
, resonance(T(0.0))
{
init();
}
/**
* Initialize filter buffers.
*/
template<class T>
void MoogFilter<T>::init()
{
// initialize values
y1=y2=y3=y4=oldx=oldy1=oldy2=oldy3=T(0.0);
calc();
}
/**
* Calculate coefficients.
*/
template<class T>
void MoogFilter<T>::calc()
{
// TODO: replace with your constant
const double kPi = 3.1415926535897931;
// empirical tuning
p = cutoff * (T(1.8) - T(0.8) * cutoff);
// k = p + p - T(1.0);
// A much better tuning seems to be:
k = T(2.0) * sin(cutoff * kPi * T(0.5)) - T(1.0);
T t1 = (T(1.0) - p) * T(1.386249);
T t2 = T(12.0) + t1 * t1;
r = resonance * (t2 + T(6.0) * t1) / (t2 - T(6.0) * t1);
};
/**
* Process single sample.
*/
template<class T>
T MoogFilter<T>::process(T input)
{
// process input
x = input - r * y4;
// four cascaded one-pole filters (bilinear transform)
y1 = x * p + oldx * p - k * y1;
y2 = y1 * p + oldy1 * p - k * y2;
y3 = y2 * p + oldy2 * p - k * y3;
y4 = y3 * p + oldy3 * p - k * y4;
// clipper band limited sigmoid
y4 -= (y4 * y4 * y4) / T(6.0);
oldx = x; oldy1 = y1; oldy2 = y2; oldy3 = y3;
return y4;
}
/**
* Filter single sample using specified params.
*/
template<class T>
T MoogFilter<T>::filter(T input, T filterCutoff, T filterRezo)
{
// set params first
cutoff = filterCutoff;
resonance = filterRezo;
calc();
return process(input);
}
}
```

BIN
docs/image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
docs/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

19
docs/matrix.md Normal file
View File

@@ -0,0 +1,19 @@
// signal -> adsr -> filter
// ^ ^ ^
// | | |
// can be modulated
// by lfo, or adsr
у каждого из них должна быть ручка для изменения состояния
на каждом семпле?????
багует сам алгоритм изменения частоты, либо алгоритм пересчета коэфициентов фильтра

View File

@@ -1,14 +1,17 @@
#pragma once
#include "Filter.h"
class BandPassFilter : public Filter {
private:
void CalculateCoefficients() override;
protected:
float GetSampleForFilterType() override;
public:
BandPassFilter();
BandPassFilter(Filter* filter);
BandPassFilter(float freq, float res, float q);
BandPassFilter(/* args */);
~BandPassFilter();
bool IsSameFilterType(FilterType type) override { return type == BandPass; };
};

View File

@@ -1,24 +1,25 @@
#pragma once
#include "IEffect.h"
#include "Settings.h"
enum FilterType {
LowPass,
BandPass,
HighPass
};
enum FilterType { LowPass, BandPass, HighPass };
class Filter : public IEffect {
protected:
float m_freq; // cutoff frequency
float m_q; // filter quantity (resonance)
float m_order; // filter order (peakGain)
/* todo: filter adsr */
float m_norm, m_v, m_k;
float m_a0, m_a1, m_a2, m_b1, m_b2;
float m_z1, m_z2;
void CalculateNormals();
virtual void CalculateCoefficients(){};
// float* m_output; // output buffer
float m_fs = SAMPLE_RATE; // sampling frequency;
float m_fc; // cutoff frequency normally something like: 440.0*pow(2.0,
// (midi_note - 69.0)/12.0);
float m_res; // resonance 0 to 1;
float m_drive; // internal distortion 0 to 0.1
float m_freq;
float m_damp;
float m_notcho; // notch output
float m_lowo; // low pass output
float m_higho; // high pass output
float m_bando; // band pass output
float m_peako; // peaking output = low - high
virtual float GetSampleForFilterType(){ return 0.0; };
public:
Filter(/* args */);
@@ -27,9 +28,9 @@ class Filter : public IEffect {
void Release() override final;
float Process(float in);
void Process(std::vector<float>& samples) override final;
void SetParameters(float freq, float res, float q);
float GetFreq() { return m_freq; }
float GetRes() { return m_q; }
float GetPeakGain() { return m_norm; }
void SetParameters(float freq, float res, float drive);
float GetFreq() { return m_fc; }
float GetRes() { return m_res; }
float GetPeakGain() { return m_drive; }
virtual bool IsSameFilterType(FilterType type) { return false; };
};

View File

@@ -2,8 +2,8 @@
#include "Filter.h"
class HighPassFilter : public Filter {
private:
void CalculateCoefficients() override;
protected:
float GetSampleForFilterType() override;
public:
HighPassFilter();

View File

@@ -11,10 +11,3 @@ public:
void SetFreq(float freq) { m_phase_dt = (this->*m_dt_function)(freq); }
};
LFO::LFO(/* args */): Oscillator(Sine, 0.f, 0.5f)
{
}
LFO::~LFO()
{
}

View File

@@ -4,7 +4,7 @@
class LowPassFilter : public Filter {
protected:
void CalculateCoefficients() override;
float GetSampleForFilterType() override;
public:
LowPassFilter();
@@ -13,3 +13,4 @@ class LowPassFilter : public Filter {
~LowPassFilter();
bool IsSameFilterType(FilterType type) override { return type == LowPass; };
};

View File

@@ -8,6 +8,7 @@
#include "Oscillator.h"
#include "Settings.h"
#include <vector>
#include "LFO.h"
class Synth {
private:
@@ -15,7 +16,7 @@ class Synth {
std::vector<Oscillator*> m_oscillators;
std::vector<IEffect*> m_effects;
std::vector<float> m_out_signal;
Oscillator* m_lfo;
LFO* m_lfo;
void ZeroSignal();
void GetNote();
void TriggerNoteOnEffects();

View File

@@ -1,23 +1,22 @@
#include "BandPassFilter.h"
#include "Settings.h"
BandPassFilter::BandPassFilter(/* args */) {}
BandPassFilter::BandPassFilter(Filter* filter) {
m_freq = filter->GetFreq();
m_q = filter->GetRes();
m_order = filter->GetPeakGain();
BandPassFilter::BandPassFilter() {
SetParameters(200, 0.1, 0.001);
}
BandPassFilter::BandPassFilter(float freq, float res, float q) {}
BandPassFilter::BandPassFilter(
Filter* filter) {
SetParameters(filter->GetFreq(), filter->GetRes(), filter->GetPeakGain());
}
BandPassFilter::BandPassFilter(float freq, float res,
float q) {
SetParameters(freq, res, q);
}
BandPassFilter::~BandPassFilter() {}
void BandPassFilter::CalculateCoefficients() {
CalculateNormals();
m_norm = 1 / (1 + m_k / m_q + m_k * m_k);
m_a0 = m_k / m_q * m_norm;
m_a1 = 0;
m_a2 = -m_a0;
m_b1 = 2 * (m_k * m_k - 1) * m_norm;
m_b2 = (1 - m_k / m_q + m_k * m_k) * m_norm;
float BandPassFilter::GetSampleForFilterType() {
return m_lowo;
}

View File

@@ -1,36 +1,50 @@
#include "Filter.h"
#include "Settings.h"
#include <algorithm>
#include <cmath>
Filter::Filter(/* args */) {}
Filter::~Filter() {}
void Filter::CalculateNormals() {
m_v = powf(10, fabs(m_order) / 20.0);
m_k = tanf(M_PI * m_freq);
}
void Filter::Trigger() {}
void Filter::Release() {}
float Filter::Process(float in) {
// may move to a compile-time dictionary calculation, if needed
CalculateCoefficients();
float out = in * m_a0 + m_z1;
m_z1 = in * m_a1 + m_z2 - m_b1 * out;
m_z2 = in * m_a2 - m_b2 * out;
return out;
}
void Filter::Process(std::vector<float>& samples) {
for (std::size_t i = 0; i < samples.size(); i++) {
samples[i] = Process(samples[i]);
}
}
void Filter::SetParameters(float freq, float res, float q) {
m_freq = freq / SAMPLE_RATE;
m_q = res;
m_order = q;
float Filter::Process(float in) {
m_notcho = in - m_damp * m_bando;
m_lowo = m_lowo + m_freq * m_bando;
m_higho = m_notcho - m_lowo;
m_bando =
m_freq * m_higho + m_bando - m_drive * m_bando * m_bando * m_bando;
// (m_notcho or m_lowo or m_higho or m_bando or m_peako)
float out = 0.5 * GetSampleForFilterType();
m_notcho = in - m_damp * m_bando;
m_lowo = m_lowo + m_freq * m_bando;
m_higho = m_notcho - m_lowo;
m_bando =
m_freq * m_higho + m_bando - m_drive * m_bando * m_bando * m_bando;
out += 0.5 * GetSampleForFilterType();
return out;
}
void Filter::SetParameters(float freq, float res, float drive) {
m_fc = freq;
m_res = res;
m_drive = drive;
// the fs*2 is because it's double sampled
m_freq =
2.0 * std::sinf(SYNTH_PI * std::min(0.25f, m_fc / (m_fs * 2)));
m_damp = std::min(2.0f * (1.0f - std::powf(m_res, 0.25f)),
std::min(2.0f, 2.0f / m_freq - m_freq * 0.5f));
}

View File

@@ -1,23 +1,22 @@
#include "HighPassFilter.h"
#include "Settings.h"
HighPassFilter::HighPassFilter(/* args */) {}
HighPassFilter::HighPassFilter(Filter* filter) {
m_freq = filter->GetFreq();
m_q = filter->GetRes();
m_order = filter->GetPeakGain();
HighPassFilter::HighPassFilter() {
SetParameters(200, 0.1, 0.001);
}
HighPassFilter::HighPassFilter(float freq, float res, float q) {}
HighPassFilter::HighPassFilter(
Filter* filter) {
SetParameters(filter->GetFreq(), filter->GetRes(), filter->GetPeakGain());
}
HighPassFilter::HighPassFilter(float freq, float res,
float q) {
SetParameters(freq, res, q);
}
HighPassFilter::~HighPassFilter() {}
void HighPassFilter::CalculateCoefficients() {
CalculateNormals();
m_norm = 1 / (1 + m_k / m_q + m_k * m_k);
m_a0 = 1 * m_norm;
m_a1 = -2 * m_a0;
m_a2 = m_a0;
m_b1 = 2 * (m_k * m_k - 1) * m_norm;
m_b2 = (1 - m_k / m_q + m_k * m_k) * m_norm;
float HighPassFilter::GetSampleForFilterType() {
return m_higho;
}

10
src/LFO.cpp Normal file
View File

@@ -0,0 +1,10 @@
#include "LFO.h"
LFO::LFO(/* args */): Oscillator(Sine, 0.f, 0.5f)
{
}
LFO::~LFO()
{
}

View File

@@ -2,32 +2,21 @@
#include "Settings.h"
LowPassFilter::LowPassFilter() {
// todo: defaults
m_freq = 200.f / SAMPLE_RATE;
m_q = 1.0f;//0.707f;
m_order = 0;
SetParameters(200, 0.1, 0.001);
}
LowPassFilter::LowPassFilter(float freq, float res, float q) {
m_freq = freq / SAMPLE_RATE;
m_q = res;
m_order = q;
LowPassFilter::LowPassFilter(
Filter* filter) {
SetParameters(filter->GetFreq(), filter->GetRes(), filter->GetPeakGain());
}
LowPassFilter::LowPassFilter(Filter* filter) {
m_freq = filter->GetFreq();
m_q = filter->GetRes();
m_order = filter->GetPeakGain();
LowPassFilter::LowPassFilter(float freq, float res,
float q) {
SetParameters(freq, res, q);
}
LowPassFilter::~LowPassFilter() {}
void LowPassFilter::CalculateCoefficients() {
CalculateNormals();
m_norm = 1 / (1 + m_k / m_q + m_k * m_k);
m_a0 = m_k * m_k * m_norm;
m_a1 = 2 * m_a0;
m_a2 = m_a0;
m_b1 = 2 * (m_k * m_k - 1) * m_norm;
m_b2 = (1 - m_k / m_q + m_k * m_k) * m_norm;
float LowPassFilter::GetSampleForFilterType() {
return m_lowo;
}

View File

@@ -257,6 +257,10 @@ float Renderer::DrawFilterPanel(Synth& synth, FilterGuiState& gui_filter,
// apply values to real one
// todo: thrid (order) parameter
// todo: why resonance changing does not work?
// todo: limit filter lowest frequency to ~40 hz
if (gui_filter.freq < 40.0) {
gui_filter.freq = 50.0;
}
filter->SetParameters(gui_filter.freq, filter->GetRes(),
filter->GetPeakGain());

View File

@@ -1,13 +1,14 @@
#include "Synth.h"
#include "FilterFactory.h"
#include "KeyBoard.h"
#include "Logger.h"
#include "OscillatorType.h"
#include "Settings.h"
#include "FilterFactory.h"
#include "LFO.h"
#include "LowPassFilter.h"
Synth::Synth(/* args */) {
m_lfo = new LFO();
m_lfo->SetFreq(5.0);
AddOscillator();
AddOscillator();
AddEffect(new ADSR());
@@ -38,8 +39,14 @@ void Synth::GetNote() {
}
void Synth::ApplyEffects() {
for (IEffect* effect : m_effects) {
effect->Process(m_out_signal);
auto* adsr = m_effects[0];
adsr->Process(m_out_signal);
Filter* filter = (Filter*)m_effects[1];
for (std::size_t i = 0; i < m_out_signal.size(); i++) {
ApplyFilterLfo();
m_out_signal[i] = filter->Process(m_out_signal[i]);
}
}
@@ -56,8 +63,7 @@ void Synth::UntriggerNoteOnEffects() {
}
void Synth::AddOscillator() {
m_oscillators.push_back(
new Oscillator(OscillatorType::Sine, 0.0f, VOLUME));
m_oscillators.push_back(new Oscillator(OscillatorType::Sine, 0.0f, VOLUME));
}
void Synth::Trigger(Note input) {
@@ -70,19 +76,14 @@ void Synth::Trigger(Note input) {
TriggerNoteOnEffects();
}
// todo: fix this
void Synth::ApplyFilterLfo() {
float dt = m_lfo->Process();
Filter* filter = (Filter*)m_effects[1];
float freq = filter->GetFreq();
//todo: check formula
//filter->SetParameters(freq + dt * 0.2f, filter->GetRes(), filter->GetPeakGain());
filter->SetParameters(freq + dt, filter->GetRes(), filter->GetPeakGain());
}
void Synth::Process() {
//todo: on each sample.
//in order to do that, we need to move to per-sample processing
//ApplyFilterLfo();
GetNote();
ApplyEffects();
}
@@ -98,6 +99,7 @@ void Synth::AddEffect(IEffect* fx) { m_effects.push_back(fx); }
void Synth::SetFilter(FilterType type) {
Filter* old_filter = this->GetFilter();
if (!old_filter->IsSameFilterType(type)) {
// todo: implement other types of state variable filters;
Filter* new_filter = FilterFactory::CreateFilter(old_filter, type);
delete old_filter;
m_effects[1] = new_filter;