From 37b3c6ed6607c7b2b6094dfef74e632515917e38 Mon Sep 17 00:00:00 2001 From: Blitblank Date: Wed, 24 Dec 2025 22:25:42 -0600 Subject: [PATCH] implement envelopes --- CMakeLists.txt | 2 ++ README.md | 2 +- src/synth/Envelope.cpp | 52 ++++++++++++++++++++++++++++++++++++++++ src/synth/Envelope.h | 54 ++++++++++++++++++++++++++++++++++++++++++ src/synth/Synth.cpp | 20 +++++++++------- src/synth/Synth.h | 7 +++++- 6 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 src/synth/Envelope.cpp create mode 100644 src/synth/Envelope.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 001b6d2..004e535 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,8 @@ qt_add_executable(metabolus src/NoteQueue.h src/synth/AudioEngine.cpp src/synth/AudioEngine.h + src/synth/Envelope.cpp + src/synth/Envelope.h src/synth/Synth.cpp src/synth/Synth.h resources/resources.qrc diff --git a/README.md b/README.md index 28df2ed..5892ffd 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This synthesizer isn't very good, but it's neat :3 - [+] Add note control via either Midi or a keyboard. Coordinate on-off events to start and stop tone generation - [ ] Create a widget for this smart-slider to clean up the ui code -- [ ] Add envelope generation, attach to global volume for now. ADSR and such, +- [x] Add envelope generation, attach to global volume for now. ADSR and such, responds to note-on/note-off events - [ ] Make midi/keyboard control cross-platform. Use case will mostly be Midi -> linux and Keyboard -> windows though diff --git a/src/synth/Envelope.cpp b/src/synth/Envelope.cpp new file mode 100644 index 0000000..2edac52 --- /dev/null +++ b/src/synth/Envelope.cpp @@ -0,0 +1,52 @@ + +#include "Envelope.h" + +Envelope::Envelope() { + +} + +void Envelope::noteOn() { + state_ = State::Attack; +} + +void Envelope::noteOff() { + if(state_ != State::Idle) state_ = State::Release; +} + +float Envelope::process() { + + switch (state_) { + case State::Idle: + value_ = 0.0f; + break; + case State::Attack: + value_ += 1.0f / (attack_ * sampleRate_); + if(value_ >= 1.0f) { + value_ = 1.0f; + state_ = State::Decay; + } + break; + case State::Decay: + value_ -= (1.0f - sustain_) / (decay_ * sampleRate_); + if(value_ <= sustain_) { + value_ = sustain_; + state_ = State::Sustain; + } + break; + case State::Sustain: + // wait until release + break; + case State::Release: + value_ -= sustain_ / (release_ * sampleRate_); + if(value_ <= 0.0f) { + value_ = 0.0f; + state_ = State::Idle; + } + break; + default: // unreachable + break; + } + + return value_; +} + \ No newline at end of file diff --git a/src/synth/Envelope.h b/src/synth/Envelope.h new file mode 100644 index 0000000..02b64ce --- /dev/null +++ b/src/synth/Envelope.h @@ -0,0 +1,54 @@ + +#pragma once + +#include +#include + +enum class State { + Idle, + Attack, + Sustain, + Decay, + Release +}; + +class Envelope { + +public: + + Envelope(); + ~Envelope() = default; + + // setters + void setSampleRate(float sampleRate) { sampleRate_ = sampleRate; } + void setAttack(float seconds) { attack_ = std::max(seconds, 0.0001f); } + void setDecay(float seconds) { decay_ = std::max(seconds, 0.0001f); } + void setSustain(float level) { sustain_ = level; } + void setRelease(float seconds) { release_ = std::max(seconds, 0.0001f); } + // values close to zero introduce that popping sound on noteOn/noteOffs + + // note events + void noteOn(); + void noteOff(); + + // return current level + float process(); + + // determine if a note is playing or not + bool isActive() const { return state_ != State::Idle; } + +private: + + State state_ = State::Idle; + + float sampleRate_ = 44100.0f; + + float attack_ = 0.05f; // seconds + float decay_ = 0.2f; // seconds + float sustain_ = 0.7f; // level + float release_ = 0.2f; // seconds + + float value_ = 0.0f; + +}; + diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index b4a819f..105ca5c 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -8,6 +8,7 @@ #define M_PI 3.14159265358979323846 #endif +// TODO: you get it, also in a yml config #define SYNTH_PITCH_STANDARD 432.0f // frequency of home pitch #define SYNTH_MIDI_HOME 69 // midi note index of home pitch #define SYNTH_NOTES_PER_OCTAVE 12 @@ -36,6 +37,7 @@ void Synth::handleNoteEvent(const NoteEvent& event) { // add note to activeNotes list if (std::find(heldNotes_.begin(), heldNotes_.end(), event.note) == heldNotes_.end()) { heldNotes_.push_back(event.note); + gainEnvelope_.noteOn(); } } else { // remove note from activeNotes list @@ -50,13 +52,12 @@ void Synth::handleNoteEvent(const NoteEvent& event) { void Synth::updateCurrentNote() { if(heldNotes_.empty()) { - noteActive_ = false; + gainEnvelope_.noteOff(); // TODO: move somewhere else when polyphony return; } uint8_t note = heldNotes_.back(); frequency_ = noteToFrequency(note); - noteActive_ = true; } void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { @@ -71,10 +72,11 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { // updates internal buffered parameters for smoothing for(auto& p : params_) p.update(); // TODO: profile this - // skip if no note is being played - // TODO: this will be handled by an idle envelope eventually - // could also say gain = 0.0f; but w/e, this saves computing - if(!noteActive_) { + // process all envelopes + float gain = gainEnvelope_.process(); + + // skip if no active notes + if(!gainEnvelope_.isActive()) { out[2*i] = 0.0f; out[2*i+1] = 0.0f; continue; @@ -83,8 +85,10 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { float phaseInc = 2.0f * M_PI * frequency_ / static_cast(sampleRate); // sample generation - float gain = getParam(ParamId::Osc1Gain); - sampleOut = std::sin(phase_) * gain; + float sineSample = std::sin(phase_); + float squareSample = -0.707f; + if(phase_ >= M_PI) squareSample = 0.707f; + sampleOut = sineSample * gain; // write to buffer out[2*i] = sampleOut; // left diff --git a/src/synth/Synth.h b/src/synth/Synth.h index 76fc118..0fe44b1 100644 --- a/src/synth/Synth.h +++ b/src/synth/Synth.h @@ -3,6 +3,7 @@ #include "../ParameterStore.h" #include "../NoteQueue.h" +#include "Envelope.h" #include #include @@ -51,11 +52,15 @@ private: // here's where the actual sound generation happens // TODO: put this in an oscillator class - bool noteActive_ = false; 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 !! + // TODO: set these parameters via sliders + Envelope gainEnvelope_; + };