implement envelopes
This commit is contained in:
@@ -52,6 +52,8 @@ qt_add_executable(metabolus
|
|||||||
src/NoteQueue.h
|
src/NoteQueue.h
|
||||||
src/synth/AudioEngine.cpp
|
src/synth/AudioEngine.cpp
|
||||||
src/synth/AudioEngine.h
|
src/synth/AudioEngine.h
|
||||||
|
src/synth/Envelope.cpp
|
||||||
|
src/synth/Envelope.h
|
||||||
src/synth/Synth.cpp
|
src/synth/Synth.cpp
|
||||||
src/synth/Synth.h
|
src/synth/Synth.h
|
||||||
resources/resources.qrc
|
resources/resources.qrc
|
||||||
|
|||||||
@@ -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
|
- [+] Add note control via either Midi or a keyboard. Coordinate on-off events to
|
||||||
start and stop tone generation
|
start and stop tone generation
|
||||||
- [ ] Create a widget for this smart-slider to clean up the ui code
|
- [ ] 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
|
responds to note-on/note-off events
|
||||||
- [ ] Make midi/keyboard control cross-platform. Use case will mostly be
|
- [ ] Make midi/keyboard control cross-platform. Use case will mostly be
|
||||||
Midi -> linux and Keyboard -> windows though
|
Midi -> linux and Keyboard -> windows though
|
||||||
|
|||||||
52
src/synth/Envelope.cpp
Normal file
52
src/synth/Envelope.cpp
Normal 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
54
src/synth/Envelope.h
Normal 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;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// TODO: you get it, also in a yml config
|
||||||
#define SYNTH_PITCH_STANDARD 432.0f // frequency of home pitch
|
#define SYNTH_PITCH_STANDARD 432.0f // frequency of home pitch
|
||||||
#define SYNTH_MIDI_HOME 69 // midi note index of home pitch
|
#define SYNTH_MIDI_HOME 69 // midi note index of home pitch
|
||||||
#define SYNTH_NOTES_PER_OCTAVE 12
|
#define SYNTH_NOTES_PER_OCTAVE 12
|
||||||
@@ -36,6 +37,7 @@ void Synth::handleNoteEvent(const NoteEvent& event) {
|
|||||||
// add note to activeNotes list
|
// add note to activeNotes list
|
||||||
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();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// remove note from activeNotes list
|
// remove note from activeNotes list
|
||||||
@@ -50,13 +52,12 @@ void Synth::handleNoteEvent(const NoteEvent& event) {
|
|||||||
|
|
||||||
void Synth::updateCurrentNote() {
|
void Synth::updateCurrentNote() {
|
||||||
if(heldNotes_.empty()) {
|
if(heldNotes_.empty()) {
|
||||||
noteActive_ = false;
|
gainEnvelope_.noteOff(); // TODO: move somewhere else when polyphony
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t note = heldNotes_.back();
|
uint8_t note = heldNotes_.back();
|
||||||
frequency_ = noteToFrequency(note);
|
frequency_ = noteToFrequency(note);
|
||||||
noteActive_ = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
|
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
|
// updates internal buffered parameters for smoothing
|
||||||
for(auto& p : params_) p.update(); // TODO: profile this
|
for(auto& p : params_) p.update(); // TODO: profile this
|
||||||
|
|
||||||
// skip if no note is being played
|
// process all envelopes
|
||||||
// TODO: this will be handled by an idle envelope eventually
|
float gain = gainEnvelope_.process();
|
||||||
// could also say gain = 0.0f; but w/e, this saves computing
|
|
||||||
if(!noteActive_) {
|
// skip if no active notes
|
||||||
|
if(!gainEnvelope_.isActive()) {
|
||||||
out[2*i] = 0.0f;
|
out[2*i] = 0.0f;
|
||||||
out[2*i+1] = 0.0f;
|
out[2*i+1] = 0.0f;
|
||||||
continue;
|
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);
|
float phaseInc = 2.0f * M_PI * frequency_ / static_cast<float>(sampleRate);
|
||||||
|
|
||||||
// sample generation
|
// sample generation
|
||||||
float gain = getParam(ParamId::Osc1Gain);
|
float sineSample = std::sin(phase_);
|
||||||
sampleOut = std::sin(phase_) * gain;
|
float squareSample = -0.707f;
|
||||||
|
if(phase_ >= M_PI) squareSample = 0.707f;
|
||||||
|
sampleOut = sineSample * gain;
|
||||||
|
|
||||||
// write to buffer
|
// write to buffer
|
||||||
out[2*i] = sampleOut; // left
|
out[2*i] = sampleOut; // left
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "../ParameterStore.h"
|
#include "../ParameterStore.h"
|
||||||
#include "../NoteQueue.h"
|
#include "../NoteQueue.h"
|
||||||
|
#include "Envelope.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -51,11 +52,15 @@ private:
|
|||||||
|
|
||||||
// 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
|
||||||
bool noteActive_ = false;
|
|
||||||
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
|
// 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
|
// so non-zero elements are the ones currently being played
|
||||||
std::vector<uint8_t> heldNotes_;
|
std::vector<uint8_t> heldNotes_;
|
||||||
|
|
||||||
|
// envelopes !!
|
||||||
|
// TODO: set these parameters via sliders
|
||||||
|
Envelope gainEnvelope_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user