add oscillator class
This commit is contained in:
@@ -1,6 +1,63 @@
|
|||||||
|
|
||||||
#include "Oscillator.h"
|
#include "Oscillator.h"
|
||||||
|
|
||||||
// placeholder
|
void Oscillator::setWavetable(uint8_t waveTableId) {
|
||||||
|
activeWavetable_ = waveTableId;
|
||||||
|
}
|
||||||
|
|
||||||
// will eventually hold wavetable sampling, store phase state, and all that silliness
|
void Oscillator::setSampleRate(float sampleRate) {
|
||||||
|
sampleRate_ = sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Oscillator::frequency() {
|
||||||
|
return frequency_;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Oscillator::process(uint8_t note, bool& scopeTrigger) {
|
||||||
|
frequency_ = noteToFrequency(note);
|
||||||
|
return process(frequency_, scopeTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
float Oscillator::process(float frequency, bool& scopeTrigger) {
|
||||||
|
|
||||||
|
float sampleOut = 0.0f;
|
||||||
|
float pitchOffset = 0.5f;
|
||||||
|
float phaseInc = pitchOffset * 2.0f * M_PI * frequency / sampleRate_;
|
||||||
|
|
||||||
|
switch (activeWavetable_) {
|
||||||
|
case 0: // sine
|
||||||
|
sampleOut = std::sin(phase_) / 0.707f;
|
||||||
|
break;
|
||||||
|
case 1: // square
|
||||||
|
sampleOut = (phase_ >= M_PI) ? 1.0f : -1.0f;
|
||||||
|
break;
|
||||||
|
case 2: // saw
|
||||||
|
sampleOut = ((phase_ / M_PI) - 1.0f) / 0.577f;
|
||||||
|
break;
|
||||||
|
case 3: // triangle
|
||||||
|
if(phase_ <= M_PI/2.0f) {
|
||||||
|
sampleOut = phase_ * 2.0f/M_PI;
|
||||||
|
} else if(phase_ <= 3.0f*M_PI/2.0f) {
|
||||||
|
sampleOut = phase_ * -2.0f/M_PI + 2.0f;
|
||||||
|
} else {
|
||||||
|
sampleOut = phase_ * 2.0f/M_PI - 4.0f;
|
||||||
|
}
|
||||||
|
sampleOut /= 0.577f;
|
||||||
|
break;
|
||||||
|
default: // unreachable
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
phase_ += phaseInc;
|
||||||
|
if (phase_ > 2.0f * M_PI) {
|
||||||
|
phase_ -= 2.0f * M_PI;
|
||||||
|
scopeTrigger = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sampleOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inline float Oscillator::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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,50 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// placeholder
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#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
|
||||||
|
#define SYNTH_WAVETABLE_SIZE 2048
|
||||||
|
|
||||||
|
class Oscillator {
|
||||||
|
public:
|
||||||
|
|
||||||
|
Oscillator() = default;
|
||||||
|
~Oscillator() = default;
|
||||||
|
|
||||||
|
void setWavetable(uint8_t waveTableId);
|
||||||
|
void setSampleRate(float sampleRate);
|
||||||
|
float frequency();
|
||||||
|
|
||||||
|
float process(uint8_t note, bool& scopeTrigger);
|
||||||
|
float process(float frequency, bool& scopeTrigger);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
float sampleRate_ = 44100.0f;
|
||||||
|
|
||||||
|
inline float noteToFrequency(uint8_t note);
|
||||||
|
|
||||||
|
// internal state tracking
|
||||||
|
float phase_ = 0.0f;
|
||||||
|
uint8_t activeWavetable_;
|
||||||
|
float frequency_ = 220.0f;
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
// TODO: wavetable class that can load from files
|
||||||
|
// TODO: wavetables should be shared among the entire synth
|
||||||
|
std::array<float, SYNTH_WAVETABLE_SIZE> wavetable1_;
|
||||||
|
std::array<float, SYNTH_WAVETABLE_SIZE> wavetable2_;
|
||||||
|
std::array<float, SYNTH_WAVETABLE_SIZE> wavetable3_;
|
||||||
|
std::array<float, SYNTH_WAVETABLE_SIZE> wavetable4_;
|
||||||
|
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,12 +21,9 @@ void Voice::setSampleRate(float sampleRate) {
|
|||||||
filter2_.setSampleRate(sampleRate);
|
filter2_.setSampleRate(sampleRate);
|
||||||
|
|
||||||
// then foreach oscillator
|
// then foreach oscillator
|
||||||
//osc1_.setSampleRate(sampleRate);
|
for(Oscillator& o : oscillators_) {
|
||||||
|
o.setSampleRate(sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculates oscillator frequency based on midi note
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline float Voice::getParam(ParamId id) {
|
inline float Voice::getParam(ParamId id) {
|
||||||
@@ -36,13 +33,13 @@ inline float Voice::getParam(ParamId id) {
|
|||||||
void Voice::noteOn(int midiNote, float velocity) {
|
void Voice::noteOn(int midiNote, float velocity) {
|
||||||
note_ = midiNote;
|
note_ = midiNote;
|
||||||
velocity_ = velocity;
|
velocity_ = velocity;
|
||||||
frequency_ = noteToFrequency(midiNote);
|
|
||||||
active_ = true;
|
active_ = true;
|
||||||
|
|
||||||
// TODO: for each envelope ...
|
// TODO: for each envelope ...
|
||||||
gainEnvelope_.noteOn();
|
gainEnvelope_.noteOn();
|
||||||
cutoffEnvelope_.noteOn();
|
cutoffEnvelope_.noteOn();
|
||||||
resonanceEnvelope_.noteOn();
|
resonanceEnvelope_.noteOn();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Voice::noteOff() {
|
void Voice::noteOff() {
|
||||||
@@ -77,57 +74,26 @@ float Voice::process(float* params, bool& scopeTrigger) {
|
|||||||
float cutoffEnv = cutoffEnvelope_.process();
|
float cutoffEnv = cutoffEnvelope_.process();
|
||||||
float resonanceEnv = resonanceEnvelope_.process();
|
float resonanceEnv = resonanceEnvelope_.process();
|
||||||
|
|
||||||
// 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_);
|
|
||||||
|
|
||||||
// calculate the change that the velocity will make
|
// calculate the change that the velocity will make
|
||||||
// TODO: make velocity parameters configurable, probably also for filterCutoff and filterResonance
|
// TODO: make velocity parameters configurable, probably also for filterCutoff and filterResonance
|
||||||
float velocityGain = std::lerp(velocityCenter, velocity_, velocitySensitivity);
|
float velocityGain = std::lerp(velocityCenter, velocity_, velocitySensitivity);
|
||||||
|
|
||||||
float gain = gainEnv * getParam(ParamId::Osc1VolumeDepth) * velocityGain;
|
float gain = gainEnv * getParam(ParamId::Osc1VolumeDepth) * velocityGain;
|
||||||
float sampleOut = 0.0f;
|
|
||||||
|
|
||||||
// sample generation
|
// sample generation
|
||||||
// TODO: move this into the oscillator class
|
uint8_t osc1Wave = (static_cast<uint8_t>(std::round(getParam(ParamId::Osc1WaveSelector1))));
|
||||||
// TODO: wavetables
|
oscillators_[0].setWavetable(osc1Wave);
|
||||||
// TODO: wavetables should be scaled by their RMS for equal loudness (prelim standard = 0.707)
|
|
||||||
float sineSample = std::sin(phase_);
|
float sampleOut = oscillators_[0].process(note_, scopeTrigger) * gain;
|
||||||
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
|
// filter sample
|
||||||
float cutoffFreq = cutoffEnv * pow(2.0f, getParam(ParamId::FilterCutoffDepth)) * frequency_ * velocityGain;
|
float baseFreq = oscillators_[0].frequency();
|
||||||
|
float cutoffFreq = cutoffEnv * pow(2.0f, getParam(ParamId::FilterCutoffDepth)) * baseFreq * velocityGain;
|
||||||
float resonance = resonanceEnv * getParam(ParamId::FilterResonanceDepth) * velocityGain;
|
float resonance = resonanceEnv * getParam(ParamId::FilterResonanceDepth) * velocityGain;
|
||||||
filter1_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance);
|
filter1_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance);
|
||||||
filter2_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance);
|
filter2_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance);
|
||||||
sampleOut = filter1_.biquadProcess(sampleOut);
|
sampleOut = filter1_.biquadProcess(sampleOut);
|
||||||
sampleOut = filter2_.biquadProcess(sampleOut);
|
sampleOut = filter2_.biquadProcess(sampleOut);
|
||||||
|
|
||||||
// state tracking, may keep this here even if oscillators store their own phase because it might help with scope triggering
|
|
||||||
phase_ += phaseInc;
|
|
||||||
if (phase_ > 2.0f * M_PI) {
|
|
||||||
scopeTrigger = true;
|
|
||||||
phase_ -= 2.0f * M_PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sampleOut;
|
return sampleOut;
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,8 @@
|
|||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// TODO: you get it, also in a yml config
|
// TODO: make configurable
|
||||||
#define SYNTH_PITCH_STANDARD 440.0f // frequency of home pitch
|
#define SYNTH_OSCILLATOR_COUNT 3
|
||||||
#define SYNTH_MIDI_HOME 69 // midi note index of home pitch
|
|
||||||
#define SYNTH_NOTES_PER_OCTAVE 12
|
|
||||||
|
|
||||||
struct SmoothedParam {
|
struct SmoothedParam {
|
||||||
float current = 0.0f;
|
float current = 0.0f;
|
||||||
@@ -40,7 +38,7 @@ public:
|
|||||||
float process(float* params, bool& scopeTrigger);
|
float process(float* params, bool& scopeTrigger);
|
||||||
|
|
||||||
uint8_t note() { return note_; }
|
uint8_t note() { return note_; }
|
||||||
float frequency() { return frequency_; }
|
float frequency() { return oscillators_[0].frequency(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
@@ -52,12 +50,9 @@ private:
|
|||||||
uint8_t note_ = 0;
|
uint8_t note_ = 0;
|
||||||
float velocity_ = 1.0f;
|
float velocity_ = 1.0f;
|
||||||
bool active_ = false;
|
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;
|
float phase_ = 0.0f;
|
||||||
//Oscillator osc_; // example
|
|
||||||
|
std::array<Oscillator, 3> oscillators_;
|
||||||
|
|
||||||
// envelopes !!
|
// envelopes !!
|
||||||
// TODO: foreach envelope in vector<Envelope> envelopes_
|
// TODO: foreach envelope in vector<Envelope> envelopes_
|
||||||
|
|||||||
Reference in New Issue
Block a user