diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e3e9d5..cb3626f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,12 @@ qt_add_executable(metabolus src/synth/Synth.h src/synth/ScopeBuffer.cpp 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 src/ui/widgets/SmartSlider/SmartSlider.cpp src/ui/widgets/SmartSlider/SmartSlider.h diff --git a/src/ParameterStore.h b/src/ParameterStore.h index c04308e..0fca5ff 100644 --- a/src/ParameterStore.h +++ b/src/ParameterStore.h @@ -66,15 +66,15 @@ constexpr std::array(ParamId::Count)> PARAM_DE { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD { 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvA - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvD - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvS - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvR - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvA - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvD - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvS - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvR - { 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector1 + { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvA + { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvD + { 1000.f, 0.0f, 40000.f}, // FilterCutoffEnvS + { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvR + { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvA + { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvD + { 0.707f, 0.0f, 2.0f}, // FilterResonanceEnvS + { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvR + { 0.0f, 0.0f, 0.0f}, // Osc1WaveSelector1 { 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector2 }}; diff --git a/src/synth/Filter.cpp b/src/synth/Filter.cpp new file mode 100644 index 0000000..9abd1e3 --- /dev/null +++ b/src/synth/Filter.cpp @@ -0,0 +1,87 @@ + +#include "Filter.h" + +#include +#include + +#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; +} diff --git a/src/synth/Filter.h b/src/synth/Filter.h new file mode 100644 index 0000000..239c6be --- /dev/null +++ b/src/synth/Filter.h @@ -0,0 +1,46 @@ + +#pragma once + +#include + +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_; + +}; \ No newline at end of file diff --git a/src/synth/Oscillator.cpp b/src/synth/Oscillator.cpp new file mode 100644 index 0000000..93dd432 --- /dev/null +++ b/src/synth/Oscillator.cpp @@ -0,0 +1,4 @@ + +#include "Oscillator.h" + +// placeholder diff --git a/src/synth/Oscillator.h b/src/synth/Oscillator.h new file mode 100644 index 0000000..84440e1 --- /dev/null +++ b/src/synth/Oscillator.h @@ -0,0 +1,4 @@ + +# pragma once + +// placeholder diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index 09a6b94..6f1cd5a 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -23,6 +23,11 @@ void Synth::updateParams() { } } +void Synth::setSampleRate(uint32_t sampleRate) { + sampleRate_ = sampleRate; + filter_.setSampleRate(static_cast(sampleRate)); +} + inline float Synth::getParam(ParamId id) { return params_[static_cast(id)].current; } @@ -37,6 +42,9 @@ void Synth::handleNoteEvent(const NoteEvent& event) { if (std::find(heldNotes_.begin(), heldNotes_.end(), event.note) == heldNotes_.end()) { heldNotes_.push_back(event.note); gainEnvelope_.noteOn(); + cutoffEnvelope_.noteOn(); + resonanceEnvelope_.noteOn(); + // TODO: envelopes in an array so we can loop over them } } else { // remove note from activeNotes list @@ -52,6 +60,8 @@ void Synth::handleNoteEvent(const NoteEvent& event) { void Synth::updateCurrentNote() { if(heldNotes_.empty()) { gainEnvelope_.noteOff(); // TODO: move somewhere else when polyphony + cutoffEnvelope_.noteOff(); + resonanceEnvelope_.noteOff(); return; } @@ -75,7 +85,10 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { // process all envelopes // 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)); + 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(); + 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: // 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; scope_->push(0.0f); 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(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(sampleRate); // sample generation // TODO: wavetables @@ -114,6 +131,9 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { break; } + // filter sample + sampleOut = filter_.biquadProcess(sampleOut); + // write to buffer out[2*i] = sampleOut; // left out[2*i+1] = sampleOut; // right diff --git a/src/synth/Synth.h b/src/synth/Synth.h index da4fd9e..5d14b24 100644 --- a/src/synth/Synth.h +++ b/src/synth/Synth.h @@ -5,6 +5,7 @@ #include "../NoteQueue.h" #include "Envelope.h" #include "ScopeBuffer.h" +#include "Filter.h" #include #include @@ -30,7 +31,7 @@ public: void handleNoteEvent(const NoteEvent& event); // setters - void setSampleRate(uint32_t sampleRate) { sampleRate_ = sampleRate; } + void setSampleRate(uint32_t sampleRate); void setScopeBuffer(ScopeBuffer* scope) { scope_ = scope; } private: @@ -51,20 +52,25 @@ private: // smoothed params creates a buffer in case the thread controlling paramStore gets blocked std::array params_; 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 heldNotes_; // here's where the actual sound generation happens // TODO: put this in an oscillator class float frequency_ = 220.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 heldNotes_; - // envelopes !! Envelope gainEnvelope_; + Envelope cutoffEnvelope_; + Envelope resonanceEnvelope_; - // for the scope - ScopeBuffer* scope_ = nullptr; + // filters, just one for now + Filter filter_; }; diff --git a/src/synth/Voice.cpp b/src/synth/Voice.cpp new file mode 100644 index 0000000..5957268 --- /dev/null +++ b/src/synth/Voice.cpp @@ -0,0 +1,4 @@ + +#include "Voice.h" + +// placeholder diff --git a/src/synth/Voice.h b/src/synth/Voice.h new file mode 100644 index 0000000..84440e1 --- /dev/null +++ b/src/synth/Voice.h @@ -0,0 +1,4 @@ + +# pragma once + +// placeholder diff --git a/src/ui/widgets/SmartSlider/SmartSlider.ui b/src/ui/widgets/SmartSlider/SmartSlider.ui index 1d0122b..1484e8b 100644 --- a/src/ui/widgets/SmartSlider/SmartSlider.ui +++ b/src/ui/widgets/SmartSlider/SmartSlider.ui @@ -117,6 +117,9 @@ QAbstractSpinBox::ButtonSymbols::NoButtons + + 80000.000000000000000 +