From 198b7f0dde7897175fb864848f80889eeb5973f4 Mon Sep 17 00:00:00 2001 From: Blitblank Date: Fri, 16 Jan 2026 22:55:28 -0600 Subject: [PATCH] add oscillator class --- src/synth/Oscillator.cpp | 61 ++++++++++++++++++++++++++++++++++++++-- src/synth/Oscillator.h | 50 ++++++++++++++++++++++++++++++-- src/synth/Voice.cpp | 54 +++++++---------------------------- src/synth/Voice.h | 15 ++++------ 4 files changed, 122 insertions(+), 58 deletions(-) diff --git a/src/synth/Oscillator.cpp b/src/synth/Oscillator.cpp index bc8c857..fc236a4 100644 --- a/src/synth/Oscillator.cpp +++ b/src/synth/Oscillator.cpp @@ -1,6 +1,63 @@ #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(note - SYNTH_MIDI_HOME) / static_cast(SYNTH_NOTES_PER_OCTAVE)); +} diff --git a/src/synth/Oscillator.h b/src/synth/Oscillator.h index 84440e1..3ecf14a 100644 --- a/src/synth/Oscillator.h +++ b/src/synth/Oscillator.h @@ -1,4 +1,50 @@ -# pragma once +#pragma once -// placeholder +#include +#include +#include + +#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 wavetable1_; + std::array wavetable2_; + std::array wavetable3_; + std::array wavetable4_; + +}; diff --git a/src/synth/Voice.cpp b/src/synth/Voice.cpp index a3a15b2..77c80a0 100644 --- a/src/synth/Voice.cpp +++ b/src/synth/Voice.cpp @@ -21,12 +21,9 @@ void Voice::setSampleRate(float sampleRate) { filter2_.setSampleRate(sampleRate); // then foreach oscillator - //osc1_.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(note - SYNTH_MIDI_HOME) / static_cast(SYNTH_NOTES_PER_OCTAVE)); + for(Oscillator& o : oscillators_) { + o.setSampleRate(sampleRate); + } } inline float Voice::getParam(ParamId id) { @@ -36,13 +33,13 @@ inline float Voice::getParam(ParamId id) { 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() { @@ -77,57 +74,26 @@ float Voice::process(float* params, bool& scopeTrigger) { float cutoffEnv = cutoffEnvelope_.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(sampleRate_); - // calculate the change that the velocity will make // TODO: make velocity parameters configurable, probably also for filterCutoff and filterResonance float velocityGain = std::lerp(velocityCenter, velocity_, velocitySensitivity); float gain = gainEnv * getParam(ParamId::Osc1VolumeDepth) * velocityGain; - 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(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; - } + uint8_t osc1Wave = (static_cast(std::round(getParam(ParamId::Osc1WaveSelector1)))); + oscillators_[0].setWavetable(osc1Wave); + + float sampleOut = oscillators_[0].process(note_, scopeTrigger) * gain; // 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; filter1_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance); filter2_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance); sampleOut = filter1_.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; } \ No newline at end of file diff --git a/src/synth/Voice.h b/src/synth/Voice.h index 79c6233..cb75731 100644 --- a/src/synth/Voice.h +++ b/src/synth/Voice.h @@ -10,10 +10,8 @@ #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 +// TODO: make configurable +#define SYNTH_OSCILLATOR_COUNT 3 struct SmoothedParam { float current = 0.0f; @@ -40,7 +38,7 @@ public: float process(float* params, bool& scopeTrigger); uint8_t note() { return note_; } - float frequency() { return frequency_; } + float frequency() { return oscillators_[0].frequency(); } private: @@ -52,12 +50,9 @@ private: 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 + + std::array oscillators_; // envelopes !! // TODO: foreach envelope in vector envelopes_