add filter (needs fixes)

This commit is contained in:
2025-12-27 17:51:57 -06:00
parent 814002f0d9
commit b36d68ae99
11 changed files with 201 additions and 17 deletions

View File

@@ -59,6 +59,12 @@ qt_add_executable(metabolus
src/synth/Synth.h src/synth/Synth.h
src/synth/ScopeBuffer.cpp src/synth/ScopeBuffer.cpp
src/synth/ScopeBuffer.h src/synth/ScopeBuffer.h
src/synth/Filter.cpp
src/synth/Filter.h
src/synth/Oscillator.cpp
src/synth/Oscillator.h
src/synth/Voice.cpp
src/synth/Voice.h
resources/resources.qrc resources/resources.qrc
src/ui/widgets/SmartSlider/SmartSlider.cpp src/ui/widgets/SmartSlider/SmartSlider.cpp
src/ui/widgets/SmartSlider/SmartSlider.h src/ui/widgets/SmartSlider/SmartSlider.h

View File

@@ -66,15 +66,15 @@ constexpr std::array<ParamDefault, static_cast<size_t>(ParamId::Count)> PARAM_DE
{ 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD
{ 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS { 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS
{ 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvA { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvA
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvD { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvD
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvS { 1000.f, 0.0f, 40000.f}, // FilterCutoffEnvS
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvR { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvR
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvA { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvA
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvD { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvD
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvS { 0.707f, 0.0f, 2.0f}, // FilterResonanceEnvS
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvR { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvR
{ 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector1 { 0.0f, 0.0f, 0.0f}, // Osc1WaveSelector1
{ 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector2 { 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector2
}}; }};

87
src/synth/Filter.cpp Normal file
View File

@@ -0,0 +1,87 @@
#include "Filter.h"
#include <cmath>
#include <iostream>
#ifndef M_PI // I hate my stupid chungus life
#define M_PI 3.14159265358979323846
#endif
void Filter::setSampleRate(float sampleRate) {
sampleRate_ = sampleRate;
calculateCoefficients();
}
void Filter::setParams(Type type, float frequency, float q) {
type_ = type;
frequency_ = frequency;
q_ = q;
calculateCoefficients();
}
float Filter::biquadProcess(float in) {
// calculate filtered sample
float out = a0_ * in + z1_;
// update states
z1_ = a1_ * in - b1_ * out + z2_;
z2_ = a2_ * in - b2_ * out;
return out;
}
void Filter::calculateCoefficients() {
if(q_ < 0.001f) q_ = 0.001f;
float omega = 2.0f * M_PI * frequency_ / sampleRate_;
float sinOmega = std::sin(omega);
float cosOmega = std::cos(omega);
float alpha = sinOmega / (2.0f * q_);
float b0, b1, b2, a0, a1, a2;
switch (type_) {
case Type::BiquadLowpass:
b0 = (1.0f - cosOmega) * 0.5f;
b1 = 1.0f - cosOmega;
b2 = (1.0f - cosOmega) * 0.5f;
a0 = 1.0f + alpha;
a1 = -2.0f * cosOmega;
a2 = 1.0f - alpha;
break;
case Type::BiquadHighpass:
b0 = (1.0f + cosOmega) * 0.5f;
b1 = -(1.0f + cosOmega);
b2 = (1.0f + cosOmega) * 0.5f;
a0 = 1.0f + alpha;
a1 = -2.0f * cosOmega;
a2 = 1.0f - alpha;
break;
case Type::BiquadBandpass:
b0 = sinOmega * 0.5f;
b1 = 0.0f;
b2 = -sinOmega * 0.5f;
a0 = 1.0f + alpha;
a1 = -2.0f * cosOmega;
a2 = 1.0f - alpha;
break;
}
// Normalize
a0_ = b0 / a0;
a1_ = b1 / a0;
a2_ = b2 / a0;
b1_ = a1 / a0;
b2_ = a2 / a0;
}
void Filter::resetSate() {
z1_ = 0.0f;
z2_ = 0.0f;
}

46
src/synth/Filter.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <cstdint>
class Filter {
public:
enum class Type : uint16_t {
BiquadLowpass,
BiquadNotch,
BiquadBandpass,
BiquadHighpass
};
Filter() = default;
~Filter() = default;
void setSampleRate(float sampleRate);
void setParams(Type type, float frequency, float q);
float biquadProcess(float in);
void resetSate();
// TODO: add more filter types here
// high pass
// band pass
// notch
// https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
// instead of making different calculate functions, have an enum which specifies the filter type
// one public calculate which the enum is passed through and a private calculate for the specific types, switch statement to choose
private:
void calculateCoefficients();
Type type_ = Type::BiquadLowpass;
float sampleRate_ = 44100.0f;
float frequency_ = 6000.0f;
float q_ = 0.707f;
// biquad filter structure
float a0_, a1_, a2_, b1_, b2_;
float z1_, z2_;
};

4
src/synth/Oscillator.cpp Normal file
View File

@@ -0,0 +1,4 @@
#include "Oscillator.h"
// placeholder

4
src/synth/Oscillator.h Normal file
View File

@@ -0,0 +1,4 @@
# pragma once
// placeholder

View File

@@ -23,6 +23,11 @@ void Synth::updateParams() {
} }
} }
void Synth::setSampleRate(uint32_t sampleRate) {
sampleRate_ = sampleRate;
filter_.setSampleRate(static_cast<float>(sampleRate));
}
inline float Synth::getParam(ParamId id) { inline float Synth::getParam(ParamId id) {
return params_[static_cast<size_t>(id)].current; return params_[static_cast<size_t>(id)].current;
} }
@@ -37,6 +42,9 @@ void Synth::handleNoteEvent(const NoteEvent& event) {
if (std::find(heldNotes_.begin(), heldNotes_.end(), event.note) == heldNotes_.end()) { if (std::find(heldNotes_.begin(), heldNotes_.end(), event.note) == heldNotes_.end()) {
heldNotes_.push_back(event.note); heldNotes_.push_back(event.note);
gainEnvelope_.noteOn(); gainEnvelope_.noteOn();
cutoffEnvelope_.noteOn();
resonanceEnvelope_.noteOn();
// TODO: envelopes in an array so we can loop over them
} }
} else { } else {
// remove note from activeNotes list // remove note from activeNotes list
@@ -52,6 +60,8 @@ void Synth::handleNoteEvent(const NoteEvent& event) {
void Synth::updateCurrentNote() { void Synth::updateCurrentNote() {
if(heldNotes_.empty()) { if(heldNotes_.empty()) {
gainEnvelope_.noteOff(); // TODO: move somewhere else when polyphony gainEnvelope_.noteOff(); // TODO: move somewhere else when polyphony
cutoffEnvelope_.noteOff();
resonanceEnvelope_.noteOff();
return; return;
} }
@@ -75,7 +85,10 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
// process all envelopes // process all envelopes
// should be easy enough if all the envelopes are in an array to loop over them // should be easy enough if all the envelopes are in an array to loop over them
gainEnvelope_.set(getParam(ParamId::Osc1VolumeEnvA), getParam(ParamId::Osc1VolumeEnvD), getParam(ParamId::Osc1VolumeEnvS), getParam(ParamId::Osc1VolumeEnvR)); gainEnvelope_.set(getParam(ParamId::Osc1VolumeEnvA), getParam(ParamId::Osc1VolumeEnvD), getParam(ParamId::Osc1VolumeEnvS), getParam(ParamId::Osc1VolumeEnvR));
cutoffEnvelope_.set(getParam(ParamId::FilterCutoffEnvA), getParam(ParamId::FilterCutoffEnvD), getParam(ParamId::FilterCutoffEnvS), getParam(ParamId::FilterCutoffEnvR));
resonanceEnvelope_.set(getParam(ParamId::FilterResonanceEnvA), getParam(ParamId::FilterResonanceEnvD), getParam(ParamId::FilterResonanceEnvS), getParam(ParamId::FilterResonanceEnvR));
float gain = gainEnvelope_.process(); float gain = gainEnvelope_.process();
filter_.setParams(Filter::Type::BiquadLowpass, cutoffEnvelope_.process(), resonanceEnvelope_.process());
// TODO: envelope is shared between all notes so this sequence involves a note change but only one envelope attack: // TODO: envelope is shared between all notes so this sequence involves a note change but only one envelope attack:
// NOTE_A_ON > NOTE_B_ON > NOTE_A_OFF and note B starts playing part-way through note A's envelope // NOTE_A_ON > NOTE_B_ON > NOTE_A_OFF and note B starts playing part-way through note A's envelope
@@ -85,9 +98,13 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
out[2*i+1] = 0.0f; out[2*i+1] = 0.0f;
scope_->push(0.0f); scope_->push(0.0f);
continue; continue;
// TODO: should I have a write() function ?
// maybe we change the synth.process into just returning a single float and the write can be in audioEngine
} }
float phaseInc = 2.0f * M_PI * frequency_ / static_cast<float>(sampleRate); // TODO: make pitchOffset variable for each oscillator (maybe three values like octave, semitone offset, and pitch offset in cents)
float pitchOffset = 0.5f;
float phaseInc = pitchOffset * 2.0f * M_PI * frequency_ / static_cast<float>(sampleRate);
// sample generation // sample generation
// TODO: wavetables // TODO: wavetables
@@ -114,6 +131,9 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
break; break;
} }
// filter sample
sampleOut = filter_.biquadProcess(sampleOut);
// write to buffer // write to buffer
out[2*i] = sampleOut; // left out[2*i] = sampleOut; // left
out[2*i+1] = sampleOut; // right out[2*i+1] = sampleOut; // right

View File

@@ -5,6 +5,7 @@
#include "../NoteQueue.h" #include "../NoteQueue.h"
#include "Envelope.h" #include "Envelope.h"
#include "ScopeBuffer.h" #include "ScopeBuffer.h"
#include "Filter.h"
#include <vector> #include <vector>
#include <atomic> #include <atomic>
@@ -30,7 +31,7 @@ public:
void handleNoteEvent(const NoteEvent& event); void handleNoteEvent(const NoteEvent& event);
// setters // setters
void setSampleRate(uint32_t sampleRate) { sampleRate_ = sampleRate; } void setSampleRate(uint32_t sampleRate);
void setScopeBuffer(ScopeBuffer* scope) { scope_ = scope; } void setScopeBuffer(ScopeBuffer* scope) { scope_ = scope; }
private: private:
@@ -51,20 +52,25 @@ private:
// smoothed params creates a buffer in case the thread controlling paramStore gets blocked // smoothed params creates a buffer in case the thread controlling paramStore gets blocked
std::array<SmoothedParam, PARAM_COUNT> params_; std::array<SmoothedParam, PARAM_COUNT> params_;
uint32_t sampleRate_; uint32_t sampleRate_;
// for the scope
ScopeBuffer* scope_ = nullptr;
// TODO: might make this a fixed array where index=midi-note and the value=velocity
// so non-zero elements are the ones currently being played
std::vector<uint8_t> heldNotes_;
// here's where the actual sound generation happens // here's where the actual sound generation happens
// TODO: put this in an oscillator class // TODO: put this in an oscillator class
float frequency_ = 220.0f; float frequency_ = 220.0f;
float phase_ = 0.0f; float phase_ = 0.0f;
// TODO: might make this a fixed array where index=midi-note and the value=velocity
// so non-zero elements are the ones currently being played
std::vector<uint8_t> heldNotes_;
// envelopes !! // envelopes !!
Envelope gainEnvelope_; Envelope gainEnvelope_;
Envelope cutoffEnvelope_;
Envelope resonanceEnvelope_;
// for the scope // filters, just one for now
ScopeBuffer* scope_ = nullptr; Filter filter_;
}; };

4
src/synth/Voice.cpp Normal file
View File

@@ -0,0 +1,4 @@
#include "Voice.h"
// placeholder

4
src/synth/Voice.h Normal file
View File

@@ -0,0 +1,4 @@
# pragma once
// placeholder

View File

@@ -117,6 +117,9 @@
<property name="buttonSymbols"> <property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum> <enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property> </property>
<property name="maximum">
<double>80000.000000000000000</double>
</property>
</widget> </widget>
<widget class="Line" name="line"> <widget class="Line" name="line">
<property name="geometry"> <property name="geometry">