implement envelopes

This commit is contained in:
2025-12-24 22:25:42 -06:00
parent 544e1d1849
commit 37b3c6ed66
6 changed files with 127 additions and 10 deletions

View File

@@ -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

View File

@@ -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

52
src/synth/Envelope.cpp Normal file
View File

@@ -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_;
}

54
src/synth/Envelope.h Normal file
View File

@@ -0,0 +1,54 @@
#pragma once
#include <cstdint>
#include <algorithm>
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;
};

View File

@@ -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<float>(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

View File

@@ -3,6 +3,7 @@
#include "../ParameterStore.h"
#include "../NoteQueue.h"
#include "Envelope.h"
#include <vector>
#include <atomic>
@@ -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<uint8_t> heldNotes_;
// envelopes !!
// TODO: set these parameters via sliders
Envelope gainEnvelope_;
};