add oscillator class

This commit is contained in:
2026-01-16 22:55:28 -06:00
parent 2e8953e489
commit 198b7f0dde
4 changed files with 122 additions and 58 deletions

View File

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

View File

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

View File

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

View File

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