polyphony checkpoint
This commit is contained in:
@@ -7,6 +7,7 @@ KeyboardController::KeyboardController(NoteQueue& queue) : queue_(queue) {
|
|||||||
|
|
||||||
// TODO: also configurable via a yml
|
// TODO: also configurable via a yml
|
||||||
keymap_ = {
|
keymap_ = {
|
||||||
|
{ Qt::Key_Shift, 47 }, // B 2
|
||||||
{ Qt::Key_Z, 48 }, // C 3
|
{ Qt::Key_Z, 48 }, // C 3
|
||||||
{ Qt::Key_S, 49 }, // C#
|
{ Qt::Key_S, 49 }, // C#
|
||||||
{ Qt::Key_X, 50 }, // D
|
{ Qt::Key_X, 50 }, // D
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public:
|
|||||||
void setRelease(float seconds) { release_ = std::max(seconds, 0.001f); }
|
void setRelease(float seconds) { release_ = std::max(seconds, 0.001f); }
|
||||||
// values close to zero introduce that popping sound on noteOn/noteOffs
|
// values close to zero introduce that popping sound on noteOn/noteOffs
|
||||||
|
|
||||||
|
State state() { return state_; };
|
||||||
|
|
||||||
// note events
|
// note events
|
||||||
void noteOn();
|
void noteOn();
|
||||||
void noteOff();
|
void noteOff();
|
||||||
|
|||||||
@@ -25,48 +25,41 @@ void Synth::updateParams() {
|
|||||||
|
|
||||||
void Synth::setSampleRate(uint32_t sampleRate) {
|
void Synth::setSampleRate(uint32_t sampleRate) {
|
||||||
sampleRate_ = sampleRate;
|
sampleRate_ = sampleRate;
|
||||||
filter_.setSampleRate(static_cast<float>(sampleRate));
|
|
||||||
|
for(Voice& v : voices_) {
|
||||||
|
v.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline float Synth::noteToFrequency(uint8_t note) {
|
|
||||||
return SYNTH_PITCH_STANDARD * pow(2.0f, static_cast<float>(note - SYNTH_MIDI_HOME) / static_cast<float>(SYNTH_NOTES_PER_OCTAVE));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Synth::handleNoteEvent(const NoteEvent& event) {
|
void Synth::handleNoteEvent(const NoteEvent& event) {
|
||||||
if(event.type == NoteEventType::NoteOn) {
|
if(event.type == NoteEventType::NoteOn) {
|
||||||
// add note to activeNotes list
|
|
||||||
if (std::find(heldNotes_.begin(), heldNotes_.end(), event.note) == heldNotes_.end()) {
|
// TODO: find quietest voice and assign a note to it instead of just the first inactive one
|
||||||
heldNotes_.push_back(event.note);
|
// find inactive voice and start it with the given note
|
||||||
gainEnvelope_.noteOn();
|
for(Voice& v : voices_) {
|
||||||
cutoffEnvelope_.noteOn();
|
if(!v.isActive()) {
|
||||||
resonanceEnvelope_.noteOn();
|
v.noteOn(event.note, event.velocity);
|
||||||
// TODO: envelopes in an array so we can loop over them
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// remove note from activeNotes list
|
|
||||||
auto it = std::find(heldNotes_.begin(), heldNotes_.end(), event.note);
|
// find voice associated with note event and end it
|
||||||
if (it != heldNotes_.end()) {
|
for(Voice& v : voices_) {
|
||||||
heldNotes_.erase(it);
|
if(v.isActive() && v.note() == event.note) {
|
||||||
|
v.noteOff();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentNote();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Synth::updateCurrentNote() {
|
|
||||||
if(heldNotes_.empty()) {
|
|
||||||
gainEnvelope_.noteOff(); // TODO: move somewhere else when polyphony
|
|
||||||
cutoffEnvelope_.noteOff();
|
|
||||||
resonanceEnvelope_.noteOff();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t note = heldNotes_.back();
|
|
||||||
frequency_ = noteToFrequency(note);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
|
void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
|
||||||
@@ -82,63 +75,14 @@ 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
|
||||||
|
|
||||||
// process all envelopes
|
// assemble float array of parameters so that its easier for voices to retrieve
|
||||||
// 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 gainEnv = gainEnvelope_.process();
|
|
||||||
float cutoffEnv = cutoffEnvelope_.process();
|
|
||||||
float resonanceEnv = 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
|
|
||||||
|
|
||||||
// skip if no active notes
|
// foreach voice, process...
|
||||||
if(!gainEnvelope_.isActive()) {
|
float mix = 0.0f;
|
||||||
out[2*i] = 0.0f;
|
for(Voice& v : voices_) {
|
||||||
out[2*i+1] = 0.0f;
|
mix += v.process(¶ms_[0].current, triggered);
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make pitchOffset variable for each oscillator (maybe three values like octave, semitone offset, and pitch offset in cents)
|
|
||||||
float pitchOffset = 1.0f;
|
|
||||||
float phaseInc = pitchOffset * 2.0f * M_PI * frequency_ / static_cast<float>(sampleRate);
|
|
||||||
|
|
||||||
float gain = gainEnv * getParam(ParamId::Osc1VolumeDepth);
|
|
||||||
|
|
||||||
// sample generation
|
|
||||||
// TODO: wavetables
|
|
||||||
// TODO: wavetables should be scaled by their RMS for equal loudness (prelim standard = 0.707)
|
|
||||||
float sineSample = std::sin(phase_);
|
|
||||||
float squareSample = (phase_ >= M_PI) ? 0.707f : -0.707f;
|
|
||||||
float sawSample = ((phase_ / M_PI) - 1.0f) / 0.577f * 0.707f;
|
|
||||||
// switch statement will be replaced with an array index for our array of wavetables
|
|
||||||
switch (static_cast<int32_t>(std::round(getParam(ParamId::Osc1WaveSelector1)))) {
|
|
||||||
case 0:
|
|
||||||
sampleOut = sineSample * gain;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
sampleOut = squareSample * gain;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
sampleOut = sawSample * gain;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
// TODO: no triable wave yet :(
|
|
||||||
sampleOut = sineSample * gain;
|
|
||||||
break;
|
|
||||||
default: // unreachable
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter sample
|
|
||||||
float cutoffFreq = cutoffEnv * pow(2.0f, getParam(ParamId::FilterCutoffDepth)) * frequency_;
|
|
||||||
filter_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonanceEnv * getParam(ParamId::FilterResonanceDepth));
|
|
||||||
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
|
||||||
@@ -148,8 +92,9 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
|
|||||||
scope_->push(sampleOut); // visualization tap
|
scope_->push(sampleOut); // visualization tap
|
||||||
}
|
}
|
||||||
|
|
||||||
// sampling business
|
// triggering business
|
||||||
phase_ += phaseInc;
|
// TODO: get trigger info from voice (lowest frequency voice)
|
||||||
|
float phase_ = 0.0f;
|
||||||
if (phase_ > 2.0f * M_PI) {
|
if (phase_ > 2.0f * M_PI) {
|
||||||
phase_ -= 2.0f * M_PI;
|
phase_ -= 2.0f * M_PI;
|
||||||
if(!triggered) {
|
if(!triggered) {
|
||||||
|
|||||||
@@ -6,18 +6,11 @@
|
|||||||
#include "Envelope.h"
|
#include "Envelope.h"
|
||||||
#include "ScopeBuffer.h"
|
#include "ScopeBuffer.h"
|
||||||
#include "Filter.h"
|
#include "Filter.h"
|
||||||
|
#include "Voice.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
struct SmoothedParam {
|
|
||||||
float current = 0.0f;
|
|
||||||
float target = 0.0f;
|
|
||||||
float gain = 0.001f;
|
|
||||||
|
|
||||||
inline void update() { current += gain * (target - current); }
|
|
||||||
};
|
|
||||||
|
|
||||||
class Synth {
|
class Synth {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -42,35 +35,21 @@ private:
|
|||||||
// small getter that abstracts all the static casts and such
|
// small getter that abstracts all the static casts and such
|
||||||
inline float getParam(ParamId);
|
inline float getParam(ParamId);
|
||||||
|
|
||||||
// for calculating frequency based on midi note id
|
Voice* findFreeVoice();
|
||||||
inline float noteToFrequency(uint8_t note);
|
Voice* findVoiceByNote(uint8_t note);
|
||||||
|
|
||||||
// finds the active voice
|
|
||||||
void updateCurrentNote();
|
|
||||||
|
|
||||||
const ParameterStore& paramStore_;
|
const ParameterStore& paramStore_;
|
||||||
// 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_;
|
||||||
|
|
||||||
|
std::vector<uint8_t> heldNotes_;
|
||||||
|
|
||||||
|
// voices
|
||||||
|
static constexpr int MAX_VOICES = 12;
|
||||||
|
std::array<Voice, MAX_VOICES> voices_;
|
||||||
uint32_t sampleRate_;
|
uint32_t sampleRate_;
|
||||||
|
|
||||||
// for the scope
|
// for the scope
|
||||||
ScopeBuffer* scope_ = nullptr;
|
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
|
|
||||||
// TODO: put this in an oscillator class
|
|
||||||
float frequency_ = 220.0f;
|
|
||||||
float phase_ = 0.0f;
|
|
||||||
|
|
||||||
// envelopes !!
|
|
||||||
Envelope gainEnvelope_;
|
|
||||||
Envelope cutoffEnvelope_;
|
|
||||||
Envelope resonanceEnvelope_;
|
|
||||||
|
|
||||||
// filters, just one for now
|
|
||||||
Filter filter_;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,113 @@
|
|||||||
|
|
||||||
#include "Voice.h"
|
#include "Voice.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
// placeholder
|
Voice::Voice(std::array<SmoothedParam, PARAM_COUNT>* params) : params_(params) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::setSampleRate(float sampleRate) {
|
||||||
|
sampleRate_ = sampleRate;
|
||||||
|
|
||||||
|
// foreach envelope...
|
||||||
|
gainEnvelope_.setSampleRate(sampleRate);
|
||||||
|
cutoffEnvelope_.setSampleRate(sampleRate);
|
||||||
|
resonanceEnvelope_.setSampleRate(sampleRate);
|
||||||
|
|
||||||
|
// foreach filter...
|
||||||
|
filter1_.setSampleRate(sampleRate);
|
||||||
|
filter2_.setSampleRate(sampleRate);
|
||||||
|
|
||||||
|
// then foreach oscillator
|
||||||
|
//osc1_.setSampleRate(sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float Voice::noteToFrequency(uint8_t note) {
|
||||||
|
return SYNTH_PITCH_STANDARD * pow(2.0f, static_cast<float>(note - SYNTH_MIDI_HOME) / static_cast<float>(SYNTH_NOTES_PER_OCTAVE));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::noteOn(int midiNote, float velocity) {
|
||||||
|
note_ = midiNote;
|
||||||
|
velocity_ = velocity;
|
||||||
|
frequency_ = noteToFrequency(midiNote);
|
||||||
|
active_ = true;
|
||||||
|
|
||||||
|
// TODO: for each envelope ...
|
||||||
|
gainEnvelope_.noteOn();
|
||||||
|
cutoffEnvelope_.noteOn();
|
||||||
|
resonanceEnvelope_.noteOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::noteOff() {
|
||||||
|
// again, foreach
|
||||||
|
gainEnvelope_.noteOff();
|
||||||
|
cutoffEnvelope_.noteOff();
|
||||||
|
resonanceEnvelope_.noteOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Voice::isActive() {
|
||||||
|
return active_;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Voice::process(float* params, bool& scopeTrigger) {
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// skip if no active notes
|
||||||
|
if(!gainEnvelope_.isActive()) {
|
||||||
|
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float gainEnv = gainEnvelope_.process();
|
||||||
|
float cutoffEnv = cutoffEnvelope_.process();
|
||||||
|
float resonanceEnv = 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
|
||||||
|
|
||||||
|
// TODO: make pitchOffset variable for each oscillator (maybe three values like octave, semitone offset, and pitch offset in cents)
|
||||||
|
float pitchOffset = 1.0f;
|
||||||
|
float phaseInc = pitchOffset * 2.0f * M_PI * frequency_ / static_cast<float>(sampleRate);
|
||||||
|
|
||||||
|
float gain = gainEnv * getParam(ParamId::Osc1VolumeDepth);
|
||||||
|
float sampleOut = 0.0f;
|
||||||
|
|
||||||
|
// sample generation
|
||||||
|
// TODO: move this into the oscillator class
|
||||||
|
// TODO: wavetables
|
||||||
|
// TODO: wavetables should be scaled by their RMS for equal loudness (prelim standard = 0.707)
|
||||||
|
float sineSample = std::sin(phase_);
|
||||||
|
float squareSample = (phase_ >= M_PI) ? 0.707f : -0.707f;
|
||||||
|
float sawSample = ((phase_ / M_PI) - 1.0f) / 0.577f * 0.707f;
|
||||||
|
// switch statement will be replaced with an array index for our array of wavetables
|
||||||
|
switch (static_cast<int32_t>(std::round(getParam(ParamId::Osc1WaveSelector1)))) {
|
||||||
|
case 0:
|
||||||
|
sampleOut = sineSample * gain;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
sampleOut = squareSample * gain;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
sampleOut = sawSample * gain;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// TODO: no triable wave yet :(
|
||||||
|
sampleOut = sineSample * gain;
|
||||||
|
break;
|
||||||
|
default: // unreachable
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter sample
|
||||||
|
float cutoffFreq = cutoffEnv * pow(2.0f, getParam(ParamId::FilterCutoffDepth)) * frequency_;
|
||||||
|
filter1_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonanceEnv * getParam(ParamId::FilterResonanceDepth));
|
||||||
|
filter2_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonanceEnv * getParam(ParamId::FilterResonanceDepth));
|
||||||
|
sampleOut = filter1_.biquadProcess(sampleOut);
|
||||||
|
sampleOut = filter2_.biquadProcess(sampleOut);
|
||||||
|
|
||||||
|
return sampleOut;
|
||||||
|
}
|
||||||
@@ -1,4 +1,72 @@
|
|||||||
|
|
||||||
# pragma once
|
#pragma once
|
||||||
|
|
||||||
// placeholder
|
#include "Oscillator.h"
|
||||||
|
#include "Envelope.h"
|
||||||
|
#include "Filter.h"
|
||||||
|
#include "ParameterStore.h"
|
||||||
|
|
||||||
|
#ifndef M_PI // I hate my stupid chungus life
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// TODO: you get it, also in a yml config
|
||||||
|
#define SYNTH_PITCH_STANDARD 440.0f // frequency of home pitch
|
||||||
|
#define SYNTH_MIDI_HOME 69 // midi note index of home pitch
|
||||||
|
#define SYNTH_NOTES_PER_OCTAVE 12
|
||||||
|
|
||||||
|
struct SmoothedParam {
|
||||||
|
float current = 0.0f;
|
||||||
|
float target = 0.0f;
|
||||||
|
float gain = 0.001f;
|
||||||
|
|
||||||
|
inline void update() { current += gain * (target - current); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class Voice {
|
||||||
|
public:
|
||||||
|
|
||||||
|
Voice(std::array<SmoothedParam, PARAM_COUNT>* params);
|
||||||
|
~Voice() = default;
|
||||||
|
|
||||||
|
void setSampleRate(float sampleRate);
|
||||||
|
|
||||||
|
void noteOn(int midiNote, float velocity);
|
||||||
|
void noteOff();
|
||||||
|
|
||||||
|
bool isActive();
|
||||||
|
|
||||||
|
float process(float* params, bool& scopeTrigger);
|
||||||
|
|
||||||
|
uint8_t note() { return note_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
float sampleRate_ = 44100.0f;
|
||||||
|
|
||||||
|
inline float noteToFrequency(uint8_t note);
|
||||||
|
|
||||||
|
uint8_t note_ = 0;
|
||||||
|
float velocity_ = 1.0f;
|
||||||
|
bool active_ = false;
|
||||||
|
|
||||||
|
// here's where the actual sound generation happens
|
||||||
|
// TODO: put this in an oscillator class
|
||||||
|
float frequency_ = 220.0f;
|
||||||
|
float phase_ = 0.0f;
|
||||||
|
//Oscillator osc_; // example
|
||||||
|
|
||||||
|
// envelopes !!
|
||||||
|
// TODO: foreach envelope in vector<Envelope> envelopes_
|
||||||
|
Envelope gainEnvelope_;
|
||||||
|
Envelope cutoffEnvelope_;
|
||||||
|
Envelope resonanceEnvelope_;
|
||||||
|
|
||||||
|
// filters
|
||||||
|
Filter filter1_;
|
||||||
|
Filter filter2_;
|
||||||
|
|
||||||
|
// paramstore pointer
|
||||||
|
std::array<SmoothedParam, PARAM_COUNT>* params_;
|
||||||
|
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user